搞完KVC
搞KVO
,谁让他们名字这么接近呢,是吧?KVO
其实我们都很熟悉了,这里就不做过多的文字描述了,无非就是给一个对象的属性添加一个观察者可以实现观察检测该属性值的变化的这么一个机制。我们这里就直接进入主题去探索下他的一些细节和原理。
KVO
方法简介
我们先来看看我们经常用的KVO的方法:
第一个参数是一般为self。第二个为KeyPath
,就是我们要监听的key
。第三个option
和第四个context
我们看下面官方解释:
第三个参数:option
上面官方的解释其实就是对option
这个内容选项做了一个解释简单来讲就是:
NSKeyValueObservingOptionOld
: 选择在更改之前接收观察属性的值 也就是观察旧值。
NSKeyValueObservingOptionNew
: 请求属性的新值。也就是观察新值变化。
NSKeyValueObservingOptionInitial
:发送立即更改通知(在 addObserver:forKeyPath:options:context:returns
之前)。可以使用这个额外的一次性通知来在观察者中建立属性的初始值。
NSKeyValueObservingOptionPrior
: 指示被观察对象在属性更改之前发送通知(除了更改之后的通常通知之外)。更改字典通过包含键NSKeyValueChangeNotificationIsPriorKey
和包含YES
的 NSNumber
值来表示更改前通知。该密钥不存在。当观察者自己的 KVO
合规性要求它为依赖于被观察属性的属性之一调用 -willChange...
方法之一时,您可以使用 prechange
通知。通常的更改后通知来得太晚了,无法及时调用 。
总结的话就是监听的
Key
的不同情况下的值的变化策略。
第四个参数:context
关于文中的context
,其实简单来讲就是为了使我们观察的对象值更加安全更加有针对性的正确获取而存在。你可以设置为Null
。你也可以设置一个静态变量的地址
。而且这是苹果推荐的方式。因为在我们在使用KVO
的过程中,我们可能会对多个对象多个属性进行观察,这时候我们经常用KeyPath
和object
同时判断来区分,但是有时候难免出现重合
或者误写
的情况导致获取的值混乱
,并且代码判断变多,可读性变差,复杂。这个时候context
就可以发挥作用了,用它来区分每一个对象每一个属性值的变化。
示例:
context
可以更加方便和准确的一对一获取对象和值的变化。
KVO
移除
上面的文章大致讲的内容就是举例移除KVO
观察者的方法。同时下方比较值得注意的就是有讲到 如果我们不主动移除观察者
,那么当我们的key
的值发生变化时就会继续给观察者发消息。这样就有一种情况出现。当我们从页面A
跳转到 页面B
我们给页面B
的某个对象(非单例对象)的属性添加观察者。并且发送消息,这个时候没有问题。然后我们从页面B
返回页面A
然后再次进入页面B
并且给新增的观察者发送消息的时候(改变B页面
被观察的某个对象的属性)这个时候也没问题,但是当我们把B页面
的对象换成单例对象
的时候就会奔溃。原因就是因为前面第一次进来B页面
创建的对象观察者没有移除
,当第二次进来的时候单例对象
还存在只是前一个B页面
已经释放了,这个时候系统仍然会给前面释放掉的B页面
里未移除的观察者的发送消息
,但是这个观察者的内存地址已经随着页面B
的消失而被移除了。所以当我们发送消息的时候第一次设置的观察者接收消息就报错了。下面我们把两种情况都运行试试:
情况一:非单例对象添加观察者不移除
非单例对象不移除,不会造成崩溃。
情况二:单例对象添加观察者不移除
在情况一上做些改造:
对单例对象添加观察者不移除,当持有者(
self
)释放后再次给观察者发送消息就会造成崩溃报空指针。
KVO
自动开关控制
1,打开自动开关(默认是打开的)
2,关闭自动开关(默认是打开的)
我们可以利用这个开关来控制某个对象的观察者开关选择
KVO
设置影响因素
可以对观察对象属性设置影响因素,改变影响因素即可得到观察对象属性的变化值。
KVO
观察数组
在KVO
文档开头有告诉我们要了解KVO
就要先了解KVC
(如图24)在上一篇文章KVC分析
中我们重点分析KVC
的细节和要点,其实在KVC
文档里有告诉我们关于KVC
和KVO
的一些关联(如图25)。
上面的文档告诉我们:如果我们在用KVO
来操作可变的一些集合类型属性时就需要按照上面文档给出的方法来执行。
在上图我们发现可变数组在修改值之后change
打印的时候 kind
变成了2
。这个我们去查看下:command+点击
观察方法里的NSKeyValueChangeKey
:
chang
里的kind
是一个枚举类型
,刚好insert
是枚举类型定义的2
。
KVO
原理探究
我们在KVO
的官方文档详细介绍里看到下面一段话:
谷歌翻译:
自动键值观察是使用一种称为isa-swizzling
的技术实现的。
顾名思义,isa
指针指向维护调度表的对象的类。该调度表主要包含指向类实现的方法的指针,以及其他数据。
当观察者为对象的属性注册时,被观察对象的isa
指针被修改,指向中间类而不是真正的类。因此,isa
指针的值不一定反映实例的实际类。
您永远不应该依赖isa
指针来确定类成员资格。相反,您应该使用类方法来确定对象实例的类。
从上面的文档我们可以知道KVO
在实现过程中还生成了中间产物,并且这个中间产物还把我们观察对象的isa
指针进行了指向修改。
动态生成NSKVONotifying_ZYPerson
类
下面我们就来利用断点和LLDB
调试打印探索:
在我们
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<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[I]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
从上图的打印可知:
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<count; i++) {
Method method = methodList[I];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
从上图我们可以看到 除了方法_isKVOA
作为一个标志符号之外 其他的方法都是其父类ZYPerson
拥有的。所以我们可以知道他是在重写父类的方法。
ps:为了方便下面的验证调试,我们创建一个新的类ZYViewController
。把viewController
里的代码都搬到ZYViewController
然后从ViewController
页面push
过去。
继续,上面我们看到NSKVONotifying_ZYPerson
类继承了父类ZYPerson
的方法。而且我们在文档看到说在addObserver
后底层进行了isa-swizzling
操作。将原来对象的isa
指向了新建的类。那我们就来验证下:
在添加观察者的过程中确实进行了
isa
指向转移,从元对象转移指向了动态创建的NSKVONotifying_xxx
类,并且在当前页面销毁走dealloc
的时候将被观察者对象的isa
转移回元对象本身。
动态生成的子类NSKVONotifying_xxx
会销毁么
下面我们不禁有疑问,既然在最后页面走dealloc之后会把isa指针指回,那么动态创建的子类NSKVONotifying_xxx
会被销毁么?
下面我们来探究下:
我们通过
KVO
添加观察者动态生成的子类NSKVONotifying_xxx``并不会
随着观察对象的销毁
而销毁
而是一直存在
于原对象的子类列表中。
重写的setter
和class
方法
1,class
方法重写探索:
我们在上面可以看到动态生成的NSKVONotifying_ZYPerson
子类重写了setter
和calss
方法,那么我们不妨来看看 当我们给person对象
的nickName属性
添加观察者后(动态生成子类后),打印下 person
的类
这个时候是什么。
我们发现打印出来的还是
ZYPerson
类,也就是说苹果处理这个子类NSKVONotifying_ZYPerson
的时候 在明面上给开发者看到的还是原本的那个类。生成的子类只是在后台帮我们处理一些事物并不会显示出来。
2,setter
方法重写探索:
下面我们来探索下setter
方法到底做了什么。到这里我们不禁思考到一点,NSKVONotifying_ZYPerson
子类重写setter方法的目的。如果说重写setter
方法就是为了达到监听的作用那么成员变量
是不是就监听不到了(属性才会自动生成setter/getter
方法)?
观察者确实是针对
setter
方法进行的监听,所以没有setter
方法的成员变量监听不到
到此我们又有了一个疑问,KVO
确实是重写并监听了setter
方法。那么他监听的setter
方法是自己重写的呢?还是父类的呢?正常来讲应该是监听自己重写的,不然重写的意义就没有了。下面我们看看:
从上图我们发现在isa指针
指回父类的时候打印父类的nickName
发现值变化了,而且是我们监听的值。这就有点奇怪了。下面我们利用lldb下符号断点的方式来查看下ZYPerson
的属性nickName
的变化。下完断点运行点击页面赋值结果如下图42
到此我们断住了ZYPerson
的nickName
属性。我们利用bt
命令观察堆栈变化。如图43
从上图我们可知在底层其实是调用了Foundation框架
的一系列的方法:
-[ZYPerson setNickName:]
-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:]
-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]
_NSSetObjectValueAndNotify
所以我们可以知道,其实在底层他并不是直接调用了setter
方法来赋值的,而是调用了一系列如:_changeValueForKeys
的方法最终实现setter
方法赋值。我们可以利用刚才的断点查看下这些方法都做了什么,我们直接去看断点的汇编:
从上面的汇编流程我们看到当观察到值变化后调用了NSKeyValueWillChange
,然后走到了断点setter
方法,然后就调用NSKeyValueDidChange
。然后发通知给观察者。我们进一步验证下,我们在观察者方法打上断点.
果然,当监听的setter方法改变时候,就会走
NSKeyValueWillChange
然后设置值然后走NSKeyValueDidChange
方法,最后发通知NSKeyValueNotifyObserver
。
总结
KVO流程:
1,我们给对象属性设置观察者
2,系统自动生成对象的子类NSKVONotifying_xxx
,将原对象的isa
指向生成的子类并且自动重写class
、setter
、dealloc
等方法。
3,改变观察对象子类NSKVONotifying_xxx
属性的值(self.person.nickName = @"WY";
set
新值,此时我们实际调用的是动态生成的子类的setter
方法而非原类的setter
方法)
4,通知父类,调用父类setter
方法修改父类的属性值
5,通知观察者持有者,调用到观察者observeValueForKeyPath
方法。
6,当观察者持有者调用removeObserver:forKeyPath:
释放观察者,就会将isa
指回父类。但是此时动态生成的子类NSKVONotifying_xxx
不会释放。
至此,文章就算是完结了,对于KVO
的一些API
和原理
都有做了简单的分析。下面还有一篇文章我们将会去尝试自己自定以一个KVO
。
遇事不决,可问春风。站在巨人的肩膀上学习,如有疏忽或者错误的地方还请多多指教。谢谢!