FBKVOController 源码阅读理解
简介
苹果原生API
提供的KVO
有一些显而易见的缺点。
- 添加和移除观察者要配对出现;
- 移除一个未添加的观察者,程序会crash;
- 添加观察者,移除观察者,通知回调,三块儿代码过于分散;
那么,有没有改良版的KVO呢?FBKVOController
是Facebook
开源的代码,主要是对我们经常使用的 KVO
机制进行了额外的一层封装,源码简单,设计感好。其中最亮眼的特色是提供了一个block
回调让我们进行处理,避免KVO
的相关代码四处散落。
使用
[observer.KVOControllerNonRetaining observe:object keyPath:@"keyPath"
options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull
object, NSDictionary * _Nonnull change) {
}];
使用非常简单,提供了block
回调,而且并不需要考虑remove observer
的事情。同时还可以以数组形式,同时对一个被观察者object
的多个不同成员变量进行KVO
。
阅读
KVOController
一共只有两个文件NSObject+FBKVOController
和FBKVOController
。
NSObject+FBKVOController
中的代码和逻辑非常简单,通过Category
的形式结合Runtime
的特性,通过objc_setAssociatedObject
,并支持懒加载的形式,给所有NSObject
类添加了两个FBKVOController
类型的属性。
FBKVOController
中定义了三个类,FBKVOController
、_FBKVOSharedController
和_FBKVOInfo
。因为代码中直接使用的是FBKVOController
,我们先看它。
初始化
// 在FBKVOController.h中
@property (nullable, nonatomic, weak, readonly) id observer;
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
self = [super init];
if (nil != self) {
// observer 本身会持有 FBKVOController,而如果FBKVOController再持有observer,那么必须使用weak
_observer = observer;
// 定义 NSMapTable key的内存管理策略
// retainObserved : 是否对 NSMapTable中的key(key是Observed-被观察者)进行retain操作
// 在默认情况,传入的参数 retainObserved = YES
NSPointerFunctionsOptions keyOptions = retainObserved ?
NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality :
NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
//创建NSMapTable :key 为 id 类型,value 为 NSMutableSet<_FBKVOInfo *> 类型
_objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
// C语言中的互斥锁,一般在开发跨平台的框架时使用
pthread_mutex_init(&_lock, NULL);
}
return self;
}
我在看源码的时候比价陌生的是NSMapTable,于是查阅了一波资料,推荐一篇文章NSMapTable: 不只是一个能放weak指针的 NSDictionary。
简单来说,NSMapTable
与NSDictionary
类似。
NSDictionary
对Key
的内存策略是固定为copy
,因此key
应该是小且高效的,以至于复制的时候不会对CPU
和内存造成负担。NSDictionary
中真的只适合将值类型的对象作为key
(如简短字符串和数字)。当key
为object
时,copy
的开销可能比较大!并不适合自己的模型类来做对象到对象的映射。
NSMapTable
可以自主控制key -> value
的内存管理策略,在这里只能使用相对比较灵活的 NSMapTable。
注册observe
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:
(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
// Debug模式下:不满足条件的注册,会产生断言直接crash
NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters
observe:%@ keyPath:%@ block:%p", object, keyPath, block);
// 非Debug模式下:不满足条件的注册,直接返回
if (nil == object || 0 == keyPath.length || NULL == block) {
return;
}
// create info
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath
options:options block:block];
// observe object with info
[self _observe:object info:info];
}
用NSAssert()
预处理宏捕获错误,它与NSLog
一样,如果使用过多, 也会影响程序运行。不用担心,Xcode
已经帮我们设置好了,在debug
模式下放心使用,Xcode
已经默认将release
环境下的断言取消了, 免除了忘记关闭断言造成的程序不稳定。
在这里创建了一个_FBKVOInfo对象,使用调用者传入的参数(除了object)和 self进行初始化。_FBKVOInfo是一个模型类,负责将记录这些数据。
接上段代码的最后一句[self _observe:object info:info];
。
- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
// lock
pthread_mutex_lock(&_lock);
// _objectInfosMap : 初始化时创建的 NSMapTable
// 其结构是以 被观察者 object 为 key。并不像我们常用的 NSDictionary 那样是以 NSString 为 key
NSMutableSet *infos = [_objectInfosMap objectForKey:object];
// check for info existence
// 使用 NSSet 的 member 方法判断是否已存在
_FBKVOInfo *existingInfo = [infos member:info];
if (nil != existingInfo) {
// observation info already exists; do not observe it again
// unlock and return
pthread_mutex_unlock(&_lock);
return;
}
// lazilly create set of infos
// 如果没有 关于这个 object(被观察者)的相关信息,则创建 NSMutableSet,并添加到 NSMapTable 中
if (nil == infos) {
infos = [NSMutableSet set];
[_objectInfosMap setObject:infos forKey:object];
}
// add info and oberve
[infos addObject:info];
// unlock prior to callout
pthread_mutex_unlock(&_lock);
[[_FBKVOSharedController sharedController] observe:object info:info];
}
NSMapTable
通过key
(object
:被观察的对象),来查找对应的value
(NSMutableSet
:存放_FBKVOInfo
类型的info
),然后从NSMutableSet
中查找是否已经保存了相同_FBKVOInfo
。如果已经存在了相同的_FBKVOInfo
,那么就可以return
不做操作;如果不存在该_FBKVOInfo
,查看是否存在value
(NSMutableSet
),不存在则创建NSMutableSet
,与key
(object
)映射添加到NSMapTable
中。之后再将传入的_FBKVOInfo
添加到NSMutableSet
中。
避免添加重复的keypath
当我在查看的时候比较在意的是NSSet
这个方法:
// 判断集合是否包含对象object
- (nullable ObjectType)member:(ObjectType)object;
它用来查看NSSet
中是否已经包含了相同的object
。之前的代码中我们知道,传入相同的keyPath
也会创建不同的_FBKVOInfo
,那么是如何做到避免了相同的keyPath
重复添加的?
通过重写 - (NSUInteger)hash;
以及 - (BOOL)isEqual:(id)anObject;
这两个方法,来告诉NSSet
“相等”的含义。
为了优化判等的效率, 基于
hash
的NSSet
和NSDictionary
在判断成员是否相等时, 会这样做
Step 1: 集成成员的hash
值是否和目标hash
值相等, 如果相同进入Step 2, 如果不等, 直接判断不相等
Step 2: 在hash
值相同的情况下, 再进行对象判等(- (BOOL)isEqual:
), 作为判等的结果
hash值是对象判等的必要非充分条件
数据结构
观察者observe
持有FBKVOController
,同时FBKVOController
又弱引用(weak
)了observe
,而FBKVOController
拥有成员变量NSMapTable
,NSMapTable
以被观察者(object
)为key
,NSMutableSet
为value
,在NSMutableSet
中,存储了不同info
。如图:
_FBKVOSharedController
_FBKVOSharedController
是单例类,其职责是:接收并转发KVO
通知,通过FBKVOController
框架添加的KVO
都由_FBKVOSharedController
来处理。
- (instancetype)init
{
self = [super init];
if (nil != self) {
NSHashTable *infos = [NSHashTable alloc];
#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
_infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
#elif defined(__MAC_OS_X_VERSION_MIN_REQUIRED)
if ([NSHashTable respondsToSelector:@selector(weakObjectsHashTable)]) {
_infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
} else {
// silence deprecated warnings
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
_infos = [infos initWithOptions:NSPointerFunctionsZeroingWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
#pragma clang diagnostic pop
}
#endif
pthread_mutex_init(&_mutex, NULL);
}
return self;
}
初始化方法中有一个NSHashTable
,同NSMapTable
一样,接触不是很多。NSHashTable
效仿了NSSet
,但提供了比NSSet
更多的操作选项,尤其是在对弱引用关系的支持上,NSHashTable
在对象/内存处理时更加的灵活。相较于NSSet
,NSHashTable
具有以下特性:
- NSSet(NSMutableSet)持有其元素的强引用,同时这些元素是使用hash值及isEqual:方法来做hash检测及判断是否相等的。
- NSHashTable是可变的,它没有不可变版本。
- 它可以持有元素的弱引用,而且在对象被销毁后能正确地将其移除。而这一点在NSSet是做不到的。
- 它的成员可以在添加时被拷贝。
- 它的成员可以使用指针来标识是否相等及做hash检测。
- 它可以包含任意指针,其成员没有限制为对象。我们可以配置一个NSHashTable实例来操作任意的指针,而不仅仅是对象。
在初始化中使用了NSPointerFunctionsWeakMemory
,简单来说就是定义NSHashTable中的元素采用弱引用内存管理策略。当里面存放的_FBKVOInfo
销毁时,NSHashTable
自动将它移除。猜想由于在FBKVOController
中,已经通过NSMutableSet对_FBKVOInfo
持有了一个强引用,那么这里采用弱引用特性的集合类型,给自己省去了很多麻烦的操作,交由系统完成。
继续追踪之注册observe
的方法,最后一句代码[[_FBKVOSharedController sharedController] observe:object info:info];
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
if (nil == info) {
return;
}
// register info
// 注意:在 _FBKVOController 类中的 NSMutableSet 已经强引用了 info
// 这里是为了弱引用 info,才使用 NSHashTable,当 info dealloc 时,同时会从容器中删除
pthread_mutex_lock(&_mutex);
[_infos addObject:info];
pthread_mutex_unlock(&_mutex);
// add observer
// _FBKVOSharedController 是实际的观察者, 随后会进行转发。
// context 是 void * 无类型指针,是 info 的指针
[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
// 如果 state 是原始状态,则改为正在观察的状态,表明是在正在观察的状态
if (info->_state == _FBKVOInfoStateInitial) {
info->_state = _FBKVOInfoStateObserving;
} else if (info->_state == _FBKVOInfoStateNotObserving) {
// 这里是做容错的处理,避免意外情况,与移除观察者的逻辑相关
// this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions,
// and the observer is unregistered within the callback block.
// at this time the object has been registered as an observer (in Foundation KVO),
// so we can safely unobserve it.
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
}
在这段代码中,思路非常清晰,将所有本该在观察者中写的逻辑,改为统一由_FBKVOSharedController
中进行注册,由_FBKVOSharedController
这个单例类来注册和接收。
有些特殊的是context
参数使用的是(void *)info
的指针,这样可以保证context
的唯一性,同时会将info
传递给回调方法,也是为了做容错处理,让代码更加严谨。在修改_state
状态时,也考虑到了在移除观察者方法中存在的某个漏洞,在这里进行安全的移除。注释的清晰,严谨的逻辑,细心的设计,吾辈要多多学习。
实现observeValueForKeyPath:ofObject:Change:context
来接收通知:
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary *)change
context:(nullable void *)context
{
NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);
_FBKVOInfo *info;
{
// lookup context in registered infos, taking out a strong reference only if it exists
// 这里就很巧妙啊,通过注册观察时,将info传过来,然后在NSHashTable中查看是否存在这个info
pthread_mutex_lock(&_mutex);
info = [_infos member:(__bridge id)context];
pthread_mutex_unlock(&_mutex);
}
if (nil != info) {
// take strong reference to controller
// 从这里拿到了FBKVOController
FBKVOController *controller = info->_controller;
if (nil != controller) {
// take strong reference to observer
// 从这里拿到了observer
id observer = controller.observer;
if (nil != observer) {
// dispatch custom block or action, fall back to default action
if (info->_block) {
NSDictionary *changeWithKeyPath = change;
// add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed
if (keyPath) {
// 字典合并,并重新拷贝一份,
// 包含信息有:1、改变了哪个值 mChange 2、 原先的 change 字典
NSMutableDictionary *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
[mChange addEntriesFromDictionary:change];
changeWithKeyPath = [mChange copy];
}
info->_block(observer, object, changeWithKeyPath);
} else if (info->_action) {
// 忽略警告!
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[observer performSelector:info->_action withObject:change withObject:object];
#pragma clang diagnostic pop
} else {
// 默认情况 调用观察者的原生函数!!
[observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
}
}
}
}
}
-
context
传递的是info
,在NSHashTable
中,查看是否还存在这个info
。因为在NSHashTable
是弱引用,所以它有可能已经被释放或者已经被移除。 - 在传递参数和对象初始化赋值成员变量的时候,考虑到安全性,像
block
,NSString
等这些要进行copy
操作。
移除
不用像使用原生的KVO
考虑移除的问题,当被观察者object
销毁时,注册的观察者也就不会再收到回调。
有些场景需要我们手动移除注册,FBKVOController
也提供了相关的方法。
/**
移除被观察的对象的某个属性
*/
- (void)unobserve:(nullable id)object keyPath:(NSString *)keyPath;
/**
移除被观察对象的所有
*/
- (void)unobserve:(nullable id)object;
/**
移除所有
*/
- (void)unobserveAll;
在实现中有这样一段修改_state
的代码:
if (info->_state == _FBKVOInfoStateObserving) {
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
info->_state = _FBKVOInfoStateNotObserving;
根据_state
来进行移除,并修改了_state
的状态,这样就和之前注册observe
时的一段移除逻辑相对应,如果在_FBKVOSharedController
中还未注册成功,就被移除掉的话,那么_state
状态值是_FBKVOInfoStateNotObserving
,那么在注册时就会将这个注册移除掉。
自释放
FBKVOController
是如何做到自释放的?可以归纳为四个字——动态属性。其为观察者绑定动态属性self.KVOController
,动态绑定的KVOController
会随着观察者的释放而释放,KVOController
在自己的dealloc
函数中移除KVO
监听,巧妙的将观察者的remove
转移到其动态属性的dealloc
函数中。
注意
其还是有一定的局限性——对象无法监听自己的属性,如果你的代码是这样的:
[self.KVOController observe:self keyPath:@"date"
options:NSKeyValueObservingOptionNew block:^(NSDictionary *change) {
// to do
}];
很遗憾,循环引用的问题又出现,因为FBKVOController
中的NSMapTable
对象会retain
key
对象:
[_objectInfosMap setObject:infos forKey:object];
在NSObject+FBKVOController
中,动态添加了两个属性
@interface NSObject (FBKVOController)
// 会对被观察者强引用
@property (nonatomic, strong) FBKVOController *KVOController;
// 会对被观察者弱引用
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;
@end
当在使用KVOController
时,如果不手动取消对被观察者的注册,那么只有在observe
的FBKVOController
被释放时,被观察者object
才会被释放掉。
总结
FBKVOController对于喜好使用kvo的工程师来说,是一个好的,精简的开发框架。源码优雅,可读性高,利于自己维护。
优点如下:
- 提供了干净的
block
的回调,避免了处理这个函数的逻辑散落的到处都是。 - 不用担心
remove
问题,不用再在dealloc
中写remove
代码。当然,如果你需要在其他时机进行remove observer
,你大可放心的remove
,不会出现因为没有添加而crash
的问题。
缺点:
- 对象无法监听自己的属性,否则会出现循环引用。