源码地址: KVOController
系统的KVO功能很强大, 可以在完全无耦合的情况下, 在被观察者的值变化时触发回调, 但是也是有诸多的限制,很容易出现crash, 比如必须在dealloc中取消观察者,不取消注册就会crash, 注册观察者和取消观察者必须成对出现, 多调和少调就会crash. 场景简单的情况下一般不会有问题, 但是在复杂场景下, 多调和少调就很容易出现crash.
我们在一个比较复杂的场景中使用了观察者,
- 观察TableView的contentOffset属性, 同时有一个变量currScrollView记录当前显示的tableView,
- 一个页面有4个tableView, 同一时间只注册一个tableView监听
- 当发生从tableViewA切换到tableVIewB的时候, 需要取消上一个tableViewA的观察, 注册tableViewB的观察,
- 在dealloc时取消currScrollView的观察
从逻辑上并无漏洞, 在开发过程中一直正常, 上线后也无人反馈出现crash, 但是bugly总有类似的crash , 这4个是完全独立的crash, 并没有把他们联系到一起, 但是都是同一个原因引起的.少调了tableView的取消观察者.
- -[UIView dealloc],
- -[UIViewController dealloc],
- -[UITableView .cxx_destruct],
- An instance 0x184abc00 of class ULTableView was deallocated while key value observers were still registered with it. Current observation info: <NSKeyValueObservationInfo 0x9ca2680> ( <NSKeyValueObservance 0x45bbf290: Observer: 0x193fb550, Key path: contentOffset, Options: <New: YES, Old: YES, Prior: NO> Context: 0x0, Property: 0x9c289f0> )
crash的原因是,有极低的概率出现, 从tableView首次从A切换到B时, 获取的BTableView为nil, 此时ATableView的观察者没有消除,currScrollView也被置为nil, 在dealloc时,currScrollView为nil无法取消观察者, 就会出现crash, 系统的这个crash名字超长 ,翻译过来是: KVO将保留该对象的所有观察者,如果该对象崩溃,则观察者被过度释放, 标准的野指针错误.
KVO_IS_RETAINING_ALL_OBSERVERS_OF_THIS_OBJECT_IF_IT_CRASHES_AN_OBSERVER_WAS_OVERRELEASED_OR_SMASHE
在写demo尝试复现的时候,采用切换Window.rootViewController来触发vc的dealloc, 但是这种情况下竟然不会crash, 必须是NavigationController的pop操作才会100%复现crash, 这个让人很迷, 难道和触发dealloc的路径还有关系?? 尝试查看原因发现他们的dealloc堆栈并无差异, dealloc的执行顺序也是一致的, 真是百思不得其解.
demo在这里, 有兴趣的可以试试, 地址 : https://github.com/guochaoshun/KVODeallockCrash
继续测试还发现某些类的某些属性不移除监听也不会crash, 比如监听下面4个属性, 在dealloc中不调用移除操作也不会crash, 可能系统有补救策略.
- self.currScrollView.contentSize, 系统类的结构体属性
- self.view.backgroundColor, 系统类的对象属性
- self.currScrollView.frame 系统类的结构体属性
- self.person.name 自建类的对象属性
而self.currScrollView.contentOffset 在dealloc中不调用取消观察者, 则必定会crash, 有点搞不懂系统的机制是什么? 什么情况有能补救? 搜索资料也未找到系统提供的文档.
作为开发者, 能做的就是保持注册和取消成对出现, 不要过于依赖系统的补救.
知道了原因, 也进行了修改, 当获取到BTableView为nil时, ATableView保证能正确触发取消观察者, 改完上线后, 此crash不在出现, 复盘的时候在想有没有一种方案, 可以自己管理观察者的注册和取消, 降低KVO使用者出错的概率. 于是找到了FBKVOController.
系统标准KVO的使用
- (void)viewDidLoad {
[super viewDidLoad];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
}
// 某个方法修改了self.person的name属性
...
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (object == self.person && [keyPath isEqualToString:@"name"]) {
NSLog(@"change: %@",change);
NSLog(@"旧值是:%@", change[NSKeyValueChangeOldKey]);
NSLog(@"新值是:%@", change[NSKeyValueChangeNewKey]);
self.secondLabel.text = change[NSKeyValueChangeNewKey];
}
}
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"name"];
NSLog(@"%s",__func__);
}
Apple原生KVO也有一些显而易见的缺点。
- 添加和移除观察者要配对出现。
移除一个未添加的观察者,程序会crash;
重复添加观察者会造成回调方法多次调用,给程序造成逻辑上的混乱。 - 添加观察者,移除观察者,通知回调,三块儿代码过于分散。
- 观察对象很多时, observeValueForKeyPath: 太多if判断, 看起来比较乱
FBKVOController 做了什么?
简单来说,Facebook
开源的这套代码, 很少, 只有2个类+1个类别,主要是对我们经常使用的 KVO
机制进行了额外的一层封装。其中最亮眼的特色是提供了一个 block 回调让我们进行处理,避免 KVO
的相关代码四处散落,不再需要使用下面这个方法:
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
使用姿势
利用开源框架,我们这样实现,其中第二种方法可以用一行代码实现 KVO
:
#import "ViewController.h"
#import "FBKVOController.h"
#import "NSObject+FBKVOController.h"
@interface KVOModel : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation KVOModel
@end
NS_ASSUME_NONNULL_BEGIN
@interface ViewController ()
@property (nonatomic, strong) KVOModel *model;
@property (nonatomic, strong) FBKVOController *kvoController;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//创建被观察的 model 类
KVOModel *model = [[KVOModel alloc] init];
//初始化设置 model 的成员变量值
model.name = @"wo";
model.age = 5;
self.model = model;
//第一种方法:创建 FBKVOController 对象,并被 VC 强引用,否则出了当前作用域,就会被销毁
FBKVOController *kvoController = [[FBKVOController alloc] initWithObserver:self];
_kvoController = kvoController;
//添加 观察
[kvoController observe:model keyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
NSLog(@"我的旧名字是:%@", change[NSKeyValueChangeOldKey]);
NSLog(@"我的新名字是:%@", change[NSKeyValueChangeNewKey]);
}];
//第二种方法:无需主动创建 FBKVOController 对象,self.KVOController 直接懒加载创建FBKVOController 对象
//可以直接对某个对象的多个成员变量执行 KVO
//------真正实现一行代码搞定 KVO------
[self.KVOController observe:model keyPaths:@[@"name", @"age"] options: NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew block:^(id observer, id object, NSDictionary *change) {
NSString *changedKeyPath = change[FBKVONotificationKeyPathKey];
if ([changedKeyPath isEqualToString:@"name"]) {
NSLog(@"修改了名字");
} else if ([changedKeyPath isEqualToString:@"age"]) {
NSLog(@"修改了年龄");
}
NSLog(@"旧值是:%@", change[NSKeyValueChangeOldKey]);
NSLog(@"新值是:%@", change[NSKeyValueChangeNewKey]);
}];
//修改 model 的 name 成员变量
model.name = @"ni";
}
@end
NS_ASSUME_NONNULL_END
相比于原生 API 优势:
- 1 可以以
数组形式
,同时对model
的多个 不同成员变量进行KVO
。 - 2 利用提供的
block
,将KVO
相关代码集中在一块,而不是四处散落。比较清晰,一目了然。 - 3 不需要在
dealloc
方法里取消对 object 的观察,当FBKVOController
对象dealloc
,会自动取消观察。
源码解析
这套源代码主要包括了FBKVOController.h
、FBKVOController.m
、NSObject+FBKVOController.h
、NSObject+FBKVOController.m
四个文件。
其中,NSObject+FBKVOController
这个分类比较简单。它主要干的事是通过 objc_setAssociatedObject
(关联对象),以懒加载的形式给 NSObject
,创建并关联一个 FBKVOController
的对象。
接下来,我会着重介绍一下今天的主角 FBKVOController
类。其文件中还包含另外两个类,_FBKVOInfo
、_FBKVOSharedController
。下面都会介绍到。
先来看看 FBKVOController
指定初始化函数:
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
self = [super init];
if (nil != self) {
//一般情况下 observer 会持有 FBKVOController 为了避免循环引用,此处的_observer 的内存管理语义是弱引用
_observer = observer;
//定义 NSMapTable key的内存管理策略,在默认情况,传入的参数 retainObserved = YES
NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
//创建 NSMapTable key 为 id 类型,value 为 NSMutableSet<_FBKVOInfo *> 类型
_objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
//初始化互斥锁,避免多线程间的数据竞争
pthread_mutex_init(&_lock, NULL);
}
return self;
}
以上初始化代码中,注释都写得比较清楚了。唯一比较陌生的是 NSMapTable
。简单来说,它与 NSDictionary
类似。不同之处是 NSMapTable
可以自主控制 key
/ value
的内存管理策略。而 NSDictionary
的内存策略是固定为 copy
。当 key 为 object
时, copy
的开销可能比较大!因此,在这里只能使用相对比较灵活的 NSMapTable
。
执行 KVO
的相关方法代码解析
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
//当 keyPath 字符串长度为 0 或者 block 为空时,会产生断言,程序会 crash
NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
//如果 “被观察对象” 为 nil,同样会直接返回
if (nil == object || 0 == keyPath.length || NULL == block) {
return;
}
// create info _FBKVOInfo
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
// observe object with info (利用存储的信息对 “被观察对象” 进行观察!)
[self _observe:object info:info];
}
上述代码中主要干了2件事情,
- 对于传入的参数,构建一个内部的FBKVOInfo数据结构,其存储的信息包括了
FBKVOController
、keypath
、options
、block
。 - 调用[self _observe:object info:info];
接上段代码的最后一句 [self _observe:object info:info];
- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
// lock 互斥锁加锁
pthread_mutex_lock(&_lock);
//还记得初始化 FBKVOController 时创建的 NSMapTable 么?
//其结构是以 被观察者 object 为 key。并不像我们常用的 NSDictionary 那样是以 NSString 为 key
NSMutableSet *infos = [_objectInfosMap objectForKey:object];
// check for info existence
// 必须重写 _FBKVOInfo hash 以及 isEqual 方法,这样才能使用 NSSet 的 member 方法。
_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;
}
//如果没有 关于这个 object(被观察者)的相关信息,则创建 NSMutableSet,并添加到 NSMapTable 中
// lazilly create set of infos
if (nil == infos) {
infos = [NSMutableSet set];
[_objectInfosMap setObject:infos forKey:object];
}
// add info and oberve -- NSMutableSet 加 info
[infos addObject:info];
// unlock prior to callout
pthread_mutex_unlock(&_lock);
//sharedController 是 干嘛的? 将所有观察信息统一交由一个单例来完成
[[_FBKVOSharedController sharedController] observe:object info:info];
}
-
根据被观察的object获取其对应的infos 集合。这个主要作用在于避免多次对同一个keyPath添加多次观察。因为每调用一次addObserverForKeyPath就要有一个对应的removeObserverForKey。
-
从infos 集合判断是不是已经有了与此次info相同的观察(重写了_FBKVOInfo的isEqual来协助判断集合中的重复元素)。
如果集合中已有相同的keypath,解锁,返回;
如果没有,进行第3步. -
如果以上都顺利通过,将观察的信息及关系注册到_FBKVOSharedController中。
总结一下上面一段的数据结构。FBKVOController
拥有成员变量 NSMapTable
,NSMapTable
以被观察者
(object)为 key, NSMutableSet
为 value 。在 NSMutableSet
中,存储了不同 info
。其关系图如下图:
追踪一下这句代码
[[_FBKVOSharedController sharedController] observe:object info:info];
_FBKVOSharedController 是一个单例, 是真正观察的类。其职责是:负责将FBKVOController发送过来的信息转发给系统的KVO处理, 当系统的KVO发出回调时, 在根据KVOInfo 调用Block或者selector。因此 app 当中所有 KVO
的通知都是由这个单例来完成的。
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
if (nil == info) {
return;
}
// register info 向 NSHashTable 添加 info
//注意:在 _FBKVOController 类中的 NSMutableSet 已经强引用了 info
//这里是为了弱引用 info,才使用 NSHashTable,当 info dealloc 时,同时会从容器中删除
pthread_mutex_lock(&_mutex);
[_infos addObject:info];
pthread_mutex_unlock(&_mutex);
//_FBKVOSharedController 是实际的观察者! 随后会进行转发 ,
//context 是 void * 无类型指针,是 info 的指针!
[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
//如果 state 是原始状态,则改为正在观察的状态,表明是在正在观察的状态
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进行接受,随后进行转发。
- 对应的添加代码 有一个 移除代码,设计的相当细心啊
以上代码中想单独说一下下面的代码,其中的 context
参数使用的是 (void *)info
的指针,这样可以保证 context
的唯一性。
接收 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
// 利用 context 查找 info,其中用到了 void * 转换为 id 型变量 (__bridge id)
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];
//字典合并,并重新拷贝一份,
//包含信息有:1、改变了哪个值 mChange 2、 原先的 change 字典
[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 {
// 没有传block或者selector, 调用观察者的原生函数!!
[observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
}
}
}
}
}
- 根据context上下文获取对应的KVOInfo
- 判断当前info的observer和controller,是否仍然存在(因为之前我们采用的weak持有)
- 根据 info的block或者selector或者overwrite进行消息转发。
设计思路总结
- 1
FBKVOController
持有NSMapTable
,以object
为key
得到相对应的NSMutableSet
。NSMutableSet
中存储了不同的_FBKVOInfo
。这套数据结构的主要作用是防止开发人员重复添加相同的KVO
。当检查到其中已存在相同的_FBKVOInfo
对象时,不再执行后面的代码。 - 2
_FBKVOSharedController
持有NSHashTable
。NSHashTable
以弱引用的方式持有不同的_FBKVOInfo
。此处实际执行KVO
代码。_FBKVOInfo
有一个重要的成员变量_FBKVOInfoState
,根据这个枚举值(_FBKVOInfoStateInitial
、_FBKVOInfoStateObserving
、_FBKVOInfoStateNotObserving
) 来决定新增或者删除KVO
。
收获(通读、研究源代码后)
NSSet
/ NSHashTable
、NSDictionary
/ NSMapTable
的学习
NSSet
是过滤掉重复 object
的集合类,NSHashTable
是 NSSet
的升级版容器,并且只有可变版本,允许对添加到容器中的对象是弱引用的持有关系, 当NSHashTable
中的对象销毁时,该对象也会从容器中移除。
NSMapTable
同 NSDictionary
类似,唯一区别是多了个功能:可以设置 key
和 value
的 NSPointerFunctionsOptions
特性! NSDictionary
的 key
策略固定是 copy
,考虑到开销问题,一般使用简单的数字或者字符串为 key
。但是如果碰到需要用 object
作为 key
的应用场景呢?NSMapTable
就可以派上用场了!可以通过 NSFunctionsPointer
来分别定义对 key
和 value
的内存管理策略,简单可以分为 strong
,weak
以及 copy
。
什么“自释放”?
可以简单的理解为对象在生命周期结束后自动清理回收所有与其相关的资源或链接,这个清理不仅仅包括对象内存的回收,还包括对象解耦以及附属事件的清理等,比如定时器的自我停止、KVO对象的监听移除等。
那么,FBKVOController是如何做到自释放的?可以归纳为四个字——动态属性。其为观察者绑定动态属性self.KVOController,动态绑定的KVOController会随着观察者的释放而释放,KVOController在自己的dealloc函数中移除KVO监听,巧妙的将观察者的remove转移到其动态属性的dealloc函数中。