KVO 分析

1.jpeg

搞完KVCKVO,谁让他们名字这么接近呢,是吧?KVO其实我们都很熟悉了,这里就不做过多的文字描述了,无非就是给一个对象的属性添加一个观察者可以实现观察检测该属性值的变化的这么一个机制。我们这里就直接进入主题去探索下他的一些细节和原理。

KVO方法简介

我们先来看看我们经常用的KVO的方法:

2.png

第一个参数是一般为self。第二个为KeyPath,就是我们要监听的key。第三个option和第四个context我们看下面官方解释:

第三个参数:option
3.png

上面官方的解释其实就是对option这个内容选项做了一个解释简单来讲就是:

NSKeyValueObservingOptionOld : 选择在更改之前接收观察属性的值 也就是观察旧值。

NSKeyValueObservingOptionNew: 请求属性的新值。也就是观察新值变化。

NSKeyValueObservingOptionInitial :发送立即更改通知(在 addObserver:forKeyPath:options:context:returns 之前)。可以使用这个额外的一次性通知来在观察者中建立属性的初始值。

NSKeyValueObservingOptionPrior : 指示被观察对象在属性更改之前发送通知(除了更改之后的通常通知之外)。更改字典通过包含键NSKeyValueChangeNotificationIsPriorKey 和包含YESNSNumber 值来表示更改前通知。该密钥不存在。当观察者自己的 KVO 合规性要求它为依赖于被观察属性的属性之一调用 -willChange... 方法之一时,您可以使用 prechange 通知。通常的更改后通知来得太晚了,无法及时调用 。

5.png

总结的话就是监听的Key的不同情况下的值的变化策略。

第四个参数:context
4.png

关于文中的context,其实简单来讲就是为了使我们观察的对象值更加安全更加有针对性的正确获取而存在。你可以设置为Null。你也可以设置一个静态变量的地址。而且这是苹果推荐的方式。因为在我们在使用KVO的过程中,我们可能会对多个对象多个属性进行观察,这时候我们经常用KeyPathobject同时判断来区分,但是有时候难免出现重合或者误写的情况导致获取的值混乱,并且代码判断变多,可读性变差,复杂。这个时候context就可以发挥作用了,用它来区分每一个对象每一个属性值的变化。

示例:

6.png
7.png

context可以更加方便和准确的一对一获取对象和值的变化。

KVO移除

8.png

上面的文章大致讲的内容就是举例移除KVO观察者的方法。同时下方比较值得注意的就是有讲到 如果我们不主动移除观察者,那么当我们的key的值发生变化时就会继续给观察者发消息。这样就有一种情况出现。当我们从页面A跳转到 页面B 我们给页面B 的某个对象(非单例对象)的属性添加观察者。并且发送消息,这个时候没有问题。然后我们从页面B返回页面A然后再次进入页面B 并且给新增的观察者发送消息的时候(改变B页面被观察的某个对象的属性)这个时候也没问题,但是当我们把B页面的对象换成单例对象的时候就会奔溃。原因就是因为前面第一次进来B页面创建的对象观察者没有移除,当第二次进来的时候单例对象还存在只是前一个B页面已经释放了,这个时候系统仍然会给前面释放掉的B页面里未移除的观察者的发送消息,但是这个观察者的内存地址已经随着页面B的消失而被移除了。所以当我们发送消息的时候第一次设置的观察者接收消息就报错了。下面我们把两种情况都运行试试:

情况一:非单例对象添加观察者不移除
9.png
10.png
11.png
12.png
13.png
14.png

非单例对象不移除,不会造成崩溃。

情况二:单例对象添加观察者不移除

在情况一上做些改造:

15.png
16.png
17.png
18.png

对单例对象添加观察者不移除,当持有者(self)释放后再次给观察者发送消息就会造成崩溃报空指针。

KVO自动开关控制

1,打开自动开关(默认是打开的)
19.png
20.png
2,关闭自动开关(默认是打开的)
21.png

我们可以利用这个开关来控制某个对象的观察者开关选择

KVO设置影响因素

22.png
23.png

可以对观察对象属性设置影响因素,改变影响因素即可得到观察对象属性的变化值。

KVO观察数组

KVO文档开头有告诉我们要了解KVO就要先了解KVC(如图24)在上一篇文章KVC分析中我们重点分析KVC的细节和要点,其实在KVC文档里有告诉我们关于KVCKVO的一些关联(如图25)。

24.png
25.png

上面的文档告诉我们:如果我们在用KVO来操作可变的一些集合类型属性时就需要按照上面文档给出的方法来执行。

26.png
27.png
28.png

在上图我们发现可变数组在修改值之后change打印的时候 kind变成了2。这个我们去查看下:command+点击观察方法里的NSKeyValueChangeKey:

29.png

chang里的kind是一个枚举类型,刚好insert是枚举类型定义的2

KVO原理探究

我们在KVO的官方文档详细介绍里看到下面一段话:

30.png

谷歌翻译:
自动键值观察是使用一种称为isa-swizzling 的技术实现的。
顾名思义,isa指针指向维护调度表的对象的类。该调度表主要包含指向类实现的方法的指针,以及其他数据。
当观察者为对象的属性注册时,被观察对象的isa指针被修改,指向中间类而不是真正的类。因此,isa 指针的值不一定反映实例的实际类。
您永远不应该依赖isa 指针来确定类成员资格。相反,您应该使用类方法来确定对象实例的类。

从上面的文档我们可以知道KVO在实现过程中还生成了中间产物,并且这个中间产物还把我们观察对象的isa指针进行了指向修改。

动态生成NSKVONotifying_ZYPerson

下面我们就来利用断点和LLDB调试打印探索:

31.png

在我们addObserve的时候动态生成了一个类:NSKVONotifying_ZYPerson

