一、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:)