关于KVO的原理,已经有许多文章来叙述,然而说原理的文章不少,但是就是没有找到一篇KVO实际使用时的。
本文旨在描述了KVO在实际使用中遇到的种种问题。git
KVO是Objective-C对观察者模式的一种实现,指定一个被观察对象,当对象的某个属性发生更改时,观察者会得到通知的一种机制。 原生的系统api使用大概以下:github
[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"];
}
复制代码
然而,在使用原生的代码的时候,有好几处不方便的地方:api
不得不说,KVO这套系统提供的API,实在是太不方便,特别是第三条和第四条,每每还会是个偶发的bug,发现时仍是线上bug。bash
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 也采用了和YYAddForKVO相似的方法,把添加观察者时的keyPath,block都统统打包成一个_FBKVOInfo,而后把_FBKVOSharedController设置成真正的观察者。
当回调到来时,再取出保存的block回调给外界。
由于FBKVOController经过一个hashMap来保存了本身添加的观察者信息,因此当属性dealloc时,关联对象FBKVOController会先一步dealloc,在这dealloc方法里面,取出MapTable便可实现自动移除观察者。post
看起来FBKVOController 很是好的实现了全部KVO中的痛点,然而在其余场景下,却带来了更多的坑
在上述的举例中,都是A类强持有B类,A类来观察B类的变化。在这个场景下,YYKit和KVOController表现的都很不错。咱们把这个场景叫场景0吧。
然而在实际使用中,还存在这2个不一样的场景:ui
@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,那么就构成了一个很明显的循环引用atom
@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个问题:spa
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];
}
}
复制代码
@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的。
有一个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];
}
复制代码
@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,造成了循环引用
@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
总共有三种不一样的写法
@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未能移除的崩溃
在此场景下,由于Model须要监听的对象是弱引用的,因此添加了KVO后,难以找到合适释放的时机。不管哪一个框架,在dealloc方法里面解除都会发生崩溃
不过也不是说这样的场景就没法使用KVO了,笔者仍是研究出了2个能解决的办法: