本篇简单学习下如何使用 KVO
KVO是什么?
KVO 是 Object-C 中定义的一个通知机制,其定义了一种对象间监控对方状态的改变,并做出反应的机制。对象可以为自己的属性注册观察者,当这个属性的值发生了改变,系统会对这些注册的观察者做出通知。其用途十分广泛,比方说,你的下载进度条是根据下载百分比决定的,那么,可以通过观察下载百分比的改变,刷新进度条的样式,来直观的反应下载进度等等。
KVO的用法
为对象的属性注册观察者
1 |
- (void)addObserver:(NSObject *)observer |
- observer: 观察者对象. 其必须实现方法
observeValueForKeyPath:ofObject:change:context:
. - keyPath: 被观察的属性,其不能为
nil
. - options: 设定通知观察者时传递的属性值,是传改变前的呢,还是改变后的,通常设置为
NSKeyValueObservingOptionNew
。 - context: 一些其他的需要传递给观察者的上下文信息,通常设置为
nil
。
观察者接收通知
1 |
- (void)observeValueForKeyPath:(NSString *)keyPath |
- keyPath: 被观察的属性,其不能为
nil
. - object: 被观察者的对象.
- change: 属性值,根据上面提到的
Options
设置,给出对应的属性值。 - context: 上面传递的
context
对象。
清除观察者
1 |
- (void)removeObserver:(NSObject *)anObserver forKeyPath:(NSString *)keyPath |
注意事项
使用KVO消息传递机制有两个要求:
- 观察者必须知道被观察对象,即在同一作用域。
- 观察者还需要知道被观察对象的生命周期,因为在销毁发送者对象之前,需要取消观察者的注册。
KVO 原理
实现原理
当一个对象使用 KVO 监听,iOS 会修改对象的 isa 指针,改为指向一个由 runtime 创建的类,这个类的 superclass 为原来的 class 对象。动态创建的类拥有自己的 set 方法实现,内部会依次调用:
1 |
1. - (void)willChangeValueForKey:(NSString *)key; |
在 didChangeValueForKey
中会调用监听器的监听方法。所以直接修改成员变量不会触发 KVO。
对象的 isa 指向可以通过 someInstance->isa
查看
如何手动触发 KVO
有时候为了在不改变属性值的情况下,触发监听方法,所以要手动触发 KVO。手动调用:
1 |
- (void)willChangeValueForKey:(NSString *)key; |
KVC 触发 KVO
KVC 会触发 KVO。即使成员变量没有 get set 方法,KVC 手动调用 willChangeValueForKey:
和 didChangeValueForKey:
。
KVOController
KVO 存在的问题
KVO 本身写起来并不友好,存在一些问题:
- 需要手动移除观察者
- 处理观察事件需要和注册观察事件割裂开
那么如何解决呢?
没有什么是一个中间变量不能解决的。可以创建一个实例,观察的事件由它分发,在其 dealloc 方法中移除观察者。这样就不用在外部业务方法中移除了。KVOController
也是这么做的。
使用方式
使用方式很简单,首先创建一个 KVOController
实例,然后执行 observe:keyPath:options:block:
方法注册观察者:
1 |
|
其中, block 的第一个参数 ClockView 为 observer,第二个参数 Clock 为 target,第三个参数为变化的 keypath 的变化前后值的字典。
整体结构
整体结构如上图所示,KVOController
对象有一个观察者 observer
还有一个 NSMapTable
的 _objectInfosMap
,它的键是被观察的对象 object
,值是被观察对象的各个属性的封装的 NSMutableSet
,Set 中的每一个元素都保存了要执行的 block
源码解析
创建 KVOController 实例
各个初始化方法都会来到这个方法:
1 |
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved |
这里面的 observer
在外部会强引用这个 KVOController
,所以初始化方法中的 _observer
默认是弱引用的,不用担心循环引用的问题。
由于参数 retainObserverd
默认是 YES
,所以创建的 NSMapTable
的 key 的选项默认是:
1 |
NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality |
选项的具体含义在下面介绍 NSMapTable
的时候会提及。总之,这里创建的 NSMapTable
默认键和值都是强引用的,所以如果被观察的对象是观察者 observer
本身,就要注意要传入弱引用,否则会产生循环引用。
最后创建了一个 pthread_mutex_t
注册观察方法
注册观察方法外部调用的方法:
1 |
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context { |
外部调用的时候会创建一个 FBKVOInfo
对象,这个对象保存了观察回调所需的各种要素,随后执行内部的注册方法:
1 |
- (void)_observe:(id)object info:(_FBKVOInfo *)info { |
可以看到,这里把要观察的对象 object
和刚刚创建的保存回调要素的 KVOInfo
对象保存在了 _onjectInfosMap
这个 NSMapTable
中。
所以,一个被观察的对象 object
的所有注册观察的 keyPath
都会保存在一个 NSMutableSet
中,再把 object
和 NSMutableSet
作为键值对,保存在 NSMapTable
中。
最后调用的 _FBKVOSharedController
才是真正注册观察者的地方。
真正用来注册观察者的 KVOSharedController
KVOSharedController
中还用到了 NSHashTable
,但是和 KVOController
中 NSMapTable
不同的是,这里都是通过弱引用保存的。原因是因为外部的 KVOController
都保存确保不会被回收了,这里就不需要再强引用了。
1 |
@implementation |
KVOSharedController
是个单例,所有的注册和回调逻辑都是通过这个实例完成的.
注册方法:
1 |
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info |
还是能比较容易想到这个方法做了什么的。注意这里把 KVOInfo
实例作为 context
传了进去。
回调方法:
1 |
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object |
也是顺利成章的,接收到回调事件后,执行 block 里面的内容。刚才通过 context
把 KVOInfo
传入,现在再通过 context
,把 KVOInfo
取出来。(说实话,这里的操作,包括下面的各种判空,其实有点多余。因为本身外部是强引用的,不会出现为空的情况)
取消注册
取消注册在 KVOController
中存在三种情况:
- 取消所有的监听
- 取消关于某一个 object 的所有的监听
- 取消某一个 object 内的某个 keypath 的监听
1 |
// 对应第三种情况 |
这三种情况中,第一种删除所有 Object 是第二种删除某一个 Object 的特殊情况,第二种又是第一种删除某一个 Object 的某个 keyPath 的特殊情况。总的来说,都是操作 _objectInfosMap
,将强引用的对象删除,然后交给 KVOSharedController
操作。
这三种情况最后都归结到 KVOSharedController
中去:
1 |
- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info { |
dealloc 时候的自动取消注册
由于观察者把整个过程交给了 KVOController
,所以在观察者销毁的时候,KVOController
也会一起执行 dealloc
方法,来清除所有的监听。
1 |
- (void)dealloc |
总结
整个库的流程是:KVOController
把观察的对象作为其 NSMapTable
属性 _objectInfomap
的键,把整个回调环境组成的对象 KVOInfo
作为值保存起来。同时通过一个单例的 KVOSharedController
执行具体的注册于监听方法。
在我看来,本身 KVOSharedController
这个单例其实是没有多大意义的,在每个 KVOController
中其实就可以处理这些监听与回调了。不过这么写可能是为了更好的职责分离吧。
最后还要强调一点,我认为 KVO 自动取消监听的核心在于让 KVOController
这类的中间类的生命周期和被监听的 object
同步,而不是和 Observer
同步。因为,只有在被监听对象回收的时候取消监听才能真正避免 crash 的危险。
事实上,我们一般在 VC 和 VM 中做监听,监听的对象是 VC 或者 VM 的属性,这种变相的把观察者 Observer
和被观察对象 object
的生命周期同步了,因而把 KVOController
作为监听者的属性也不会有问题。但是这会造成使用者的误解。
所以我认为 KVOController 这种一个 KVOController
监听多个对象的做法其实是有问题的,也是不应该被鼓励的。真正应该的是像我图上画的,给每个被观察的对象绑定一个 KVOController
实例。
NSMapTable
NSMapTable 相比较 NSDictionary 的优势有:
- NSDictionary 必须是 key-obj 的形式,key 必须是满足 NSCopying 协议的;NSMapTable 则是 obj-obj 的形式
- NSDictionary 的 obj 是强引用;NSMapTable 的 key 和 value 都可以自己决定是强引用还是弱引用。如果弱引用回收后,会自动删除。
创建
创建 NSMapTable 的时候,需要指定键和值的选项:
1 |
NSMapTable *keyToObjectMapping = [NSMapTable mapTableWithKeyOptions:NSMapTableCopyIn valueOptions:NSMapTableStrongMemory]; |
上面创建的 NSMapTable 将和 NSDictionary 一模一样,复制 key
,并对它的 object
引用计数 +1。
NSMapTable 的选项
- NSMapTableStrongMemory (a “memory option”)
- NSMapTableWeakMemory (a “memory option”)
- NSMapTableObjectPointerPersonality (a “personality option”)
- NSMapTableCopyIn (a “copy option”)
其中前两个 memory option 就是控制是对 obj 进行强引用还是弱引用。
personality option 在我的理解中是针对 key 的,它决定是否使用对象的指针来进行 hash (NSDictionary 中使用 NSString 来进行 hash,决定 object 的存储位置)。如果不指定这个选项,默认是使用整个对象进行 hash,那么在存储的过程中,作为 key 的这个对象是不能被改变的(对象变了,hash 值变了,自然就找不到 object 了)。
copy option 选项表明是否执行对象的 copy 方法,深拷贝一个新的对象进行存储。
NSArray 和 NSPointerArray 的区别
NSPointerArray 可以保存 NULL,因此,NSPointerArray 中的对象可以是 weak 的,销毁直接将该位置职位 null
NSHashTable 和 NSSet 区别
NSHashTable 是可变的,且 NSHashTable 可以放弱引用对象
参考
原文:大专栏 KVO 实践及 FBKVOController 原理