如何安全的使用KVO

前言

关于KVO的原理,已经有许多文章来叙述,然而说原理的文章不少,但是就是没有找到一篇KVO实际使用时的。
本文旨在描述了KVO在实际使用中遇到的种种问题。git

简单介绍下KVO

KVO是Objective-C对观察者模式的一种实现,指定一个被观察对象,当对象的某个属性发生更改时,观察者会得到通知的一种机制。 原生的系统api使用大概以下:github

  1. 注册观察者
[self.model addObserver:self forKeyPath:@"value" options:NSKeyValueObservingOptionNew context:nil];
复制代码
  1. 添加监听回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"value"]) {
        NSLog(@"new value = %@", change[NSKeyValueChangeNewKey]);
    }
}
复制代码
  1. 移除观察者
- (void)dealloc
{
    [self.model removeObserver:self forKeyPath:@"value"];
}
复制代码

缺陷

然而,在使用原生的代码的时候,有好几处不方便的地方:api

  • 回调统一在了一个方法里面,不利于代码的拆分
  • 当父类和子类都监听了对一个属性添加KVO时,会收到屡次回调,只能经过context来进行区分
  • 没有什么API能标示本身添加了几回KVO了,若是KVO的添加时,是有判断条件的,那么在dealloc的时候就会遇到难以判断KVO有没有被添加的麻烦,若是没有添加,移除KVO是会崩溃的
  • 若是KVO被添加上了,对象dealloc时未被移除也会崩溃

不得不说,KVO这套系统提供的API,实在是太不方便,特别是第三条和第四条,每每还会是个偶发的bug,发现时仍是线上bug。bash

应当如何改进呢

  1. KVO的回调放在Block里面作回调,每一个keyPath,每次添加观察者,都对应一个新的Block。
  2. observer的对象本身来来保存住本身添加的KVO次数,观察者,keypath等信息,须要移除的时候,直接拿出保存的信息来作移除
  3. 当对象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便可实现自动移除观察者。post

KVOController的坑

看起来FBKVOController 很是好的实现了全部KVO中的痛点,然而在其余场景下,却带来了更多的坑
在上述的举例中,都是A类强持有B类,A类来观察B类的变化。在这个场景下,YYKit和KVOController表现的都很不错。咱们把这个场景叫场景0吧。
然而在实际使用中,还存在这2个不一样的场景:ui

  • 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,那么就构成了一个很明显的循环引用atom

使用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个问题:spa

  1. 虽然KVOControllerNonRetaining可以对observe中存入的参数弱引用来打破循环引用,可是自动解除观察者这个特性却变得没法实现。由于KVOController的MapTable弱引用observe,而弱引用的指针,会在dealloc方法走到时,已经变成nil。
  2. 即使咱们在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的一个很好的替代品
相关文章
相关标签/搜索