KVO(二)探究KVO的本质
上篇文章我们通过一个简单的例子,讲述了KVO的基本使用情况,下面我们来继续深究KVO的本质是什么。
想要探究本质,就要和没有使用KVO的对象来进行对比,对比法是最容易看出不同的。
(一)添加观察者对象以后,系统帮我们做了什么操作
Person类只有一个属性age:
我们创建Person类的两个对象person1 和 person2,person1通过KVO观察age属性的变化,person2不做特殊处理。我们来对比一下person1 和 person2 的类对象和元类对象是否一样。
得到如下结果:
通过观察结果,我们得知:
1、 在向person1对象添加观察者之前,person1 和 person2 的类对象都是指向PMPerson,没有任何区别;
2、 当向person1对象添加观察者之后,person1的类对象变成了NSKVONotifying_PMPerson,person2没有变化;
3、同时我们继续观察person1和person2所指向的元类对象也不一样,person1的元类对象是NSKVONotifying_PMPerson,person2的元类对象仍旧是PMPerson。
进一步分析,也就是在添加观察者之后,系统对被观察者重新生成了一个中间类对象和对应的元类,类名以NSKVONotifying_开头。
(二)那么以NSKVONotifying_PMPerson类和PMPerson类之间究竟有什么关系呢?
我们利用在NSObject中学习到的isa指针和superClass指针来验证一下:
不熟悉的朋友可以看这篇文章:isa 与 superClass
注意观察结果:pm_person1Class是经我们转换过的person11Class对象,pm_person1MetaClass是经过转换后的meta1Class对象,便于通过superClass指针访问。
当我们使用superClass指针查看具体指向地址时,发现pm_person1MetaClass->superclass指向PMPerson的地址,由于pm_person1Class是NSKVONotifying_PMPerson类型,也就是说NSKVONotifying_PMPerson是继承自PMPerson类。
同样的道理,我们可以得到NSKVONotifying_PMPerson元类也是继承自PMPerson元类。
通过以上证明,我们得知,系统生成的NSKVONotifying_XXX类,其实是继承自原来的XXX类。
(三)探究NSKVONotifying_XXX又做了什么操作?
想知道NSKVONotifying_XXX类做了什么操作,就得知道NSKVONotifying_XXX里面究竟有什么方法,我们利用runtime机制来探究一下。通过以下方法,打印NSKVONotifying_XXX的方法列表:
我们分别传递NSKVONotifying_PMPerson的类对象和元类对象,看里面究竟有什么方法:
结果如下:
也就是说在NSKVONotifying_PMPerson中,实现了四个实例方法,分别是:
setAge:(),class(),dealloc()和_isKOVA。
dealloc()应该是做一些收尾工作,_isKOVA应该是返回一个BOOL值,在这里应该是返回一个YES。
我们把重点放在setAge:和 class()方法中。
首先我们看一下class()方法,通过之前我们对NSObject的了解,class()能够得到类对象,我们看一下调用NSKVONotifying_PMPerson 的 class() 方法会得到什么结果。
结果如下:
不难看出通过runtime获取到的是真正的类名,而Class方法获取到的是其父类名。
由此可以猜想在NSKVONotifying_PMPerson中重写class()是为了隐藏其真正实现,毕竟NSKVONotifying_PMPerson类是在编译过程中,系统利用runtime机制帮我们生成的,为了打消我们的疑虑,当我们调用class()时,就直接返回父类的方法,其内部实现有可能是这样子的:
最后只剩setAge:()了,这也是实现KVO的关键机制。当我们点击屏幕,触发KVO时,看一下系统的调用栈,可以通过工具栏直接查看,或者在lldb输入bt命令进行查看,得到如下结果:
得到两个关键信息:
1.调用Foundation框架的_NSSetLongLongValueAndNotify方法
2.调用:NSKeyValueDidChange方法。
由于我们无法看到Foundation的具体实现,我们猜想在NSKVONotifying_PMPerson中的setAge:()方法调用了_NSSetLongLongValueAndNotify,然后在_NSSetLongLongValueAndNotify()中会进行真正的赋值操作,最终触发KVO,并调用NSKeyValueDidChange。
由于NSKVONotifying_PMPerson是继承自PMPerson,那么在PMPerson中肯定能够得到验证。
有NSKeyValueDidChange(),肯定就会有NSKeyValueWillChange()
接着我们来验证一下这个猜测,在PMPerson中实现以下方法:
点击屏幕,触发KVO:
看打印结果非常清晰,实际的调用顺序:
1、NSKVONotifying_PMPerson调用setAge:();
2、setAge()调用_NSSetLongLongValueAndNotify;
3、_NSSetLongLongValueAndNotify 调用willChangeValueForKey();
4、调用PMPerson的setAge:(),改变属性的值;
5、调用didChangeValueForKey();
6、调用NSKeyValueNotifyObserver();
7、didChangeValueForKey()调用结束。
到这里关于NSKVONotifying_PMPerson中setAge()的剖析已经基本清楚。
可能大家还会有一个疑问,为什么会调用_NSSetLongLongValueAndNotify呢?其实跟我们需要观察的属性类型有关系。我们声明的age属性时NSInteger类型,即:
在我们的调试环境中,NSInteger就是long类型,假如我们需要观察的变量是int类型,在NSKVONotifying_XXX类的set方法中调用的自然就是_NSSetIntValueAndNotify方法了,感兴趣的朋友,可以自己试一下。
总结:
1、当我们给某一个对象添加观察者观察特定属性时,系统通过runtime生成了一个中间类,NSKVONotifying_XXX;
2、NSKVONotifying_XXX通过重写被观察属性的set方法,达到通知观察者的目的。