Skip to main content

KVO

原理

Key-Value Observing Implementation Details

- (void)viewDidLoad {
[super viewDidLoad];

self.demo1 = [[DemoObject alloc] init];
self.demo2 = [[DemoObject alloc] init];

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

NSLog(@"%@", object_getClass(self.demo1)); // NSKVONotifying_DemoObject
NSLog(@"%@", object_getClass(self.demo2)); // DemoObject
}

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {
NSLog(@"%@", keyPath);
NSLog(@"%@", object);
NSLog(@"%@", change);
}

可以看到,添加了 KVO 观察者后,对象的类发生了改变。实际上是系统在运行时动态为 DemoObject 创建了子类 NSKVONotifying_DemoObject,并将 demo1 实例对象的 isa 指针指向了这个新的子类。然后,通过重写子类的 set 方法,来实现 KVO。

img

通过断点调试打印方法的实现,可以看到,在添加观察者后,setCount: 的方法实现变成了 Foundation 内部的方法 _NSSetIntValueAndNotify(根据属性的不同类型,Foundation 内部会调用不同的方法)。

由于 KVO 的关键是重写 set 方法,因此直接修改成员变量显然是不可能触发 KVO 的。

KVO 的内部实现是类似于这样的:

- (void)setCount:(NSUInteger)count {
_NSSetIntValueAndNotify();
}

void _NSSetIntValueAndNotify() {
[self willChangeValueForKey:@"count"];
[super setCount:count];
[self didChangeValueForKey:@"count"];
}

- (void)didChangeValueForKey:(NSString *)key {
// 在这里通知观察者
}

可以通过 runtime 打印 NSKVONotifying_DemoObject 类对象里的方法列表,来窥探其内部实现了哪些方法:

- (void)printMethodListWithClass:(Class)cls {
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i < count; i++) {
Method *method = &methodList[i];
NSString *name = NSStringFromSelector(method_getName(*method));
NSLog(@"%@", name);
}
free(methodList);
}

/** 输出:
2022-06-26 17:01:27.445053+0800 ocdemo[15670:4354180] setCount:
2022-06-26 17:01:27.445115+0800 ocdemo[15670:4354180] class
2022-06-26 17:01:27.445161+0800 ocdemo[15670:4354180] dealloc
2022-06-26 17:01:27.445196+0800 ocdemo[15670:4354180] _isKVOA
*/

如果希望手动触发 KVO,可以通过调用 willChangeValueForKey:didChangeValueForKey: 来触发。

KVC

KVC 指的是 NSObject 遵循的 NSKeyValueCoding 协议:

- valueForKey:
- valueForKeyPath:
- setValue:forKey:
- setValue:forKeyPath:

KVC 赋值的全过程(取值的全过程与赋值是类似的):

首先,找 set 方法并调用:

img

如果没有 set 方法,则找 _set 方法:

img

如果还找不到,在 +accessInstanceVariablesDirectly 返回 YES 的前提下,会尝试直接访问成员变量,并按照 _age, _isAge, age, isAge 的顺序来查找成员变量:

img

KVC 的方法调用会触发 KVO,哪怕对象没有 set 方法:

img