我们来看看这个新生的NSKVONotifying_ZYPerson类和本类ZYPerson的关系:

利用以下方法遍历打印类和子类

#pragma mark - 遍历类以及子类
- (void)printClasses:(Class)cls{
    
    // 注册类的总数
    int count = objc_getClassList(NULL, 0);
    // 创建一个数组, 其中包含给定对象
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
    // 获取所有已注册的类
    Class* classes = (Class*)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i
32.png

从上图的打印可知:NSKVONotifying_ZYPerson类是本类ZYPerson的子类。

既然我们知道了NSKVONotifying_ZYPerson类是动态生成的ZYPerson的子类。那我们就去看看这个新生成的类内容有哪些。比如方法、属性、协议等。这里我们探索下方法。

动态生成NSKVONotifying_ZYPerson类的方法

我们利用下面的方法代码来直接打印类的方法:

#pragma mark - 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i
33.png

从上图我们可以看到 除了方法_isKVOA作为一个标志符号之外 其他的方法都是其父类ZYPerson拥有的。所以我们可以知道他是在重写父类的方法。

ps:为了方便下面的验证调试,我们创建一个新的类ZYViewController。把viewController里的代码都搬到ZYViewController然后从ViewController页面push过去。

继续,上面我们看到NSKVONotifying_ZYPerson类继承了父类ZYPerson的方法。而且我们在文档看到说在addObserver后底层进行了isa-swizzling操作。将原来对象的isa指向了新建的类。那我们就来验证下:

34.png
35.png

在添加观察者的过程中确实进行了isa指向转移,从元对象转移指向了动态创建的NSKVONotifying_xxx类,并且在当前页面销毁走dealloc的时候将被观察者对象的isa转移回元对象本身。

动态生成的子类NSKVONotifying_xxx会销毁么

下面我们不禁有疑问,既然在最后页面走dealloc之后会把isa指针指回,那么动态创建的子类NSKVONotifying_xxx会被销毁么?
下面我们来探究下:

36.png
37.png

我们通过KVO添加观察者动态生成的子类NSKVONotifying_xxx``并不会随着观察对象的销毁销毁而是一直存在于原对象的子类列表中。

重写的setterclass方法
1,class方法重写探索:

我们在上面可以看到动态生成的NSKVONotifying_ZYPerson子类重写了settercalss方法,那么我们不妨来看看 当我们给person对象nickName属性添加观察者后(动态生成子类后),打印下 person这个时候是什么。

38.png

我们发现打印出来的还是ZYPerson 类,也就是说苹果处理这个子类NSKVONotifying_ZYPerson的时候 在明面上给开发者看到的还是原本的那个类。生成的子类只是在后台帮我们处理一些事物并不会显示出来。

2,setter方法重写探索:

下面我们来探索下setter方法到底做了什么。到这里我们不禁思考到一点,NSKVONotifying_ZYPerson子类重写setter方法的目的。如果说重写setter方法就是为了达到监听的作用那么成员变量是不是就监听不到了(属性才会自动生成setter/getter方法)?

39.png
40.png

观察者确实是针对setter方法进行的监听,所以没有setter方法的成员变量监听不到

到此我们又有了一个疑问,KVO确实是重写并监听了setter方法。那么他监听的setter方法是自己重写的呢?还是父类的呢?正常来讲应该是监听自己重写的,不然重写的意义就没有了。下面我们看看:

41.png

从上图我们发现在isa指针指回父类的时候打印父类的nickName发现值变化了,而且是我们监听的值。这就有点奇怪了。下面我们利用lldb下符号断点的方式来查看下ZYPerson属性nickName的变化。下完断点运行点击页面赋值结果如下图42

42.png

到此我们断住了ZYPersonnickName属性。我们利用bt命令观察堆栈变化。如图43

43.png

从上图我们可知在底层其实是调用了Foundation框架的一系列的方法:
-[ZYPerson setNickName:]

-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:]

-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]

_NSSetObjectValueAndNotify

所以我们可以知道,其实在底层他并不是直接调用了setter方法来赋值的,而是调用了一系列如:_changeValueForKeys的方法最终实现setter方法赋值。我们可以利用刚才的断点查看下这些方法都做了什么,我们直接去看断点的汇编:

44.png
45.png
46.png
47.png

从上面的汇编流程我们看到当观察到值变化后调用了NSKeyValueWillChange ,然后走到了断点setter方法,然后就调用NSKeyValueDidChange。然后发通知给观察者。我们进一步验证下,我们在观察者方法打上断点.

48.png
49.png

果然,当监听的setter方法改变时候,就会走NSKeyValueWillChange然后设置值然后走NSKeyValueDidChange方法,最后发通知NSKeyValueNotifyObserver

总结

KVO流程:

1,我们给对象属性设置观察者
2,系统自动生成对象的子类NSKVONotifying_xxx,将原对象的isa指向生成的子类并且自动重写classsetterdealloc等方法。
3,改变观察对象子类NSKVONotifying_xxx属性的值(self.person.nickName = @"WY"; set新值,此时我们实际调用的是动态生成的子类的setter方法而非原类的setter方法)
4,通知父类,调用父类setter方法修改父类的属性值
5,通知观察者持有者,调用到观察者observeValueForKeyPath方法。
6,当观察者持有者调用removeObserver:forKeyPath:释放观察者,就会将isa指回父类。但是此时动态生成的子类NSKVONotifying_xxx不会释放。

至此,文章就算是完结了,对于KVO的一些API原理都有做了简单的分析。下面还有一篇文章我们将会去尝试自己自定以一个KVO

遇事不决,可问春风。站在巨人的肩膀上学习,如有疏忽或者错误的地方还请多多指教。谢谢!

你可能感兴趣的:(KVO 分析)