前言
关于KVO的原理,已经有许多文章来叙述,然而说原理的文章很多,可是就是没有找到一篇KVO实际使用时的。
本文旨在描述了KVO在实际使用中遇到的种种问题。
简单介绍下KVO
KVO是Objective-C对观察者模式的一种实现,指定一个被观察对象,当对象的某个属性发生更改时,观察者会获得通知的一种机制。 原生的系统api使用大概如下:
- 注册观察者
[self.model addObserver:self forKeyPath:@"value" options:NSKeyValueObservingOptionNew context:nil];
- 添加监听回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:@"value"]) {
NSLog(@"new value = %@", change[NSKeyValueChangeNewKey]);
}
}
- 移除观察者
- (void)dealloc
{
[self.model removeObserver:self forKeyPath:@"value"];
}
缺陷
然而,在使用原生的代码的时候,有好几处不方便的地方:
- 回调统一在了一个方法里面,不利于代码的拆分
- 当父类和子类都监听了对一个属性添加KVO时,会收到多次回调,只能通过context来进行区分
- 没有什么API能标示自己添加了几次KVO了,如果KVO的添加时,是有判断条件的,那么在dealloc的时候就会遇到难以判断KVO有没有被添加的麻烦,如果没有添加,移除KVO是会崩溃的
- 如果KVO被添加上了,对象dealloc时未被移除也会崩溃
不得不说,KVO这套系统提供的API,实在是太不方便,特别是第三条和第四条,往往还会是个偶发的bug,发现时还是线上bug。
应当如何改进呢
- KVO的回调放在Block里面做回调,每个keyPath,每次添加观察者,都对应一个新的Block。
- observer的对象自己来来保存住自己添加的KVO次数,观察者,keypath等信息,需要移除的时候,直接拿出保存的信息来做移除
- 当对象dealloc时,能够自动移除观察者信息
第三方框架
YYKit+YYAddForKVO
YYKit+YYAddForKVO 里面就提供了一个YYNSObjectKVOBlockTarget的私有类来充当真正的观察者,每当我们调用- (void)addObserverBlockForKeyPath:(NSString *)keyPath block:(void (^)(__weak id obj, id oldVal, id newVal))block 时,都把keyPath,block都通通打包到一个字典里面,然后把YYNSObjectKVOBlockTarget设置成真正的观察者。
Target收到回调后,在从字典里面,找到观察者的block,进行回调。
每当我们调用removeObserverBlocks时,再获取到之前存储的观察者信息字典,遍历观察的key挨个的调用移除。
这套框架很好的解决了上述问题的1,2,3,唯独对4没有什么好的解决办法
KVOController
KVOController 也采用了和YYAddForKVO类似的方法,把添加观察者时的keyPath,block都通通打包成一个_FBKVOInfo,然后把_FBKVOSharedController设置成真正的观察者。
当回调到来时,再取出保存的block回调给外界。
因为FBKVOController通过一个hashMap来保存了自己添加的观察者信息,所以当属性dealloc时,关联对象FBKVOController会先一步dealloc,在这dealloc方法里面,取出MapTable即可实现自动移除观察者。
KVOController的坑
看起来FBKVOController 非常好的实现了所有KVO中的痛点,然而在其他场景下,却带来了更多的坑
在上述的举例中,都是A类强持有B类,A类来观察B类的变化。在这个场景下,YYKit和KVOController表现的都很不错。我们把这个场景叫场景0吧。
然而在实际使用中,还存在这2个不同的场景:
- A类观察自己的某个属性变化(场景1)
- A类强持有B类,B类需要观察A类的属性变化(场景2)
场景1(A类观察自己的某个属性变化)
使用了KVOController,代码如下:
@implementation Model
- (instancetype)init
{
self = [super init];
if (self) {
__weak typeof(self) weakSelf = self;
// 添加一个定时器来改变属性
[NSTimer scheduledTimerWithTimeInterval:1.0 block:^(NSTimer * _Nonnull timer) {
weakSelf.value ++;
} repeats:YES];
// 添加观察者
[self.KVOController observe:self keyPath:@"value" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
NSLog(@"new value = %@ %@", change[NSKeyValueChangeNewKey], @(weakSelf.value));
}];
}
return self;
}
- (void)dealloc
{
NSLog(@"dealloc %@", self);
}
@end
观察到属性变化时没有问题,但是,然后释放掉当前对象就会发现,dealloc没有走。
这个问题在GitHub上面也有人提出来https://github.com/facebook/KVOController/pull/131。
self对KVOController是强持有的,而KVOController需要实现自动解除观察者。强持有了observe中存入的参数,本例中也是self,那么就构成了一个很明显的循环引用
使用KVOControllerNonRetaining,代码如下:
@implementation Model
- (instancetype)init
{
self = [super init];
if (self) {
__weak typeof(self) weakSelf = self;
[NSTimer scheduledTimerWithTimeInterval:1.0 block:^(NSTimer * _Nonnull timer) {
weakSelf.value ++;
} repeats:YES];
[self.KVOControllerNonRetaining observe:self keyPath:@"value" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
NSLog(@"new value = %@ %@", change[NSKeyValueChangeNewKey], @(weakSelf.value));
}];
}
return self;
}
- (void)dealloc
{
[self.KVOControllerNonRetaining unobserveAll];
NSLog(@"dealloc %@", self);
}
@end
这样使用会有2个问题:
- 虽然KVOControllerNonRetaining能够对observe中存入的参数弱引用来打破循环引用,但是自动解除观察者这个特性却变得无法实现。因为KVOController的MapTable弱引用observe,而弱引用的指针,会在dealloc方法走到时,已经变成nil。
- 即便我们在dealloc方法里面,使用[self.KVOControllerNonRetaining unobserveAll]; 依旧会崩溃,因为 unobserveAll也是去MapTable寻找保存的信息来做移除,弱引用的指针已经被释放,所以无法移除任何KVO
unobserveAll方法的源码:
- (void)_unobserveAll
{
// lock
pthread_mutex_lock(&_lock);
NSMapTable *objectInfoMaps = [_objectInfosMap copy];
// clear table and map
[_objectInfosMap removeAllObjects];
// unlock
pthread_mutex_unlock(&_lock);
_FBKVOSharedController *shareController = [_FBKVOSharedController sharedController];
for (id object in objectInfoMaps) {
// unobserve each registered object and infos
NSSet *infos = [objectInfoMaps objectForKey:object];
[shareController unobserve:object infos:infos];
}
}
使用YYAddForKVO,代码如下:
@implementation Model
- (instancetype)init
{
self = [super init];
if (self) {
__weak typeof(self) weakSelf = self;
[NSTimer scheduledTimerWithTimeInterval:1.0 block:^(NSTimer * _Nonnull timer) {
weakSelf.value ++;
} repeats:YES];
[self addObserverBlockForKeyPath:@"value" block:^(id _Nonnull obj, id _Nullable oldVal, id _Nullable newVal) {
NSLog(@"new value = %@", newVal);
}];
}
return self;
}
- (void)dealloc
{
[self removeObserverBlocks];
NSLog(@"dealloc %@", self);
}
@end
YYKit的框架,虽然没有自动解除KVO这个特性,但是代码确实可以正常work不crash的。
场景1结论
- 在自己观察自己这个场景下,KVOController除非我们能在对象dealloc前,找到实际移除KVO,否则这个框架完全无法使用
- YYAddForKVO可以正常使用
场景2
有一个ViewController对象持有了一个model,model通过弱应用引用了ViewController, model需要添加对value的监听,代码如下:
@interface ViewController ()
@property (nonatomic, assign) NSInteger value;
@property (nonatomic, strong) Model *model;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
[NSTimer scheduledTimerWithTimeInterval:1.0 block:^(NSTimer * _Nonnull timer) {
weakSelf.value ++;
} repeats:YES];
self.model = [[Model alloc] init];
self.model.vc = self;
[self.model startObserver];
}
使用KVOController
@implementation Model
- (void)startObserver
{
[self.KVOController observe:self.vc keyPath:@"value" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
NSLog(@"new value = %@", change[NSKeyValueChangeNewKey]);
}];
}
- (void)dealloc
{
NSLog(@"dealloc %@", self);
}
@end
结果依旧是发生了循环引用,self强持有KVOController,KVOController强持有self.vc(ViewController),ViewController强持有了Model,形成了循环引用
使用KVOControllerNonRetaining,代码如下:
@implementation Model
- (void)startObserver
{
[self.KVOControllerNonRetaining observe:self.vc keyPath:@"value" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
NSLog(@"new value = %@", change[NSKeyValueChangeNewKey]);
}];
}
- (void)dealloc
{
[self.KVOControllerNonRetaining unobserveAll];
NSLog(@"dealloc %@", self);
}
@end
结果发生了崩溃,原因同场景1一样,unobserveAll方法没能移除任何KVO
使用YYKit+YYAddForKVO,代码如下:
总共有三种不同的写法
@implementation Model
- (void)startObserver
{
[self addObserverBlockForKeyPath:@"vc.value" block:^(id _Nonnull obj, id _Nullable oldVal, id _Nullable newVal) {
NSLog(@"new value = %@", newVal);
}];
}
- (void)dealloc
{
[self removeObserverBlocks];
NSLog(@"dealloc %@", self);
}
@end
结果是崩溃。KVO未能及时移除
@implementation Model
- (void)startObserver
{
[self.vc addObserverBlockForKeyPath:@"value" block:^(id _Nonnull obj, id _Nullable oldVal, id _Nullable newVal) {
NSLog(@"new value = %@", newVal);
}];
}
- (void)dealloc
{
[self.vc removeObserverBlocks];
NSLog(@"dealloc %@", self);
}
@end
结果是崩溃。dealloc时,已经获取不到self.vc了。 KVO未能移除
@implementation Model
- (void)startObserve
{
[self.vc addObserverBlockForKeyPath:@"value" block:^(id _Nonnull obj, id _Nullable oldVal, id _Nullable newVal) {
NSLog(@"new value = %@", newVal);
}];
}
@end
@implementation SecondViewController
- (void)dealloc
{
[self removeObserverBlocks];
NSLog(@"dealloc %@", self);
}
@end
这样也会崩溃。真正的观察者,是Model类的关联对象,dealloc时,关联对象会先一步释放,所以还是会发生KVO未能移除的崩溃
场景2结论
在此场景下,因为Model需要监听的对象是弱引用的,所以添加了KVO后,难以找到合适释放的时机。无论哪个框架,在dealloc方法里面解除都会发生崩溃
不过也不是说这样的场景就无法使用KVO了,笔者还是研究出了2个能解决的办法:
- A类持有B类,B类需要监听A类的属性变化,可以实现为变成A类监听自己的变化,然后把变化直接调用方法传给B类,这样场景2其实就变成了场景1的情况了,这样YYKit的库,就能很好的实现需求
- 假如能找到A类的持有者(假设叫C对象),在C对象的dealloc方法里面,或者C手动释放B对象之前,调用A的remoObserver,也是可以实现不发生任何崩溃的。
结论
- 对于KVO使用的三种不同场景,KVOController的适用场景非常差。仅仅只有场景0能够很好的实现他说提供的各种功能,所以笔者认为不该使用此框架
- KVOController为了实现,自动移除KVO这特性,采取的办法非常不好,带来了许许多多的问题,这个实现思路肯定是不对的。
- 而在网上,也有一些使用了hook dealloc方法之类的来实现自动解除KVO,笔者也认为这非常不好,滥用runtime会有很多隐患,还是老老实实的去写一个removeObserver吧
- 对于YYAddForKVO,他在三个场景下的表现和原生代码一致,通过一点点小处理,能够三个场景下都适用。而且API的优雅性非常好,是原生KVO的一个很好的替代品