#FBKVOController源码学习 背景:Apple原生的KVO相信做过iOS开发的童鞋基本上都用过,有一些坑大家说不定都踩过(偷笑)
1.添加和移除观察者的代码要配对写,重复添加观察者可能会导致回调多次。
2.添加,移除观察者,以及回调处理的代码过于分散
3.使用原声KVO时要是忘了移除观察者,程序可能会crash。
直到一天发现了Facebook出的KVOController这个第三方库,它对KVO进行了封装,用起来更为简单,且线程安全,也提供了灰常方便的block回调,用起来贼爽,这里强烈推荐给大家。
这个库用了好长时间了,顺带读读人家的代码吧,读完发现代码质量挺高的,600多行代码,倘若仔细阅读一番,相信你会有收货的。
下面,我们来看看这个小巧精美的库的实现原理。 #源码分析
##NSObject+FBKVOControlle 目录简单,就这两大类文件
FBKVOController.h/.m
NSObject+FBKVOController.h/.m
其中NSObject+FBKVOController通过AssociateObject的方式给每个NSObject提供了一个FBKVOController对象,这样一来,使用的时候更为方便,直接self.KVOController就能获取到一个FBKVOController对象。
[self.KVOController observe:_aBook keyPath:@"name" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
NSString *name = change[NSKeyValueChangeNewKey];
NSLog(@"书名发生变化---name=%@",name);
}];
简单的几行代码就完成了对对象属性的监听,以及对监听到的值进行一些处理,so easy.
##FBKVOController FBKVOController是整个框架中对外提供API的类,来看看这个类的初始化部分
@implementation FBKVOController
{
//作为KVO的管理者,自然要知晓与KVO有关的信息,KVO有关的信息都保存在_objectInfosMap中
NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;
// 使 KVOController 达到线程安全,用于在操作 _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) {
// 1.
_observer = observer;
// 2.
NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
_objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
// 3.
pthread_mutex_init(&_lock, NULL);
}
return self;
}
1.初始化的时候对传入的observer进行弱引用,避免Retain Cycle
2.NSMapTable平时很少用到,类似于NSMutableDictionary,不同的是NSMapTable可以对Key或是value采用不同的内存管理方案.(NSMutableDictionary中对key和value都采用强引用了),这里对key值可以选择若引用或是强引用。
3.初始化锁,后续对_objectInfosMap操作时用到。
初始化完成后,紧接着就是调用API完成对某对象的keypath进行观察
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
// 1.create info
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self
keyPath:keyPath
options:options
block:block];
// 2.observe object with info
[self _observe:object info:info];
}
1.构建一个内部的_FBKVOInfo数据结构,_FBKVOInfo保存了kvo有关的信息,这个类主要是用于保存kvo相关信息。
2.调用私有方法-_observe:info:
- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
//1.
NSMutableSet *infos = [_objectInfosMap objectForKey:object];
//2.
_FBKVOInfo *existingInfo = [infos member:info];
if (nil != existingInfo) {
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);
//3.
[[_FBKVOSharedController sharedController] observe:object info:info];
}
1.根据被观察的对象获取infos。
2.判断要添加的info是否添加过了,这里避免对一个keypath多次添加,避免程序crash,毕竟每次调用addObserverForKeyPath,就得有相应的removeObserverForKey
3.添加info后,把要观察的信息交由_FBKVOSharedController来处理。
到此FBKVOController光荣完成了任务,剩下的就看_FBKVOSharedController啦。
##_FBKVOSharedController
- (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) {
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
}
_FBKVOSharedController的主要职责便是调用苹果原生的KOV -observe:forKeyPath:options:context:方法,开始对属性进行监听。
同时会在原生KVO的回调方法中,将回调的数据进行分发。
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSString *, id> *)change
context:(nullable void *)context
{
_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) {
//block
NSDictionary<NSString *, 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];
}
}
}
}
}
_FBKVOSharedController 会根据注册过的KVO信息 _KVOInfo ,选择不同的方式分发,block 或者选择 子,优先选择block的方式。
要是之前既没有传入block或者_action,则调用观察者 KVO 回调方法。
#KVO的移除操作 一般情况下,我们不需要手动去移除观察者,框架已经帮我们处理好了,每当一个FBKVOController对象被释放的时候,会主动移除观察者。
移除和添加KVO时进行的一系列操作比较相近,代码阅读起来也不费事,就不一一述说了。
说说一些框架使用的小技巧和如何避免潜在的坑。 ##使用框架的小技巧
[self.KVOController observe:_aBook
keyPath:FBKVOKeyPath(_aBook.name)
options:NSKeyValueObservingOptionNew
block:^(id _Nullable observer,
id _Nonnull object,
NSDictionary<NSString *,id> * _Nonnull change)
{
NSString *name = change[NSKeyValueChangeNewKey];
NSLog(@"book:%@",name);
}];
传入keypath参数的时候可以用上FBKVOKeyPath宏,这样在编译的时候倘若发现要添加的keypath不存在时会报错。
##注意,有个小坑
[self.KVOController observe:self keyPath:@"date" options:NSKeyValueObservingOptionNew block:^(NSDictionary *change) {
// to do
}];
监听自身某个属性的时候会出现循环引用,需要在合适的时候(例如在某些操作完成后,不需要再对该属性进行监听了)手动去调用移除监听api。
这里千万别忘记啊!!
整个框架小巧精美,解决了使用原生KVO的一些痛点,代码也不难维护,喜欢用KVO的童鞋用起来吧。