一、KVO 的基本使用
首先我们来看一下 KVO 的基本使用,KVO 的全称 Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变。
1 | - (void)viewDidLoad { |
从上述代码中可以看出,在添加监听之后,age 属性的值在发生改变时,就会通知到监听者,执行监听者的 observeValueForKeyPath 方法。
二、探寻 KVO 原理
通过上述代码我们发现,一旦
age属性的值发生改变时,就会通知到监听者,并且我们知道赋值操作都是调用属性的set方法,我们可以来到Person类中重写age的set方法,观察是否是KVO在set方法内部做了一些操作来通知监听者。我们发现即使重写了
set方法,p1对象和p2对象调用同样的set方法,但是我们发现p1除了调用set方法之外还会另外执行监听器的observeValueForKeyPath:ofObject:change:context:方法。这说明
KVO在运行时获取对p1对象做了一些改变。相当于在程序运行过程中,对p1对象做了一些变化,使得p1对象在调用setAge:方法的时候可能做了一些额外的操作,所以问题出在对象身上,两个对象在内存中肯定不一样,两个对象本质上也不一样。
三、KVO 底层实现分析
- 首先我们来看一下
p1和p2在addObserver方法 前后的isa分别是什么

通过上图我们发现,
p1对象执行过addObserver操作之后,p1对象的isa指针由之前的指向类对象Person变为指向NSKVONotifyin_Person类对象,而p2对象没有任何改变。也就是说一旦p1对象添加了KVO监听以后,其isa指针就会发生变化,因此set方法的执行效果就不一样了。那么我们先来观察
p2对象在内容中是如何存储的,然后对比p2来观察p1。
首先我们知道,p2在调用setAge:方法的时候,首先会通过p2对象中的isa指针找到Person类对象,然后在类对象中找到setAge:方法。然后找到方法对应的实现。如下图所示
但是刚才我们发现
p1对象的isa指针在经过KVO监听之后已经指向了NSKVONotifyin_Person类对象,NSKVONotifyin_Person其实是Person的子类,那么也就是说其superclass指针是指向Person类对象的,NSKVONotifyin_Person是 runtime 在运行时生成的。那么p1对象在调用setAge:方法的时候,肯定会根据p1的isa找到NSKVONotifyin_Person,在NSKVONotifyin_Person中找setAge:方法的实现。NSKVONotifyin_Person中的setAge:方法中其实调用了Fundation框架中 C 语言函数_NSsetIntValueAndNotify,_NSsetIntValueAndNotify内部做的操作相当于:- 首先调用 willChangeValueForKey:
- 之后调用父类的 setAge: 方法对成员变量赋值
- 最后调用 didChangeValueForKey:
- didChangeValueForKey: 中会调用监听器的监听方法,最终来到监听者的 observeValueForKeyPath:ofObject:change:context: 方法中
四、NSKVONotifyin_Person 内部结构是怎样的?
首先我们知道,NSKVONotifyin_Person 作为 Person 的子类,其 superclass 指针指向 Person 类,并且 NSKVONotifyin_Person 内部一定对 setAge: 方法做了单独的实现,那么 NSKVONotifyin_Person 同 Person 类的差别可能就在于其内存储的对象方法及实现不同。
我们通过 runtime 分别打印 Person 类对象和 NSKVONotifyin_Person 类对象内存储的对象方法
1 | - (void)viewDidLoad |
通过上述代码我们发现 NSKVONotifyin_Person 中有 4 个对象方法。分别为 setAge: class dealloc _isKVOA,那么至此我们可以知道 NSKVONotifyin_Person 的内存结构以及方法调用顺序。

这里
NSKVONotifyin_Person重写class方法是为了隐藏NSKVONotifyin_Person,使其不被外界所看到。我们在p1添加过KVO监听之后,分别打印p1和p2对象的class可以发现他们都返回Person。1
2NSLog(@"%@,%@",[p1 class],[p2 class]);
// 打印结果 Person,Person如果
NSKVONotifyin_Person不重写class方法,那么当对象要调用class对象方法的时候就会一直向上找来到Nsobject,而Nsobect的class的实现大致为返回自己isa指向的类,返回p1的isa指向的类那么打印出来的类就是NSKVONotifyin_Person,但是 Apple 爸爸不希望将NSKVONotifyin_Person类暴露出来,并且不希望我们知道NSKVONotifyin_Person内部实现,所以在内部重写了class 对象方法,直接返回Person类,所以外界在调用p1的class 对象方法时,是Person类。这样p1给外界的感觉p1还是Person类,并不知道NSKVONotifyin_Person子类的存在。那么我们可以猜测
NSKVONotifyin_Person内重写的class内部实现大致为1
2
3
4
5- (Class)class
{
// 得到类对象,再找到类对象父类
return class_getSuperclass(object_getClass(self));
}
五、验证 didChangeValueForKey: 内部会调用 observer 的 observeValueForKeyPath:ofObject:change:context: 方法
我们在
Person类中重写willChangeValueForKey:和didChangeValueForKey:方法,模拟它们的实现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- (void)setAge:(int)age
{
NSLog(@"setAge:");
_age = age;
}
- (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");
}
// 打印内容
setAge:
setAge:
willChangeValueForKey: - begin
willChangeValueForKey: - end
setAge:
didChangeValueForKey: - begin
监听到<Person: 0x6000032f4260>的age改变了{
kind = 1;
new = 30;
old = 10;
}
didChangeValueForKey: - end
setAge:通过上面的打印内容,我们知道
didChangeValueForKey:内部确实会调用observer的observeValueForKeyPath:ofObject:change:context:方法,验证了之前的观点。
六、总结(KVO的本质是什么?)
- 利用
RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类(NSKVONotifying_XXX) - 当修改
instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数 - 子类拥有自己的set方法实现,内部会调用
- willChangeValueForKey:
- 原来的 setter 方法
- didChangeValueForKey: 这个方法内部又会调用监听器(observe)的监听方法(observeValueForKeyPath:ofObject:change:context:)