[iOS]|[iOS] KVOController源码分析

消失的四分之三月小姐姐复习了一下之前写的好几十篇文章,发现写时一时爽,写后火葬场。终于开始恢复更新啦~
KVOController
Git:https://github.com/facebook/KVOController
KVOController builds on Cocoa's time-tested key-value observing implementation. It offers a simple, modern API, that is also thread safe. Benefits include:

  • Notification using blocks, custom actions, or NSKeyValueObserving callback.
  • No exceptions on observer removal.
  • Implicit observer removal on controller dealloc.
  • Thread-safety with special guards against observer resurrection
1. 获取一个KVOController实例 我们可以直接从自己拿到一个KVOController,因为KVOController给NSObject加了一个category:
#import "NSObject+FBKVOController.h"#import #if !__has_feature(objc_arc) #error This file must be compiled with ARC. Convert your project to ARC or specify the -fobjc-arc flag. #endif#pragma mark NSObject Category -NS_ASSUME_NONNULL_BEGINstatic void *NSObjectKVOControllerKey = &NSObjectKVOControllerKey; static void *NSObjectKVOControllerNonRetainingKey = &NSObjectKVOControllerNonRetainingKey; @implementation NSObject (FBKVOController)- (FBKVOController *)KVOController { id controller = objc_getAssociatedObject(self, NSObjectKVOControllerKey); // lazily create the KVOController if (nil == controller) { controller = [FBKVOController controllerWithObserver:self]; self.KVOController = controller; }return controller; }- (void)setKVOController:(FBKVOController *)KVOController { objc_setAssociatedObject(self, NSObjectKVOControllerKey, KVOController, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }- (FBKVOController *)KVOControllerNonRetaining { id controller = objc_getAssociatedObject(self, NSObjectKVOControllerNonRetainingKey); if (nil == controller) { controller = [[FBKVOController alloc] initWithObserver:self retainObserved:NO]; self.KVOControllerNonRetaining = controller; }return controller; }- (void)setKVOControllerNonRetaining:(FBKVOController *)KVOControllerNonRetaining { objc_setAssociatedObject(self, NSObjectKVOControllerNonRetainingKey, KVOControllerNonRetaining, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }@end

可以看到其实就是创建了一个FBKVOController然后作为关联对象保存给了self。注意这里可以拿到两种controller,一个是KVOController,另一种是KVOControllerNonRetaining,他们的区别就是在alloc init的时候传入的retainObserved是YES还是NO。
2. FBKVOController初始化 上面的retainObserved到底是啥呢我们现在来看看~
@implementation FBKVOController { NSMapTable *> *_objectInfosMap; pthread_mutex_t _lock; }#pragma mark Lifecycle -+ (instancetype)controllerWithObserver:(nullable id)observer { return [[self alloc] initWithObserver:observer]; }- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved { self = [super init]; if (nil != self) { _observer = observer; NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality; _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0]; pthread_mutex_init(&_lock, NULL); } return self; }- (instancetype)initWithObserver:(nullable id)observer { return [self initWithObserver:observer retainObserved:YES]; }- (void)dealloc { [self unobserveAll]; pthread_mutex_destroy(&_lock); }

每个FBKVOController对象会有两个成员变量:一个是_lock防止对table的多线程操作;一个是NSMapTable记录了被监听对象以及监听了他的哪些属性的infos。
我们默认初始化方法initWithObserver其实最后也是调到了initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved,并且默认是retain的~
在初始化的时候,先init了_objectInfosMap,如果retainObserved为YES,那么_objectInfosMap的key就是NSPointerFunctionsStrongMemory的,反之则是NSPointerFunctionsWeakMemory,也就是map不持有key的强引用。然后init了一下lock。
然后下面正好看到了dealloc,里面也很简单就是调用了自己的unobserveAll方法,以及destroy了锁~
3. 如何addObserver 我们使用的observe方法是做了什么呢?
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block { NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block); 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]; }

