目标
- KVO的本质是什么?
- 如何手动触发KVO?
- 如何自己动手实现KVO?
- 直接修改成员变量会触发KVO吗?
KVO的本质是什么?
想要回答这个问题,首先需要弄明白当给一个对象的属性添加KVO后,系统做了哪些事情?
- 当给一个对象的属性添加KVO监听时,系统会利用Runtime动态创建一个类,这个类是对象父类的子类
- 将对象的isa指针由父类更改到新建的子类
- 重写子类的setter方法,在子类的setter方法中添加触发KVO监听者的监听方法的机制
所以,KVO的本质是修改属性的setter方法,在属性的setter方法里添加调用监听方法的逻辑,为了不破坏原始类,系统又增加了动态创建子类并修改对象的isa指针的机制。

如何手动触发KVO?
想要知道如何手动触发KVO,首先需要弄明白系统是如何修改setter方法以调用监听方法的。
- 在子类的setter方法会调用
_NSSetXXXValueAndNotify函数 - 在
_NSSetXXXValueAndNotify函数内部会调用willChangeValueForKey:和didChangeValueForKey:方法 - 在
didChangeValueForKey:内部会调用KVO监听者的监听方法
所以,想要手动触发KVO,可以通过手动调用willChangeValueForKey:和didChangeValueForKey: 来实现,需要强调这两个方法都必须调用才会起作用
如何手动实现KVO?
手动实现KVO的过程就是把系统实现KVO的过程自己用代码实现一下。大致流程如下:
- 添加一个NSObject分类,在分类中添加addObserver和removeObserver方法
- 判断监听的对象是否含有对应的key的setter方法
- 判断添加了前缀的子类是否已经存在
- 利用runtime的
objc_allocateClassPair函数动态创建子类 - 判断KVO类有没有重写过setter方法
- 利用runtime的
class_addMethod函数重写setter方法 - 重写的setter方法内部调用原始类的setter方法
- 重写的setter方法内部找到监听者进行回调
- 监听者的保存方式利用runtime的关联对象给分类添加一个数组属性
详细内容可参考iOS_KVO_Study
直接修改成员变量会触发KVO吗?
不会触发,直接修改成员变量并不会触发setter方法,因此也就不会触发KVO
探究KVO原理
KVO全称 Key-Value Observing,键值监听。
基本用法
- 注册观察者,实施监听
[p1 addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew
context:nil];
- 在回调方法里处理属性发生变化后的后续处理
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context {
//...实现监听处理
}
- 移除观察者
[self removeObserver:self forKeyPath:@“age"];
在添加监听后,age属性的值在发生改变时就会通知监听者,执行监听者的observeValueForKeyPath方法。接下来我们就一步步探究为何会在age值发生改变后通知监听者。
重写set方法
赋值操作会调用set方法,我们通过重写Person类的setAge:方法,观察是否是KVO在set方法内部做了一些操作来通知监听者。
// ViewController类
- (void)viewDidLoad {
[super viewDidLoad];
Person *p1 = [[Person alloc] init];
Person *p2 = [[Person alloc] init];
p1.age = 1;
p1.age = 2;
p2.age = 2;
// self 监听 p1的 age属性
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
p1.age = 10;
[p1 removeObserver:self forKeyPath:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"监听到%@的%@改变了%@", object, keyPath,change);
}
// Person类
- (void)setAge:(NSInteger)age {
_age = age;
}
通过观察发现p1和p2同样调用了setAge:方法,p1除了调用setAge:方法外还会执行监听者的observeValueForKeyPath方法。显然这些并不是在setAge:方法中调用的。
对比p1在addObserver前后的变化
既然不是通过修改setAge:方法来实现监听的,那addObserver方法对p1对象做了什么特殊处理呢?我们通过打印isa指针来进行对比。

通过上图我们发现,p1对象在执行过addObserver后,isa指针发生了改变,由之前的Person变为了NSKVONotifying_Person。所以,系统生成的新类的格式是
NSKVONotifying_原类
思路验证
- 打印方法实现的地址来看p1和p2的setAge:方法实现的地址在添加KVO前后有什么变化
NSLog(@"添加KVO监听之前 - p1 = %p, p2 = %p", [p1 methodForSelector:@selector(setAge:)], [p2 methodForSelector:@selector(setAge:)]);
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
[self printMethods:object_getClass(p2)];
[self printMethods:object_getClass(p1)];
NSLog(@"添加KVO监听之后 - p1 = %p, p2 = %p", [p1 methodForSelector:@selector(setAge:)], [p2 methodForSelector:@selector(setAge:)]);

通过打印的地址信息,我们发现在添加KVO监听之前,p1和p2的setAge:方法实现的地址相同,而经过KVO监听之后,p1的setAge方法实现的地址发生了变化,p1的setAge:方法的实现转换为了C语言的Foundation框架的 _NSSetLongLongValueAndNotify 函数。
- 查看
NSKVONotifying_Person的内部结构
我们通过runtime分别打印Person类对象和NSKVONotifying_Person类对象内存储的对象方法
- (void)printMethods:(Class)cls {
unsigned int count;
Method *methods = class_copyMethodList(cls, &count);
NSMutableString *methodNames = [NSMutableString string];
[methodNames appendFormat:@"%@ - ", cls];
for (int i = 0; i < count; i++) {
Method method = methods[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
[methodNames appendString:methodName];
[methodNames appendString:@" "];
}
NSLog(@"%@", methodNames);
free(methods);
}
打印内容如下:

通过打印结果我们发现NSKVONotifying_Person中有4个对象方法,分别为
setAge: class dealloc _isKVOA ,下图是NSKVONotifying_Person的内存结构以及方法调用顺序。

这里NSKVONotifying_Person重写class方法是为了隐藏NSKVONotifying_Person。我们在p1添加KVO后,打印p1、p2对象的class可以发现他们都返回Person。
NSLog(@"%@, %@", [p1 class], [p2 class]);
// 打印结果: Person, Person
猜测NSKVONotifying_Person内重写的class内部实现大致为:
- (Class) class {
// 得到类对象,在找到类对象父类
return class_getSuperclass(object_getClass(self));
}
- 验证didChangeValueForKey:内部会调用observer的observerValueForKeyPath:ofObject:change:context:方法
在Person类中重写willChangeValueForKey:和didChangeValueForKey:方法,模拟KVO的实现。
- (void)willChangeValueForKey:(NSString *)key {
NSLog(@"willChangeValueForKey: - begin");
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey: - end");
}
- (void)didChangeValueForKey:(NSString *)key {
NSLog(@"didChangeValueForKey: - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey: - end");
}
打印结果:

通过打印的结果不难发现,确实在
didChangeValueForKey 方法内部调用了 observeValueForKeyPath:ofObject:change:context: 方法。