KVOController详解
KVO在MVC架构的项目中是一种特别有用的技术。KVOController建立在Cocoa经受时间考验的KVO实现上。它提供简单、现代的API,并且是线程安全的。优点如下:
- 通知是通过block、action或者NSKeyValueObserving回调(即-observeValueForKeyPath:ofObject:change:context)来实现;
- 不会出现移除observer的异常;
- 在controller销毁时隐式地移除observer;
- 保证了线程安全,避免出现这样的异常。
简单地说,KVOController让我们更优雅、简单、安全地使用KVO。
源码分析
KVOController是面向观察者设计的,而不是跟直接使用Cocoa的KVO时一样面向被观察者。这是一个很轻的开源库,只由一个FBKVOController
类和一个NSObject+FBKVOController
分类构成。
FBKVOController.h
FBKVOController
类是用于管理整个KVO流程,它持有了观察者对象,又提供了添加观察行为的API,头文件内容如下:
初始化方法
/**
@param observer 观察者对象
@param retainObserved 是否强引用被观察对象
*/
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved;
/**
简便初始化方法。
retainObserved默认为YES。
*/
- (instancetype)initWithObserver:(nullable id)observer;
公开属性与API
/** 弱引用的方式持有观察者对象 */
@property (nullable, nonatomic, weak, readonly) id observer;
/**
对指定对象的指定keyPath添加观察,通过Block进行回调
@param object 被观察对象
@param keyPath 被观察对象的keyPath
@param options NSKeyValueObservingOptions
*/
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block;
/**
以SEL的方式回调
*/
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options action:(SEL)action;
/**
不注册block和sel则回调观察者类的-observeValueForKeyPath:ofObject:change:context:方法
*/
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
/** 一次性监听多个keyPath */
- (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;
/** 注销观察对象对应的keyPath */
- (void)unobserve:(nullable id)object keyPath:(NSString *)keyPath;
- (void)unobserve:(nullable id)object;
- (void)unobserveAll;
KVOController的API提供了三种回调的方式,也提供了一次性添加多个观察的keyPaths的方法。此外,头文件中还定义了两个有意思的宏,用于判断编译时属性是否存在(防止手滑写错),代码如下:
#define FBKVOKeyPath(KEYPATH) \
@(((void)(NO && ((void)KEYPATH, NO)), \
({ const char *fbkvokeypath = strchr(#KEYPATH, '.'); NSCAssert(fbkvokeypath, @"Provided key path is invalid."); fbkvokeypath + 1; })))
#define FBKVOClassKeyPath(CLASS, KEYPATH) \
@(((void)(NO && ((void)((CLASS *)(nil)).KEYPATH, NO)), #KEYPATH))
因为我们平时直接在写入keyPath时,都是以字符串的方式写入,如果字符串拼写错误的话可能会造成无法监听相应属性的问题。例如:
Person *person = [[Person alloc] init];
Observer *observer = [[Observer alloc] init];
[observer.KVOController observe:person
keyPath:@"fristNmae"
options:NSKeyValueObservingOptionNew
block:block];
person.firstName = @"西瓜冰";
把firstName写错成了fristNmae,因为属性不存在,所以当属性改变时没有发生通知。使用宏FBKVOKeyPath
后,我们可以跟调用对象属性那样将需要监听的属性传入,例如:
[observer.KVOController observe:person
keyPath:FBKVOKeyPath(person.firstName)
options:NSKeyValueObservingOptionNew
block:block];
因为有自动补全功能,所以一般不会写错,即使写错了,也会在编译时报错。这个宏的校验步骤拆解后如下:
// 1 校验传入的KeyPath是否有编译错误
((void)(NO && ((void)KEYPATH, NO))
// NO && ... 是为了运行时直接返回NO减少操作, 因为有(void)KEYPATH的存在,所以编译时校验了object.property
// 2 将传入的object.property转换为"property"
{ const char *fbkvokeypath = strchr(#KEYPATH, '.'); NSCAssert(fbkvokeypath, @"Provided key path is invalid."); fbkvokeypath + 1; }
// 2.1 #KEYPATH将object.property转为字符串"object.property"
// 2.2 strchr截取".property"
// 2.3 NSCAssert保证点语法的存在
// 2.4 ".property"+1="property"
// 3 使用@()语法糖将char *转换为NSString类型
@(((void)NO, "property"))
// 因为','操作符是返回后面的值,即string = (@"a", @"b");string的值为@"b"
宏FBKVOClassKeyPath也以差不多的形式实现,就不重复了。
FBKVOController.m
FBKVOController
的实现文件里面包含了两个重要的私有类_FBKVOInfo
和_FBKVOSharedController
。KVOController的全部功能就由这三个类来共同完成,这三个类的职责分别是:
_FBKVOInfo
: 用来对每个被观察的keyPath及对应的options和回调(block或者SEL)进行了存储。
FBKVOController
: 将每个被观察对象作为key值,将保存着该对象被观察的keyPath及其对应的回调的_FBKVOInfo
的Set集合作为value值,通过一个NSMapTable
进行存储。在添加观察和移除观察操作时,操作这个NSMapTable
,并且交付_FBKVOSharedController
进行真正的KVO操作。
_FBKVOSharedController
: 一个单例。所有的Cocoa的KVO事件都发生在这个单例对象里,这个对象是真正的观察者。每次被监听对象的相关keyPath发生改变时,将会通知这个单例对象,再由这个单例对象通过_FBKVOInfo
保存的信息来进行回调。
这三个对象的关系大概如上所述,接下来看看具体的代码实现,首先是最基础的_FBKVOInfo
:
属性
@implementation _FBKVOInfo
{
@public
__weak FBKVOController *_controller;
NSString *_keyPath;
NSKeyValueObservingOptions _options;
SEL _action;
void *_context;
FBKVONotificationBlock _block;
/** 标志的keyPath状态,分别为_FBKVOInfoStateInitial、_FBKVOInfoStateObserving、_FBKVOInfoStateNotObserving */
_FBKVOInfoState _state;
}
_FBKVOInfo
类的内容大概就是由上面这些属性,以及一系列初始化这些属性的初始化方法构成。此外还重写了hash
方法和isEqual
,如下:
- (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];
}
因为一个被观察对象的keyPath具有唯一性,为了防止对同一个对象重复添加了监听,所以_FBKVOInfo
的唯一性由keyPath决定。
接下来是FBKVOController
类,除了公开的属性外还有以下私有属性:
私有属性
/** 以被观察者对象为key,以_FBKVOInfo的Set作为value,来对其进行关联和保存 */
NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;
/** 用于保证NSMapTable线程安全的锁 */
pthread_mutex_t _lock;
这里使用NSMutableSet
来保存_FBKVOInfo
是为了防止重复监听同一个被观察对象的同一个keyPath。NSMutableSet
的唯一性是通过调用_FBKVOInfo
的hash
方法和isEqual
方法来确定的,就如上面所述,KVOController已经重写了_FBKVOInfo
的hash
方法和isEqual
方法来保证keyPath的唯一性。
使用NSMapTable
而不是使用NSMutableDictionary
则是因为NSMapTable
能控制对key和value的内存管理方式。
初始化方法
- (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;
}
初始化方法里,保存了观察者对象,根据传入的retainObserved设置NSMapTable
管理内存的方式,初始化了锁。
接下来,以最方便的block方式回调为例,看一下KVOController的完整通知流程。
FBKVOController的方法
// 外部API接口,添加观察的方法
- (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;
}
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
[self _observe:object info:info];
}
// 尝试从NSMapTable中取出已保存的_FBKVOInfo对象,有则返回,无则新增,并用锁保证了存取过程的安全。
- (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];
}
该方法最后通过_FBKVOSharedController
类的方法来添加真正的KVO监听。因为_FBKVOSharedController
是个单例,所以第一次调用+sharedController
会进行初始化,所以先看下_FBKVOSharedController
的属性和初始化,如下:
属性
/** 使用NSHashTable来以弱引用的方式持有_FBKVOInfo */
NSHashTable<_FBKVOInfo *> *_infos;
/** 用于保证NSHashTable线程安全的锁 */
pthread_mutex_t _mutex;
初始化方法
- (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;
}
跟FBKVOController
差不多的初始化方法,没啥好说的。
_FBKVOSharedController的添加观察方法
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
if (nil == info) {
return;
}
pthread_mutex_lock(&_mutex);
[_infos addObject:info];
pthread_mutex_unlock(&_mutex);
// 真正使用Cocoa的KVO添加观察,以info作为context参数
[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
if (info->_state == _FBKVOInfoStateInitial) {
info->_state = _FBKVOInfoStateObserving;
} else if (info->_state == _FBKVOInfoStateNotObserving) {
// 当NSKeyValueObservingOptions属性中包含NSKeyValueObservingOptionInitial,
// 并且在回调中取消了监听(调用unobserve方法)可能因为没有移除监听导致出现安全问题。
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
}
_FBKVOSharedController的KVO监听方法
// KVO监听方法
- (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;
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
将接收到的更改信息重新封装后转发给_FBKVOInfo
保存的观察者的对象。
从上面整个流程中,我们可以看到,观察者对象并没有真正地对被观察者对象进行任何监听,而是通过一个专门负责观察监听和转发信息的单例类来完成监听和发送通知。这样做的好处是为防止发送通知时观察者对象未移除监听并且已经不存在而导致应用Crash的情况提供了双层保障。因为该单例的在APP整个生命周期内都存在,所以最多是接收到信息并不进行其他操作。最后,我们再看下KVOController怎么取消观察:
FBKVOController取消观察方法
- (void)unobserve:(nullable id)object keyPath:(NSString *)keyPath
{
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath];
[self _unobserve:object info:info];
}
- (void)_unobserve:(id)object info:(_FBKVOInfo *)info
{
pthread_mutex_lock(&_lock);
NSMutableSet *infos = [_objectInfosMap objectForKey:object];
_FBKVOInfo *registeredInfo = [infos member:info];
if (nil != registeredInfo) {
[infos removeObject:registeredInfo];
if (0 == infos.count) {
[_objectInfosMap removeObjectForKey:object];
}
}
pthread_mutex_unlock(&_lock);
[[_FBKVOSharedController sharedController] unobserve:object info:registeredInfo];
}
差不多就是添加观察的逆过程。
_FBKVOSharedController取消观察方法
- (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;
}
- (void)unobserve:(id)object infos:(nullable NSSet<_FBKVOInfo *> *)infos
{
if (0 == infos.count) {
return;
}
pthread_mutex_lock(&_mutex);
for (_FBKVOInfo *info in infos) {
[_infos removeObject:info];
}
pthread_mutex_unlock(&_mutex);
for (_FBKVOInfo *info in infos) {
if (info->_state == _FBKVOInfoStateObserving) {
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
info->_state = _FBKVOInfoStateNotObserving;
}
}
这里取消对多个_FBKVOInfo
的观察的-unobserve:infos:
方法,不是遍历着调用-unobserve:info:
,而是使用以上代码实现是为了减少互斥锁切换消耗的时间。
FBKVOController的dealloc方法
- (void)dealloc
{
[self unobserveAll];
pthread_mutex_destroy(&_lock);
}
在FBKVOController
对象销毁时会移除取消所有观察。
NSObject+FBKVOController
分类NSObject+FBKVOController
进一步简化了我们的使用,这个分类提供了两个属性:
@property (nonatomic, strong) FBKVOController *KVOController;
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;
这两属性都是懒加载的形式创建,区别在于是否强引用被观察对象。
- (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);
}
通过动态绑定的方式保存这两属性。
Reference
https://github.com/facebook/KVOController
本文作者:西瓜冰soso
本文链接:https://www.jianshu.com/p/8deccb9c8398
温馨提示:
由于本文是原创文章,可能会有更新以及修正一些错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导。另外文章如有错误,请不吝指教,谢谢。