OC对象 - KVO
俗称"键值监听" ,用来监听某个属性值的改变
1. KVO基本使用
1.1 简单的KVO
- 首先我们新建一个iOS的App项目
- 新建
ZSXPerson
类
less
@interface ZSXPerson : NSObject
@property (nonatomic, assign) int age;
@end
less
@implementation ZSXPerson
@end
- viewController 中创建
ZSXPerson
属性并初始化,然后添加KVO监听实例对象的age
变化,接着通过 touchesBegan 点击屏幕的时候修改age
值,来触发 KVO
objectivec
#import "ViewController.h"
#import "ZSXPerson.h"
@interface ViewController ()
@property (nonatomic, strong) ZSXPerson *person1;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_person1 = [[ZSXPerson alloc] init];
[_person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"点击了屏幕");
_person1.age = 20;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"监听到 %@的%@的值发生变化:%@", object, keyPath, change);
}
- (void)dealloc {
[_person1 removeObserver:self forKeyPath:@"age"];
}
@end
- 运行项目,点击屏幕,查看控制台输出

- 现在我们已经使用 KVO 正常监听 person 的 age 值变化
2. 观察给对象添加kvo监听和没有添加kvo监听的区别
2.1 增加person2对象
我们再增加一个person2属性并初始化,同样在点击屏幕的时候,给person2.age也赋值

此时点击屏幕,控制台确实只输出 person1 对象的监听变化
2.2 思考
- 我们对person1和person2仅仅做了同样了age赋值操作,kvo是怎么做到添加了监听的对象才触发observeValue呢?
3. KVO底层实现探究
3.1 添加KVO前后,setAge方法发生了什么变化?
在添加KVO前后,分别打印一下方法实现的地址 发现person1 的 setAge 方法在添加完 KVO 监听后变化了,person2 是没有变化的
3.2 添加KVO前后,setAge到底走了什么方法
我们打印一下方法
- 直接打印方法地址看不到具体方法名,使用(IMP)强转一下
可以观察到
- 未添加KVO监听的 setAge 方法是
-[ZSXPerson setAge:]
- 添加KVO监听后 setAge 方法是
Foundation
_NSSetIntValueAndNotify`
3.2.1 其他数据类型
如果我们还有一个double类型的属性 setHeight方法最终会是
_NSSetDoubleValueAndNotify
3.3 查看isa
前面学过isa的指向,我们知道对象方法
是存放类对象
中的,既然两个对象的setAge对象方法不一样,那是不是他们isa指向的类对象也不一样呢
3.3.1 控制台打印
确实他们isa指向的类对象不是同一个,
_person1
在这边没有显示类对象名称,_person2
可以看出来是ZSXPerson
3.4 使用runtime打印类对象
3.4.1通过runtime打印_person1的类对象:NSKVONotifying_ZSXPerson

3.4.2 打印_person1的类对象的superclass
同时还发现,NSKVONotifying_ZSXPerson
的superclass就是ZSXPerson
3.5 结论
添加KVO后
- 利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
NSKVONotifying_ClassName
- 当修改instance对象的属性时,会调用Foundation的
_NSSetXXXValueAndNotify
函数
3.6 _NSSetXXXValueAndNotify方法做了什么
_NSSetXXXValueAndNotify
是Foundation
框架的东西,因为无法拿到Foundation
的源码,但是可以通过一些逆向的手段,得到_NSSetXXXValueAndNotify
方法实际执行的伪代码
3.6.1 _NSSetXXXValueAndNotify
调用setAge
方法时,_NSSetXXXValueAndNotify
方法里面做的内容可以认为是这样:
-
self willChangeValueForKey:@"age"\];
-
self didChangeValueForKey:@"age"\];
3.7 NSKVONotifying_ClassName
类对象里面还有什么方法
我们遍历打印一下NSKVONotifying_ClassName
里面,看看它还有什么方法
scss
- (void)printMethodNameOfClass:(Class)cls {
unsigned int count;
// 获得方法数组
Method *methodList = class_copyMethodList(cls, &count);
// 存储方法名
NSMutableString *methodNames = [NSMutableString string];
// 遍历所有方法
for (int i = 0; i < count; i++) {
// 获得方法
Method method = methodList[i];
// 获得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法明
[methodNames appendFormat:@"%@", methodName];
[methodNames appendFormat:@", "];
}
// 释放
free(methodList);
// 打印方法名
NSLog(@"%@ %@", cls, methodNames);
}
点击屏幕的时候,我们调用一下打印
3.7.1 setAge:
会调用Foundation
_NSSetIntValueAndNotify`
3.7.2 class:
你会发现,对象添加KVO监听后,isa只想了一个新的子类NSKVONotifying_ZSXPerson
,但是我们在使用[person1 class]获取类对象的时候,返回的依然是ZSXPerson
这是因为:
苹果底层设计时,为了屏蔽内部实现,让开发者使用过程中,不会突然看到一个异常的东西,避免胡思乱想
我们可以认为NSKVONotifying_ZSXPerson
类重写了 class 方法如下
kotlin
- (Class)class {
return [ZSXPerson class];
}
3.7.2 dealloc:
可以认为里面就是做了一些跟KVO释放有关的收尾操作
- (void)dealloc { // 收尾工作 }
3.7.2 _isKVOA:
返回是否是KVO的相关类
objectivec
- (BOOL)_isKVOA {
return YES;
}
4. 总结
4.1 未使用KVO监听的对象

4.2 使用了KVO监听的对象

4.3 添加KVO后
- 利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
NSKVONotifying_ClassName
- 当修改instance对象的属性时,会调用Foundation的
_NSSetXXXValueAndNotify
函数 -
- willChangeValueForKey:
-
- 父类原来的setter
-
- didChangeValueForKey: 内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)
扩展
手动触发KVO
- 碰过这样一道面试题:使用下划线直接访问成员变量的方式给变量赋值,会不会触发KVO监听?
答案是:不会。因为KVO重写的是set方法(setAge:)。直接给成员变量赋值不会走set方法,因此也不会触发KVO监听
- 然后又会问,那能不能手动触发KVO监听?
手动触发
只要手动调用实例对象的willChangeValueForKey:
和didChangeValueForKey
方法,就能触发
注意
需要分别调用willChangeValueForKey:
和didChangeValueForKey
方法才会触发KVO
- 内部实现在执行
didChangeValueForKey
方法的时候,会判断前面是否执行了willChangeValueForKey:
方法,前面有调用过才会触发KVO 监听
@oubijiexi