[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 <objc/message.h>
#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_BEGIN
static 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<id, NSMutableSet<_FBKVOInfo *> *> *_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<NSKeyValueChangeKey, id> *)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<NSKeyValueChangeKey, id> *changeWithKeyPath = change;
// add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed
if (keyPath) {
NSMutableDictionary<NSString *, id> *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~
监听实现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就会持有一个KVOController
,KVOController
又持有了self,这个时候self是无法被释放掉的,那么KVOController
也自然不能被释放。这种情况就需要我们在对象即将销毁的时候手动的unobserveAll或者取消对这个对象的所有观察
现在考虑一下non-retain的KVOController
,还是上面那个case的话,_objectInfosMap
weak持有了被观察的对象,这个时候即使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点的小哥哥,我真的不是故意的。。
Reference:
https://www.jianshu.com/p/1f7d70ff2002