学习MJ的视频课程,整理并记录知识点–KVO的实现原理
[TOC]
KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变。
KVO的使用
先回忆下,关于KVO的使用,通常的代码类似如下:
1 | // 1. Add KVO |
KVO本质分析
我们通过一个对照组来逐步分析KVO的本质实现,如下
1 | // 创建另一个实例对象,设置同样的属性 |
person1
和person2
都会调用setAge:
方法,但不添加监听的person2
是不会触发KVO的方法的,这两个对象有什么区别呢?
我们分析下这两个对象,根据前面的学习,我们知道,instance对象只存变量,方法存储在class对象中,我们可以尝试打印person1
和person2
的isa
1 | (lldb) p self.person1->isa |
两者的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 | // 在添加observer后,利用runtime打印person1的方法列表 |
我们发现这个动态生成的class里面同样有setAge:
方法,另外多了class, dealloc, _isKVOA
这几个方法。
同时我们获取了每个方法的实现(IMP指针),这样我们就可以通过LLDB查看IMP
指针指向的内存了,作为对照组,我们先查看未添加observer的person2
对象
如下所示:
1 | (lldb) p methodNames |
我们可以看到未添加observer的对象的setAge:
的实现,是位于Interview01(二进制文件)
下的MJPerson
类。
我们对添加了observer的对象进行信息打印,如下:
1 | (lldb) p methodNames |
我们看到setAge:
方法的实现是调用了Foundation
框架里面的_NSSetIntValueAndNotify
方法。说明setAge:
方法的实现已经不一样了,由于苹果的Foundation框架是不开源的,我们无法查看_NSSetIntValueAndNotify
函数的内部实现,不过我们结合已知的信息,查资料,可以得出_NSSetIntValueAndNotify
的内部实现为下伪代码
1 | // 伪代码 |
窥探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:
, 子类还有这几个方法class
、dealloc
、_isKVOA
我们知道NSObject中是有class方法的,是通过object_getClass()
方法获取的,这个子类重写class方法,是为了返回MJPerson
这个类,也就是它原本的class,这么做是为了隐藏这个类的存在,让开发者无感,避免歧义。
子类的dealloc
方法是为了做监听的收尾工作
结合上面的知识我们可以得出这个子类的内存结构图
总结
当添加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 | [self.person1 willChangeValueForKey:@"age"]; |
注意:
willChangeValueForKey:和didChangeValueForKey:, 两个方法必须同时出现, 如果只有一个, 将不会触发KVO
源码和参考
Demo源码:KVO-Demo.zip