其实这里就是创建了一个_FBKVOInfo,其实就是一个model类用于记录keyPath之类的:
@implementation _FBKVOInfo { @public __weak FBKVOController *_controller; NSString *_keyPath; NSKeyValueObservingOptions _options; SEL _action; void *_context; FBKVONotificationBlock _block; _FBKVOInfoState _state; }========================== - (instancetype)initWithController:(FBKVOController *)controller keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block { return [self initWithController:controller keyPath:keyPath options:options block:block action:NULL context:NULL]; }- (instancetype)initWithController:(FBKVOController *)controller keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(nullable FBKVONotificationBlock)block action:(nullable SEL)action context:(nullable void *)context { self = [super init]; if (nil != self) { _controller = controller; _block = [block copy]; _keyPath = [keyPath copy]; _options = options; _action = action; _context = context; } return self; }

然后调用了内部的_observe
- (void)_observe:(id)object info:(_FBKVOInfo *)info { // lock pthread_mutex_lock(&_lock); NSMutableSet *infos = [_objectInfosMap objectForKey:object]; // check for info existence _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 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]; }

之前我们创建的_lock其实就是在这里用的~ 感觉所有lock几乎都是给set map之类的操作用的0.0
上面这段其实就是把我们刚才创建的_FBKVOInfo存起来。_objectInfosMap这个map是以object为key的,然后value是一个set,存了这个target所有加的KVOInfo,如果这个set里面已经包含了要加的info就会直接return,如果没有就加到set里然后调用[[_FBKVOSharedController sharedController] observe:object info:info]; }
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info { if (nil == info) { return; }// register info pthread_mutex_lock(&_mutex); [_infos addObject:info]; pthread_mutex_unlock(&_mutex); // add observer [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info]; 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有一个hashtable用于记录infos,是weak memory的哈,这里先把info加进去,然后给object真正的add KVO啦~~ 最后改一下state~~
这里比较神奇的是为啥会有info->_state == _FBKVOInfoStateNotObserving的情况,看注释其实就是如果是initial的那种KVO,在add的时候就会回调了,也就是[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info]这一行,那么如果在回调里面又remove了KVO,到下面的时候这个state就已经是_FBKVOInfoStateNotObserving了~
需要注意的是add KVO的时候info做为context哦
我们终于看完了怎么加的,然后来看下如何回调的~ 因为上面加监听的时候observer是self,也就是_FBKVOSharedController,所以会回调下面的:
- (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 pthread_mutex_lock(&_mutex); info = [_infos member:(__bridge id)context]; pthread_mutex_unlock(&_mutex); }if (nil != info) {// take strong reference to controller FBKVOController *controller = info->_controller; if (nil != controller) {// take strong reference to 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) { 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]; } } } } }

这里其实就是回调observeValueForKeyPath的时候可以直接从里面拿context,然后把context转为info,得到kvocontroller以及info->_block,为了防止监听了同一个object的多个keyPath,它在回调的时候往change字典里面塞了一个FBKVONotificationKeyPathKey为key的path。
到这里就完整的进行了改变属性触发回调block了~ 下面来看下remove以及自动remove~
[iOS]|[iOS] KVOController源码分析
文章图片
监听实现 4. removeObserver 我们可以手动调用removeObserver:
- (void)unobserve:(nullable id)object keyPath:(NSString *)keyPath { // create representative info _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath]; // unobserve object property [self _unobserve:object info:info]; }- (void)unobserve:(nullable id)object { if (nil == object) { return; }[self _unobserve:object]; }- (void)unobserveAll { [self _unobserveAll]; }

如果传入了keyPath,那么会创建一个_FBKVOInfo哈~ 也可以只取消某个某个被观察者的所有KVO,还可以取消这个KVOController的所有观察,这里每个NSObject会有自己的KVOController哈
- (void)_unobserve:(id)object info:(_FBKVOInfo *)info { // lock pthread_mutex_lock(&_lock); // get observation infos NSMutableSet *infos = [_objectInfosMap objectForKey:object]; // lookup registered info instance _FBKVOInfo *registeredInfo = [infos member:info]; if (nil != registeredInfo) { [infos removeObject:registeredInfo]; // remove no longer used infos if (0 == infos.count) { [_objectInfosMap removeObjectForKey:object]; } }// unlock pthread_mutex_unlock(&_lock); // unobserve [[_FBKVOSharedController sharedController] unobserve:object info:registeredInfo]; }

我们移除某个特定观察者的监听keyPath的时候,它是从_objectInfosMap里面先找到这个被监听这的info set,然后找到我们要取消的set拿出去,也就是删除掉记录,最后调用[[_FBKVOSharedController sharedController] unobserve:object info:registeredInfo]
- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info { if (nil == info) { return; }// unregister info pthread_mutex_lock(&_mutex); [_infos removeObject:info]; pthread_mutex_unlock(&_mutex); // remove observer if (info->_state == _FBKVOInfoStateObserving) { [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info]; } info->_state = _FBKVOInfoStateNotObserving; }

_FBKVOSharedController也是把info先从记录移除,然后removeObserver并改一下info->_state。这里为了防止已经被移除的仍旧执行remove会判断一下state~
移除某个被观察者的所有监听之诶的其实就是循环遍历infos,然后执行上面的移除~
我们上面其实很多地方都在判断这个info是不是已经添加了,之所以可以这么做,是因为info有覆写等同性:
- (NSUInteger)hash { return [_keyPath hash]; }- (BOOL)isEqual:(id)object { if (nil == object) { return NO; } if (self == object) { return YES; } if (![object isKindOfClass:[self class]]) { return NO; } return [_keyPath isEqualToString:((_FBKVOInfo *)object)->_keyPath]; }

也就是对于info而言只要监听的keyPath是同一个,就可以被认为是相等的哈~~
  • 此外为什么_FBKVOSharedController还要有个infos集合呢?明明kvocontroller已经用map+set的方式记录了被观察者们的info了吖。
这里其实是因为KVO被触发的时候回调的是_FBKVOSharedController的方法,然后这个时候传入的context就是info其实,但是作者从infos里面找了一下是不是在infos里面,如果不是就return了相当于。
_infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];

我猜之所以不能拿着context转为info直接用,大概因为infos是weak memory的,如果这个info已经由于某些原因被回收了,那么这个时候我们就在infos里面找不到了,虽然我觉得这个猜测不是很合理叭,如果有知道为啥不能拿着context直接用的朋友欢迎探讨啊~
另外一个问题是,为啥info重写了isEqual以及hash改为了看keyPath的情况下,如果监听了两个不同的对象的相同keyPath属性,所创建的两个info都可以加入到infos的hashTable呢,明明他俩是equal的啊?
这个原因是,其实这里的infos的等同性判断是由NSPointerFunctionsObjectPointerPersonality定义的,也就是不仅仅要hash和isEqual一样,pointer地址的shift的hash也要一样。
  • 我理解的之所以info要重写等同性,是为了在FBKVOController- (void)_observe:(id)object info:(_FBKVOInfo *)info里面可以获取重复加入的对相同object监听的相同keyPath的existingInfo,防止重复的keyPath监听。但这个_FBKVOSharedController单例里面的infos是公用的,并且不想让它keep住info的指针就用了weak的HashTable,但对于不同的被监听object即使是相同keyPath的info也应该可以加入,所以等同性就增加了NSPointerFunctionsObjectPersonality来判断pointer也得偏移hash一样才不能重复加入。
※ 自动移除监听 KVOController的一个好处就是我们可以不用必须手动unobserve,在对象dealloc的时候会自动移除所有监听:
// FBKVOController - (void)dealloc { [self unobserveAll]; pthread_mutex_destroy(&_lock); }

也就是其实是依赖于KVOController被回收的时候去unObserveAll的,而KVOController其实是NSObject的一个fake属性,存在了它的关联dict里。
我们init KVOController的时候有一个属性是要不要保留被观察者的强指针,如果是需要的话也就是retain的方式,就会用strong key的map来保存被观察者;如果是non retain的方式,就会用weak key的map来保存被观察者。
先考虑一下retain的方式会有什么问题,KVOController会持有_objectInfosMap_objectInfosMap强持有了被观察的对象,如果我们是用self监听self,那么这个时候self就会持有一个KVOControllerKVOController又持有了self,这个时候self是无法被释放掉的,那么KVOController也自然不能被释放。这种情况就需要我们在对象即将销毁的时候手动的unobserveAll或者取消对这个对象的所有观察
现在考虑一下non-retain的KVOController,还是上面那个case的话,_objectInfosMapweak持有了被观察的对象,这个时候即使self观察了self,也不会被KVOController持有,那么self是可以释放的,然后就可以释放KVOController了,KVOController在释放走dealloc的时候就会自动的unobserveAll了。
我之前有写关于non-retain的KVOController可能有点问题,如果dealloc的时候被观察的object已经是nil了,unobserveAll的时候就不能把之前添加的observer移除,那么在observer监听对象销毁以后,再次触发了监听对象的KVO事件,会导致程序崩溃。
但上面这个我并没有复现,好像是其实observer监听者是被unsafe指针引用的,如果不removeObserver也是可以正常释放的(这点是肯定的),但是如果你再改它监听的东西,触发KVO回调就会crash。所以比较妥的方式还是要removeObserver的
虽然iOS9及以上notification已经可以不用手动remove就会自动移除,但之前的应该也是类似的思路会unsafe keep观察者,如果再次触发通知会crash叭。
anyway手动移除也好,自动移除也好,不要内存泄漏以及crash就好~
5. 等同性option hashtable判断是不是同一个object有很多可选的option,默认的的那种是判断use -hash and -isEqual, object description是不是一致,其他的可以看注释哈,感觉还是英文说得更清楚~
// Personalities are mutually exclusive // default is object.As a special case, 'strong' memory used for Objects will do retain/release under non-GC NSPointerFunctionsObjectPersonality API_AVAILABLE(macos(10.5), ios(6.0), watchos(2.0), tvos(9.0)) = (0UL << 8),// use -hash and -isEqual, object description NSPointerFunctionsOpaquePersonality API_AVAILABLE(macos(10.5), ios(6.0), watchos(2.0), tvos(9.0)) = (1UL << 8),// use shifted pointer hash and direct equality NSPointerFunctionsObjectPointerPersonality API_AVAILABLE(macos(10.5), ios(6.0), watchos(2.0), tvos(9.0)) = (2UL << 8),// use shifted pointer hash and direct equality, object description NSPointerFunctionsCStringPersonality API_AVAILABLE(macos(10.5), ios(6.0), watchos(2.0), tvos(9.0)) = (3UL << 8),// use a string hash and strcmp, description assumes UTF-8 contents; recommended for UTF-8 (or ASCII, which is a subset) only cstrings NSPointerFunctionsStructPersonality API_AVAILABLE(macos(10.5), ios(6.0), watchos(2.0), tvos(9.0)) = (4UL << 8),// use a memory hash and memcmp (using size function you must set) NSPointerFunctionsIntegerPersonality API_AVAILABLE(macos(10.5), ios(6.0), watchos(2.0), tvos(9.0)) = (5UL << 8),// use unshifted value as hash & equality

我试了一下default的是hash和isEqual决定的,好像木有和description相关哈~
如果加了NSPointerFunctionsOpaquePersonality就即使hash和isEqual相同也会重复加入的,因为指针不同
KVOController用单例作为观察者都回调到一个对象上就比较舒服,你就不用覆写或者hock了,只要记录了回调回来以后要执行神马就OK啦~ 这个库真的是比较精炼的~~ 有时间可以康康吖~~ (P.S.鸣谢周六晚上被我问问题到12点的小哥哥,我真的不是故意的。。
【[iOS]|[iOS] KVOController源码分析】Reference:
https://www.jianshu.com/p/1f7d70ff2002

    推荐阅读