学习MJ的视频课程,整理并记录知识点–KVO的实现原理

[TOC]

KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变。

KVO的使用

先回忆下,关于KVO的使用,通常的代码类似如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1. Add KVO
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];

// 2. Trigger
self.person1.age = 18;

// 3. Observing
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey, id> *)change context:(void *)context {
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}

// Remove observe
- (void)dealloc {
[self.person1 removeObserver:self forKeyPath:@"age"];
}

KVO本质分析

我们通过一个对照组来逐步分析KVO的本质实现,如下

1
2
// 创建另一个实例对象,设置同样的属性
self.person2.age = 20;

person1person2都会调用setAge:方法,但不添加监听的person2是不会触发KVO的方法的,这两个对象有什么区别呢?

我们分析下这两个对象,根据前面的学习,我们知道,instance对象只存变量,方法存储在class对象中,我们可以尝试打印person1person2的isa

1
2
3
4
5
(lldb) p self.person1->isa
(Class) $0 = NSKVONotifying_MJPerson

(lldb) p self.person2->isa
(Class) $0 = MJPerson

两者的isa指向不一样,从哪里出来一个NSKVONotifying_MJPerson类。

我们知道实例对象的isa是指指向class的,但是person1的class却成了NSKVONotifying_MJPerson,这个NSKVONotifying_MJPerson的产生必然与添加observer有关,那么它的superclass是谁?

我们打印会发现NSKVONotifying_MJPerson的superclass是MJPerson。原来在给person1添加observer时,会动态创建一个叫NSKVONotifying_MJPerson的子类,然后person1的isa会指向这个新创建的子类。

KVO本质分析验证

person1的isa指向NSKVONotifying_MJPerson,当调用setAge:方法时,肯定是去NSKVONotifying_MJPerson中找,而setAge:是触发监听者的关键,那么动态创建的这个子类的setAge:方法肯定和之前的不一样。我们尝试打印这个动态类的方法列表:

1
2
3
4
5
6
7
8
9
10
11
// 在添加observer后,利用runtime打印person1的方法列表
unsigned int outCount2;
Method *methodList2 = class_copyMethodList(object_getClass(self.person1), &outCount2);
NSMutableString *methodNames = [NSMutableString string];

for (NSInteger i = 0; i < outCount2; i++) {
Method method = methodList2[i];
[methodNames appendFormat:@"%@{ %p }, ", NSStringFromSelector(method_getName(method)), (IMP)method_getImplementation(method)];
}
// ... setAge:, class, dealloc, _isKVOA
NSLog(@"%@", methodNames);

我们发现这个动态生成的class里面同样有setAge:方法,另外多了class, dealloc, _isKVOA这几个方法。

同时我们获取了每个方法的实现(IMP指针),这样我们就可以通过LLDB查看IMP指针指向的内存了,作为对照组,我们先查看未添加observer的person2对象

如下所示:

1
2
3
4
5
6
(lldb) p methodNames
(__NSCFString *) $0 = 0x0000600001e7e220 @"setAge:{ 0x101e13720 }, age{ 0x101e13700 }, "
(lldb) p (IMP)0x101e13720
(IMP) $1 = 0x0000000101e13720 (Interview01`-[MJPerson setAge:] at MJPerson.h:12)
(lldb) p (IMP)0x101e13700
(IMP) $2 = 0x0000000101e13700 (Interview01`-[MJPerson age] at MJPerson.h:12)

我们可以看到未添加observer的对象的setAge:的实现,是位于Interview01(二进制文件)下的MJPerson类。

我们对添加了observer的对象进行信息打印,如下:

