学习KVO的封装。
KVOController源码只有700行左右,读一遍下来还是比较通畅的。这里做一个记录。
一、使用 使用起来极其简便。
1 2 3 4 [self .KVOController observe:self .myButton keyPath:@"backgroundColor" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary <NSKeyValueChangeKey ,id > * _Nonnull change) { NSLog (@"%@" , change[NSKeyValueChangeNewKey ]); }];
这里的self.KVOController
可以自己创建,也可以使用默认,因为KVOController
是懒加载的。
一般情况下是像上面这样使用的,还有一种情况,不需要强持有被观察者的时候:
1 2 3 [self .KVOControllerNonRetaining observe:self .myButton keyPath:@"backgroundColor" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary <NSKeyValueChangeKey ,id > * _Nonnull change) { NSLog (@"%@" , change[NSKeyValueChangeNewKey ]); }];
只需使用self.KVOControllerNonRetaining
即可不增加被观察者self.myButton
的引用计数。
二、分类 1 2 3 4 @interface NSObject (FBKVOController)@property (nonatomic, strong) FBKVOController *KVOController;@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;@end
要实现以上使用的方式,是给 NSObject
分类添加两个属性:KVOController
和KVOControllerNonRetaining
。这个比较简单,使用Runtime
的关联属性即可。值得一提的是作者在 getter
方法里使用了懒加载,只有当使用到KVOController
或者KVOControllerNonRetaining
的时候,才会创建。当然,也可以选择自行创建。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 - (FBKVOController *)KVOController { id controller = objc_getAssociatedObject(self , NSObjectKVOControllerKey ); if (nil == controller) { controller = [FBKVOController controllerWithObserver:self ]; self .KVOController = controller; } return controller; } - (FBKVOController *)KVOControllerNonRetaining { id controller = objc_getAssociatedObject(self , NSObjectKVOControllerNonRetainingKey ); if (nil == controller) { controller = [[FBKVOController alloc] initWithObserver:self retainObserved:NO ]; self .KVOControllerNonRetaining = controller; } return controller; }
以上两个 getter
方法分别对应强引用被观察者和弱引用被观察者。
三、接口 由此可以看到,核心功能的实现依赖于FBKVOController
。
1 2 3 4 5 6 7 8 + (instancetype )controllerWithObserver:(nullable id )observer; - (instancetype )initWithObserver:(nullable id )observer; - (instancetype )initWithObserver:(nullable id )observer retainObserved:(BOOL )retainObserved NS_DESIGNATED_INITIALIZER ; - (instancetype )init NS_UNAVAILABLE ; + (instancetype )new NS_UNAVAILABLE ;
构造方法里主要暴露了两种初始化方式,其中通过initWithObserver
这个方法可以设置参数retainObserved
以表明是否需要强引用被观察者。
1 @property (nullable , nonatomic , weak , readonly ) id observer;
只有一个只读属性,给出被观察者对象。
1 2 3 - (void )observe:(nullable id )object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions )options block:(FBKVONotificationBlock)block; - (void )observe:(nullable id )object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions )options action:(SEL)action; - (void )observe:(nullable id )object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions )options context:(nullable void *)context;
作者给出了回调的三个选项:block回调,选择子回调以及 KVO 默认方法回调。可以在添加被观察者的时候自行选择。
1 2 3 - (void )observe:(nullable id )object keyPaths:(NSArray <NSString *> *)keyPaths options:(NSKeyValueObservingOptions )options block:(FBKVONotificationBlock)block; - (void )observe:(nullable id )object keyPaths:(NSArray <NSString *> *)keyPaths options:(NSKeyValueObservingOptions )options action:(SEL)action; - (void )observe:(nullable id )object keyPaths:(NSArray <NSString *> *)keyPaths options:(NSKeyValueObservingOptions )options context:(nullable void *)context;
考虑到不一定只观察一个对象的一个成员变量,因此作者提供了keyPaths
选项,可以同时观察一个对象的多个keyPath:传入一个字符串数组即可。
1 2 3 - (void )unobserve:(nullable id )object keyPath:(NSString *)keyPath; - (void )unobserve:(nullable id )object; - (void )unobserveAll;
移除监听提供三种接口:移除某个对象某个keyPath 的监听,移除对某个对象的监听,取消观察者对所有对象的所有监听。
四、FBKVOController实现 构造器 1 2 3 4 @implementation FBKVOController { NSMapTable <id , NSMutableSet <_FBKVOInfo *> *> *_objectInfosMap; pthread_mutex_t _lock; }
FBKVOController
主要维护了一个NSMapTable
。key
是被观察的对象,value
是NSMutableSet
类型的集合(内部元素是_FBKVOInfo
类型)。维护一个NSMapTable
的原因是:便于观察一个对象的多个keyPath
,这个对象作为 key
,这许多个keyPath
封装成一个个_FBKVOInfo
存入NSMutableSet
中。另外一个成员变量_lock
主要是保证线程安全。
1 2 3 4 5 6 7 8 9 10 11 - (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 ; }
FBKVOController
所有暴露的构造方法接口都指向了上面的那个实现。这个方法只做了三件事: 1,初始化线程锁_lock
,2,根据retainObserved
参数创建不同类型的NSMapTable
,是选择”强-强”还是选择”弱-强”。3,属性observer
赋值。
由此可见,FBKVOController
本身对观察者observer
是弱引用的(有一个 weak
属性的observer
成员变量),通过维护一个NSMapTable
来最终确定对被观察者的强弱引用关系。
接口实现 1 2 3 4 5 6 7 8 9 - (void )observe: (nullable id)object keyPath: (NSString *)keyPath options: (NSKeyValueObservingOptions)options block: (FBKVONotificationBlock)block { if (nil == object || 0 == keyPath.length || NULL == block) { return ; } _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController: self keyPath: keyPath options: options block: block]; [self _observe: object info: info]; }
以添加一个被观察者并且回调是 block 为例。在这个方法里首先是对参数的合理性判断,要求object
、keyPath
以及block
均是合理值。 接着把keyPath
、options
、block
包装成一个数据结构_FBKVOInfo
。 最后调用自己的_observe:info:
方法,传入object
和info
。
_FBKVOInfo数据结构 1 2 3 4 5 6 7 8 9 10 11 12 13 @interface _FBKVOInfo : NSObject@end @implementation _FBKVOInfo {@public __weak FBKVOController *_controller; NSString *_keyPath ; NSKeyValueObservingOptions _options ; SEL _action ; void *_context ; FBKVONotificationBlock _block ; _FBKVOInfoState _state ; }
_FBKVOInfo
是一个数据结构,包含了监听的keyPath
、block
、选择子、context
等等元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 - (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 ; }
构造方法的实现就是这样,不过有两个关键点:block
和keyPath
调用一下 copy
方法。keyPath
调用一下 copy
的原因是,这里的_keyPath
是使用__strong
修饰的,如果外面传进来的是不可变字符串,自然没有啥问题,可是一旦传进来一个可变字符串,如果直接赋值_keyPath = keyPath;
,当这个可变字符串改变就会造成_keyPath
也改变,比较容易产生不可控事件,所以调用 copy
方法,也即是深复制浅复制的问题。
没有深复制的示例如下:
1 2 3 4 5 6 7 NSMutableString *str = [NSMutableString stringWithString:@"key" ];_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:str options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary <NSKeyValueChangeKey ,id > * _Nonnull change) { } action:@selector (push) context:nil ]; [str appendString:@"new" ]; NSLog (@"%@" , info->_keyPath);
同样,block
的copy
是把block
从栈拷贝到堆中,防止被释放。因为block
作为参数传入函数不会被 copy
,依然在栈上,方法执行完立即释放的。
在ARC下:大部分情况下系统会把Block自动copy到堆上。
Block作为变量: 方法中声明一个 block 的时候是在栈上; 引用了外部局部变量或成员变量, 并且有赋值操作(有名字),会被 copy 到堆上; 赋值给附有__strong修饰符的id类型的类或者Blcok类型成员变量时; 赋值给一个 weak 变量不会被 copy;
Block作为属性: 用 copy 修饰会被 copy;
Block作为函数参数: 作为参数传入函数不会被 copy,依然在栈上,方法执行完即释放; 作为函数的返回值会被 copy 到堆;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 - (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]; }
_FBKVOInfo
还做了一点其他的事: 1.重写了- (NSUInteger)hash;
方法,使用_keyPath
的 hash
值来作为_FBKVOInfo
的 hash
值。分配的这个hash
值(即用于查找集合中成员的位置标识),就是通过hash
方法计算得来的,且hash
方法返回的hash
值最好唯一。 2.重写了- (BOOL)isEqual:(id)object;
方法,满足Equal
的条件有两个: 首先是类对象一致,再者是_keyPath
匹配。换句话说,_keyPath
决定了_FBKVOInfo
是否是同一个。为了优化判等的效率,基于hash
的NSSet
和NSDictionary
在判断成员是否相等时, 会这样做Step1: 成员的hash
值是否和目标hash
值相等,如果相同进入Step 2,如果不等,直接判断不相等 Step 2: hash
值相同(即Step 1)的情况下,再进行对象判等, 作为判等的结果。
hash值是对象判等的必要非充分条件
NSPointerFunctionsObjectPointerPersonality
对于 isEqual:
和 hash
使用直接的指针比较。使用移位指针(shifted pointer)来做hash检测及确定两个对象是否相等;同时使用description方法来做描述字符串。
Personalities determine hashing and equality. NSPointerFunctionsObjectPersonality provides the standard Foundation behavior of using hash and isEqual:. You can also use NSPointerFunctionsObjectPointerPersonality, which treats the contents as objects, but uses direct pointer value comparison; this is useful if you need a collection to work with object identity rather than value. NSPointerFunctionsObjectPointerPersonality 使用 ==
判断相等 NSPointerFunctionsObjectPersonality 使用hash
和isEqual:
判断相等
_observe:info:方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 - (void)_observe :(id)object info:(_FBKVOInfo *)info { pthread_mutex_lock(&_lock ); NSMutableSet *infos = [_objectInfosMap objectForKey:object]; _FBKVOInfo *existingInfo = [infos member:info]; if (nil != existingInfo) { pthread_mutex_unlock(&_lock ); return; } if (nil == infos) { infos = [NSMutableSet set ]; [_objectInfosMap setObject:infos forKey:object]; } [infos addObject:info]; pthread_mutex_unlock(&_lock ); [[_FBKVOSharedController sharedController] observe:object info:info]; }
首先加锁。把被观察的对象object
作为key
从自己的_objectInfosMap
获取其对应的NSMutableSet
类型的集合,如果这个集合包含了已经封装好的info
对象,说明已经对这个info
添加过监听了,解锁直接 return 就是了。 如果这个infos
集合不存在,创建。把info
元素添加到这个infos
集合中。解锁。调用[[_FBKVOSharedController sharedController] observe:object info:info];
方法。
可见这个方法主要是使用_objectInfosMap
保存了封装好的info
对象,具体监听调用逻辑依赖于_FBKVOSharedController
。
五、_FBKVOSharedController实现 初始化 1 2 3 4 5 6 7 8 9 10 11 12 @interface _FBKVOSharedController : NSObject + (instancetype )sharedController; - (void )observe:(id )object info:(nullable _FBKVOInfo *)info; - (void )unobserve:(id )object info:(nullable _FBKVOInfo *)info; - (void )unobserve:(id )object infos:(nullable NSSet *)infos; @end @implementation _FBKVOSharedController { NSHashTable <_FBKVOInfo *> *_infos; pthread_mutex_t _mutex; }
_FBKVOSharedController
是一个单例。作用是观察 _FBKVOInfo
中的 keyPath
,并给予回调(回调的类型可以是 block
、selector
、系统回调方法)。
暴露出两个方法:
添加监听,参数为_infos
移除监听,参数为_FBKVOInfo
或者NSSet
类型的infos
(容器内的元素仍然是_FBKVOInfo
)
内部维护了一个哈希表(NSHashTable)_infos
,用于保存这些_FBKVOInfo
。除此之外还有一个锁:_mutex
,用于实现线程安全。
哈希表的创建:
1 2 3 4 NSHashTable *infos = [NSHashTable alloc];_infos = [infos initWithOptions:NSPointerFunctionsWeakMemory |NSPointerFunctionsObjectPointerPersonality capacity:0 ]; NSPointerFunctionsWeakMemory : 持弱指针引用着_FBKVOInfo对象。NSPointerFunctionsObjectPointerPersonality 使用==判定相等。
可见_FBKVOSharedController
只是单纯地掌管_FBKVOInfo
集合,它只需要解析_FBKVOInfo
并给observer回调即可,其他的一切都不关心。
添加监听 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 - (void )observe: (id)object info: (nullable _FBKVOInfo *)info { if (nil == info) return ; pthread_mutex_lock(&_mutex); [_infos addObject: info]; pthread_mutex_unlock(&_mutex); [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]; } }
容器中添加 info 元素,添加监听。
移除监听 1 2 3 4 5 6 7 8 9 10 11 12 - (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info { if (nil == info) return; pthread_mutex_lock(&_mutex ); [_infos removeObject:info]; pthread_mutex_unlock(&_mutex ); if (info->_state == _FBKVOInfoStateObserving ) { [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info]; } info->_state = _FBKVOInfoStateNotObserving ; }
容器中移除 info 元素,移除监听。
系统KVO调用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 - (void )observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id )object change:(nullable NSDictionary <NSKeyValueChangeKey , id > *)change context:(nullable void *)context { _FBKVOInfo *info; { pthread_mutex_lock(&_mutex); info = [_infos member:(__bridge id )context]; pthread_mutex_unlock(&_mutex); } if (nil != info) { FBKVOController *controller = info->_controller; if (nil != controller) { id observer = controller.observer; if (nil != observer) { if (info->_block) { NSDictionary <NSKeyValueChangeKey , id > *changeWithKeyPath = change; 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) { [observer performSelector:info->_action withObject:change withObject:object]; } else { [observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context]; } } } } }
最终在系统方法中给予不同类型的回调。
1 2 3 4 5 typedef NS_ENUM(uint8_t, _FBKVOInfoState ) { _FBKVOInfoStateInitial = 0 , _FBKVOInfoStateObserving , _FBKVOInfoStateNotObserving , };
作者使用了三个枚举值来记录监听状态。会不会是多此一举呢?不会。作用主要体现在添加监听的方法里有个移除监听操作:
1 2 3 4 5 6 7 8 [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]; }
“未监听状态便移除“是怎么出现的?示例如下:
1 2 3 [self .KVOController observe:self .myButton keyPath:@"backgroundColor" options:NSKeyValueObservingOptionInitial block:^(id _Nullable observer, id _Nonnull object, NSDictionary <NSKeyValueChangeKey ,id > * _Nonnull change) { [self .KVOController unobserve:self .myButton keyPath:@"backgroundColor" ]; }];
包含了NSKeyValueObservingOptionInitial
选项且在回调中移除了监听就会出现这种情况。因为如果有NSKeyValueObservingOptionInitial
选项,在添加监听的时候就会有回调。调用栈如下: 执行到[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
这行代码的时候,首先添加监听,接着调用回调,而回调中又移除了观察,这时info的状态被设置为_FBKVOInfoStateNotObserving
。接着进入了下面的 if-else 判断中,才有了移除监听的操作。可见逻辑非常严谨。
NSKeyValueObservingOption参考:
NSKeyValueObservingOptionNew:接收方法中使用change参数传入变化后的新值,键为:>NSKeyValueChangeNewKey; NSKeyValueObservingOptionOld:接收方法中使用change参数传入变化前的旧值,键为:>NSKeyValueChangeOldKey; NSKeyValueObservingOptionInitial:注册之后立刻调用接收方法,如果配置了>NSKeyValueObservingOptionNew,change参数内容会包含新值,键为:>NSKeyValueChangeNewKey; NSKeyValueObservingOptionPrior:如果加入这个参数,接收方法会在变化前后分别调用一次,共两>次,变化前的通知change参数包含notificationIsPrior = 1。其他内容根据>NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld的配置确定。
六、一个函数 其实不是一个函数,不过是为了实现一个功能,核心还是一个函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 static NSString *describe_option(NSKeyValueObservingOptions option) { switch (option) { case NSKeyValueObservingOptionNew : return @"NSKeyValueObservingOptionNew" ; break ; case NSKeyValueObservingOptionOld : return @"NSKeyValueObservingOptionOld" ; break ; case NSKeyValueObservingOptionInitial : return @"NSKeyValueObservingOptionInitial" ; break ; case NSKeyValueObservingOptionPrior : return @"NSKeyValueObservingOptionPrior" ; break ; default : NSCAssert (NO , @"unexpected option %tu" , option); break ; } return nil ; } static void append_option_description(NSMutableString *s, NSUInteger option) { if (0 == s.length) { [s appendString:describe_option(option)]; } else { [s appendString:@"|" ]; [s appendString:describe_option(option)]; } } static NSUInteger enumerate_flags(NSUInteger *ptrFlags) { NSCAssert (ptrFlags, @"expected ptrFlags" ); if (!ptrFlags) return 0 ; NSUInteger flags = *ptrFlags; if (!flags) return 0 ; NSUInteger flag = 1 << __builtin_ctzl(flags); flags &= ~flag; *ptrFlags = flags; return flag; } static NSString *describe_options(NSKeyValueObservingOptions options) { NSMutableString *s = [NSMutableString string]; NSUInteger option; while (0 != (option = enumerate_flags(&options))) { append_option_description(s, option); } return s; }
不使用 switch-case 把位移枚举的值遍历出来了。
1 2 3 4 5 6 7 8 NSUInteger flag = 1 << __builtin_ctzl(flags ); flags &= ~flag;*ptrFlags = flags ;
不失为一个好办法。
七、总结
使用 KVOController 进行键值观测可以说完美地解决了在使用原生 KVO 时遇到的各种问题。
1.不需要手动移除观察者; 2.实现 KVO 与事件发生处的代码上下文相同,不需要跨方法传参数; 3.使用 block 来替代方法能够减少使用的复杂度,提升使用 KVO 的体验; 4.每一个 keyPath 会对应一个属性,不需要在 block 中使用 if 判断 keyPath;
以上引自draveness。解释如下: 1.NSMapTable 可以持有键和值的弱引用,当键或者值当中的一个被释放时,整个这一项就会被移除掉。
1 2 3 NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory |NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory |NSPointerFunctionsObjectPointerPersonality ;_objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory |NSPointerFunctionsObjectPersonality capacity:0 ];
2.因为是在@selector(observeValueForKeyPath:ofObject:change:context:)中处理的回调。 3.使用FBKVONotificationBlock。 4._FBKVOInfo封装。
纵观全部代码,作者首先给分类添加了两个属性,用于接口调用。这些属性都指向了FBKVOController,FBKVOController主要维护了一个NSMapTable。key 是所观察的对象,value 是NSMutableSet类型的集合,其内部元素是_FBKVOInfo类型对象。一个_FBKVOInfo对象对应一个信息封装。之所以使用NSMapTable集合是便于对同一个对象的多个keyPath进行观察,同时处理被观察者的强弱引用。另外_FBKVOInfo对象是对FBKVOController、keyPath、context、回调block等信息的封装。最后,各个FBKVOController把所有对观察的处理交给单例_FBKVOSharedController,这个单例调用系统KVO方法回调、处理包含所有_FBKVOInfo对象的NSHashTable集合。
参考资料KVOController iOS中Block使用注意点 isEqual与hash iOS学习笔记——KVO