如何安全的使用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的一個很好的替代品
相關文章
相關標籤/搜索