KVO的实现原理

KVO的实现原理

首先需要了解KVO基本使用,KVO的全称 Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)viewDidLoad {
[super viewDidLoad];
People *p1 = [People new];
People *p2 = [People new];
p1.age = 1;
p1.age = 2;
p2.age = 2;

[p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
p1.age = 10;

[p1 removeObserver:self forKeyPath:@"age"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"\n%@\n%@\n%@", object, keyPath, change);
}
1
2
3
4
5
6
7
8
// 打印内容为
<People: 0x6000000a0840>
age
{
kind = 1;
new = 10;
old = 10;
}

上述代码中可以看出,在添加监听之后,age 属性的值在发生改变时,就会通知到监听者,执行监听者的 observeValueForKeyPath方法。

KVO原理分析

通过上述代码我们可以发现,一旦 age 属性的值发生改变时,就会通知到监听者,而且我们知道赋值都是调用属性的 set 方法,我们可以来到 People 类中重写 ageset 方法,观察是否是KVO在 set 方法中做了什么操作来实现。

我们发现即使重写了 set 方法,p1对象和p2对象调用同样的 set 方法,但是我们发现p1除了调用set方法之外还会另外执行监听器的 observeValueForKeyPath 方法。

说明KVO在运行时获取对p1对象做了一些改变。相当于在程序运行过程中,对p1对象做了一些变化,使得p1对象在调用 setage 方法的时候可能做了一些额外的操作,所以问题出在对象身上,两个对象在内存中肯定不一样,两个对象可能本质上并不一样。接下来来探索KVO内部是怎么实现的。

KVO底层实现分析

我们在移除监听前打个断点,打印一下两个对象指向的类对象

1
2
3
4
5
(lldb) po p1->isa
NSKVONotifying_People

(lldb) po p2->isa
People

我们可以发现,p1对象执行过 addObserver 操作之后,p1对象的 isa 指针由之前的指向类对象 People 变为指向 NSKVONotifying_People 类对象,而p2对象没有任何改变。也就是说一旦p1对象添加了KVO监听以后,其 isa 指针就会发生变化,因此 set 方法的执行效果就不一样了。

我们发现p1对象的 isa 指针在经过KVO监听之后已经指向了 NSKVONotifying_People 类对象,NSKVONotifying_People 其实是 People 的子类,那么也就是说其 superclass 指针是指向 People 类对象的,NSKVONotifying_People 是runtime在运行时生成的。那么p1对象在调用 setage 方法的时候,肯定会根据p1的 isa 找到 NSKVONotifying_People ,在 NSKVONotifying_People 中找 setage 的方法及实现。

经过查阅资料了解到 NSKVONotifying_People 中的 setage 方法中其实调用了Fundation 框架中C语言函数 _NSsetIntValueAndNotify_NSsetIntValueAndNotify 内部做的操作相当于,首先调用 willChangeValueForKey 将要改变方法,之后调用父类的 setage 方法对成员变量赋值,最后调用 didChangeValueForKey 已经改变方法。didChangeValueForKey 中会调用监听器的监听方法,最终来到监听者的observeValueForKeyPath 方法中。

KVO实现方式的验证

首先经过之前打断点打印 isa 指针,我们已经验证了,在执行添加监听的方法时,会将 isa 指针指向一个通过 runtime 创建的 People 的子类 NSKVONotifying_People。另外我们可以通过打印方法实现的地址来看一下p1和p2的 setage 的方法实现的地址在添加KVO前后有什么变化。

1
2
3
NSLog(@"添加KVO前 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
[p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
NSLog(@"添加KVO后 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
1
2
3
4
5
6
7
8
9
// 输出
添加KVO前 - p1 = 0x10cee3ef0, p2 = 0x10cee3ef0
添加KVO后 - p1 = 0x10d23dcf2, p2 = 0x10cee3ef0

(lldb) po (IMP)0x10cee3ef0
(CategoryDemo`-[People(Test) setAge:] at People+Test.m:38)

(lldb) po (IMP)0x10d23dcf2
(Foundation`_NSSetIntValueAndNotify)

我们发现在添加KVO监听之前,p1和p2的 setAge 方法实现的地址相同,而经过KVO监听之后,p1的 setAge 方法实现的地址发生了变化,我们通过打印方法实现来看一下前后的变化发现,确实如我们上面所讲的一样,p1的 setAge 方法的实现由 People 类方法中的 setAge 方法转换为了C语言的 Foundation 框架的 _NSsetIntValueAndNotify 函数。

Foundation 框架中会根据属性的类型,调用不同的方法。例如我们之前定义的int类型的age属性,那么我们看到Foundation框架中调用的_NSsetIntValueAndNotify函数。那么我们把age的属性类型变为double重新打印一遍

1
2
3
4
5
6
7
8
9
// 输出
添加KVO前 - p1 = 0x10d38aeb0, p2 = 0x10d38aeb0
添加KVO后 - p1 = 0x10d6e4aac, p2 = 0x10d38aeb0

(lldb) po (IMP)0x10d38aeb0
(CategoryDemo`-[People(Test) setAge:] at People+Test.m:38)

(lldb) po (IMP)0x10d6e4aac
(Foundation`_NSSetDoubleValueAndNotify)

我们发现调用的函数变为了 _NSSetDoubleValueAndNotify ,那么这说明 Foundation 框架中有许多此类型的函数,通过属性的不同类型调用不同的函数。
那么我们可以推测 Foundation 框架中还有很多例如 _NSSetBoolValueAndNotify_NSSetCharValueAndNotify_NSSetFloatValueAndNotify_NSSetLongValueAndNotify 等等函数。

NSKVONotifying_People内部结构

首先我们知道 NSKVONotifying_People 作为 People 的子类,其 superclass 指针指向 People 类,并且NSKVONotifyin_Person内部一定对setAge 方法做了单独的实现,那么 NSKVONotifying_PeoplePeople 类的差别可能就在于其内存储的对象方法及实现不同。
我们通过 runtime 分别打印 People 类对象和 NSKVONotifying_People 类对象内存储的对象方法

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)printMethodNamesOfClass:(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 appendString:methodName];
[methodNames appendString:@", "];
}
free(methodList);
NSLog(@"%@ - %@", cls, methodNames);
}

- (void)viewDidLoad {
[super viewDidLoad];
People *p1 = [People new];
People *p2 = [People new];
p1.age = 1;
p1.age = 2;
p2.age = 2;

[p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

p1.age = 10;

[self printMethodNamesOfClass:object_getClass(p1)];
[self printMethodNamesOfClass:object_getClass(p2)];

[p1 removeObserver:self forKeyPath:@"age"];
}
1
2
3
// 输出
NSKVONotifying_People - setAge:, class, dealloc, _isKVOA,
People - test, name, setName:, run, run, setAge:, age,

这里 NSKVONotifying_People 重写 class 方法是为了隐藏NSKVONotifying_People。不被外界所看到。我们在p1添加过KVO监听之后,分别打印p1和p2对象的 class 可以发现他们都返回 People

如果 NSKVONotifying_People 不重写 class 方法,那么当对象要调用 class 对象方法的时候就会一直向上找来到 nsobject,而 nsobectclass 的实现大致为返回自己 isa 指向的类,返回p1的 isa 指向的类那么打印出来的类就是NSKVONotifying_People,但是apple不希望将 NSKVONotifying_People 类暴露出来,并且不希望我们知道NSKVONotifying_People内部实现,所以在内部重写了 class 方法,直接返回 People 类,所以外界在调用p1的 class 对象方法时,是 People 类。这样p1给外界的感觉p1还是 People 类,并不知道 NSKVONotifying_People 子类的存在。

验证didChangeValueForKey:内部会调用observer的observeValueForKeyPath:ofObject:change:context:方法

我们重写 PeoplewillChangeValueForKey:didChangeValueForKey: 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (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");
}
1
2
3
4
5
6
7
8
9
10
11
12
willChangeValueForKey: - begin
willChangeValueForKey: - end
setAge:
didChangeValueForKey: - begin
<People: 0x6000018f3320>
age
{
kind = 1;
new = 10;
old = 10;
}
didChangeValueForKey: - end

再次运行来查看 didChangeValueForKey 的方法内运行过程,通过打印内容可以看到,确实在 didChangeValueForKey 方法内部已经调用了observer的 observeValueForKeyPath:ofObject:change:context: 方法。

手动触发

1
2
3
[p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[p1 willChangeValueForKey:@"age"];
[p1 didChangeValueForKey:@"age"];
1
2
3
4
5
6
7
8
// 输出
<People: 0x600000fd1720>
age
{
kind = 1;
new = 10;
old = 10;
}

通过打印我们可以发现,didChangeValueForKey 方法内部成功调用了observeValueForKeyPath:ofObject:change:context:,并且age的值并没有发生改变。

如果想要手动触发KVO,则需要我们自己调用willChangeValueForKey和didChangeValueForKey方法即可在不改变属性值的情况下手动触发KVO,并且这两个方法缺一不可。