1
2
3
4
(lldb) p methodNames
(__NSCFString *) $2 = 0x0000600002143ed0 @"setAge:{ 0x1040ebcf2 }, class{ 0x1040ea06e }, dealloc{ 0x1040e9e12 }, _isKVOA{ 0x1040e9e0a }, "
(lldb) p (IMP)0x1040ebcf2
(IMP) $3 = 0x00000001040ebcf2 (Foundation`_NSSetIntValueAndNotify)

我们看到setAge:方法的实现是调用了Foundation框架里面的_NSSetIntValueAndNotify方法。说明setAge:方法的实现已经不一样了,由于苹果的Foundation框架是不开源的,我们无法查看_NSSetIntValueAndNotify函数的内部实现,不过我们结合已知的信息,查资料,可以得出_NSSetIntValueAndNotify的内部实现为下伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 伪代码
void _NSSetIntValueAndNotify()
{
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}

// 伪代码
- (void)didChangeValueForKey:(NSString *)key
{
// 通知监听器,某某属性值发生了改变
[observer observeValueForKeyPath:key ofObject:self change:nil context:nil];
}

窥探Foundation

如何验证Foundation下面确实有_NSSetIntValueAndNotify方法?

我们平时使用Foundation.framework都是已经编译后的二进制文件,不过我们可以利用反编译工具hopper来查看Foundation里面的方法名和汇编代码(想拿到Foundation二进制文件我们通过已越狱的手机获取或者去网上搜索,hopper工具的使用这里不作介绍)。

除了Hopper,在mac上,我们还可以系统给提供的工具nm(OVERVIEW: llvm symbol table dumper)命令来导出函数符号表:

1
nm Foundation | grep ValueAndNotify

我们可以确定,Foundation里面确实有_NSSet...ValueAndNotify方法。

子类的内部方法

除了setAge:, 子类还有这几个方法classdealloc_isKVOA

我们知道NSObject中是有class方法的,是通过object_getClass()方法获取的,这个子类重写class方法,是为了返回MJPerson这个类,也就是它原本的class,这么做是为了隐藏这个类的存在,让开发者无感,避免歧义。

子类的dealloc方法是为了做监听的收尾工作

结合上面的知识我们可以得出这个子类的内存结构图
KVO子类

总结

当添加observer时,runtime会动态创建一个子类,重写被监听属性的set方法。

KVO一定会调用willChangeValueForKey:didChangeValueForKey:方法

思路:
分析两个对象,发现isa不一样,通过isa找到class,通过class找到方法列表以及_NSSetIntValueAndNotify方法,分析_NSSetIntValueAndNotify方法。引出willChangeValueForKey:didChangeValueForKey:

面试题

1、iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么)

  • 利用RuntimeAPI动态生成一个子类, 并且让instance对象的isa指向这个全新的子类
  • 当修改instance对象的属性时, 会调用Fundation的_NSSet*ValueAndNotify函数
    • willChangeValueForKey:
    • 父类原来的setter方法
    • didChangeValueForKey:
      • 内部会触发监听器Observer的监听方法(observeValueForKeyPath:ofObject:change:context:)

2、如果直接修改对象的成员变量, 是否会触发监听器的(observeValueForKeyPath:ofObject:change:context:)方法?

直接修改对象的成员变量, 而不调用set方法, 将不会触发观察者的observeValueForKeyPath:ofObject:change:context:方法

3、如何手动触发KVO?

  • 已知实例对象被观察的属性, 在调用set方法进行修改时, 会触发_NSSet*ValueAndNotify函数
  • 并触发willChangeValueForKey:和didChangeValueForKey:这两个方法, 所以我们可以手动添加这两个方法, 来触发KVO
  • 现在已知直接修改成员变量时, 不会触发KVO, 那么就在修改成员变量的前后添加这两个方法
1
2
3
[self.person1 willChangeValueForKey:@"age"];
self.person1->_age = 21;
[self.person1 didChangeValueForKey:@"age"];

注意:
willChangeValueForKey:和didChangeValueForKey:, 两个方法必须同时出现, 如果只有一个, 将不会触发KVO

源码和参考

Demo源码:KVO-Demo.zip

评论