深入理解KVO

iOS | KVO | Objective-C

KVO的本质是什么,如何手动触发KVO?


1.什么是KVO

KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变

添加监听:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef enum NSKeyValueObservingOptions : NSUInteger {
// 新值(包含于回调方法change字典中)
NSKeyValueObservingOptionNew = 0x01,
// 旧值(包含于回调方法change字典中)
NSKeyValueObservingOptionOld = 0x02,
// 观察最初的值(在注册观察服务时会调用一次触发方法)
NSKeyValueObservingOptionInitial = 0x04,
// 分别在值修改前后触发方法(即一次修改有两次触发)
NSKeyValueObservingOptionPrior = 0x08
} NSKeyValueObservingOptions;

/**
监听属性方法,方法调用者为被观察对象

@param observer 观察者/订阅者
@param keyPath 要观察的属性
@param options 监听变化条件
@param context 上下文,将会传递到监听回调函数中
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

监听回调:

1
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSString*, id> *)change context:(nullable void *)context;

移除监听:

1
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;

KVO使用大家都比较熟悉,Demo应该就没有写的必要了,下面我们直接来探索下本质

2.KVO的本质

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.

self.person1 = [[Person alloc] init];
self.person1.age = 10;

self.person2 = [[Person alloc] init];
self.person2.age = 15;

// 输出 >> 监听之前:Person, Person
NSLog(@"监听之前:%@, %@", object_getClass(self.person1), object_getClass(self.person2));
// 输出 >> 监听之前 setter 方法:0x10d2fb550, 0x10d2fb550
NSLog(@"监听之前 setter 方法:%p, %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];

// 输出 >> 监听之后:NSKVONotifying_Person, Person
NSLog(@"监听之前:%@, %@", object_getClass(self.person1), object_getClass(self.person2));
// 输出 >> 监听之后 setter 方法:0x10d643bf4, 0x10d2fb550
NSLog(@"监听之后 setter 方法:%p, %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);

// (lldb)po ([NSKVONotifying_Person class]).superclass Person
// (lldb) p (IMP)0x10d2fb550 (IMP) $0 = 0x000000010d2fb550 (KVO与KVC`-[Person setAge:] at Person.h:13)
// (lldb) p (IMP)0x10d643bf4 (IMP) $1 = 0x000000010d643bf4 (Foundation`_NSSetLongLongValueAndNotify)
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person1.age = 20;
self.person2.age = 25;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"监听到:%@ 的:%@ 属性值改变了,change:%@ - context:%@", object, keyPath, change, context);
}

通过断点调试,我们发现 person2 的类对象没有发生变化,person1 的类对象变成了 NSKVONotifying_Person,而且是Person的子类。

使用 Runtime 打印 NSKVONotifying_Person 方法列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)printMethodNamesOfClass:(Class)cls {
unsigned int methCount = 0;
NSMutableArray *methodArr = [NSMutableArray array];
Method *meths = class_copyMethodList(cls, &methCount);
for(int i = 0; i < methCount; i++) {
Method meth = meths[i];
SEL sel = method_getName(meth);
NSString *methodName = NSStringFromSelector(sel);
[methodArr addObject:methodName];
}
if (methodArr.count) NSLog(@"%@", methodArr);
free(meths);
}

  • NSKVONotifying_Person 实例方法列表:
    • setAge:
    • class
    • dealloc
    • _isKVOA

总结:
1> 添加监听时,利用RuntimeAPI动态生成Person子类: NSKVONotifying_Person ,并且使 person1 的 isa 指针指向新的类
2> 重写 setAge: ,person1 调用 setter 方法时会从 NSKVONotifying_Person 开始查找,在自己的类对象中能够找到,所以会调用自己的 setAge:方法( 会调用Foundation的_NSSetValueAndNotify函数)
3> _NSSet
ValueAndNotify 调用流程:willChangeValueForKey -> [super setAge:] (Person 的 setter 方法) -> didChangeValueForKey(同时触发 observeValueForKeyPath 监听回调方法,订阅者接收)
4> class 方法:重写 class 方法的目的是什么呢?(lldb) po self.person1.class 输出为:Person,原来,苹果粑粑是想要隐藏NSKVONotifying_Person,让开发者无感,使用时与未添加监听时无异
5> dealloc方法:释放 KVO 新产生的资源
6> _isKVOA方法:标记这个新类 KVO 机制新建的

willChangeValueForKey/didChangeValueForKey还有疑惑的同学可以在 Person.m 中对这两个方法进行重写,再进行调试以变理解。