iOS 如何自動移除KVO觀察者

聲明

有人說這是所謂的黑魔法, 本人在此聲明: 本項目無任何黑魔法, 對原代碼無任何侵害, 只是對註冊方法的封裝.git

問題

咱們都知道, 使用KVO模式, 對某個屬性進行監聽時, Observer 須要在必要的時刻進行移除, 不然 App 必然會 Crash. 這個問題有點煩人, 由於偶爾會忘記寫移除 Observer 的代碼...github

我一直想要這樣一個效果: 只管監聽, 並處理監聽方法. 不去分心, 管什麼時候移除 Observer , 讓其可以適時自動處理.bash

所幸, 它可以實現, 先預覽一下:markdown

@interface NSObject (SJObserverHelper)

- (void)sj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

@interface SJObserverHelper : NSObject
@property (nonatomic, unsafe_unretained) id target;
@property (nonatomic, unsafe_unretained) id observer;
@property (nonatomic, strong) NSString *keyPath;
@property (nonatomic, weak) SJObserverHelper *factor;
@end

@implementation SJObserverHelper
- (void)dealloc {
    if ( _factor ) {
        [_target removeObserver:_observer forKeyPath:_keyPath];
    }
}
@end

@implementation NSObject (ObserverHelper)

- (void)sj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
    
    [self addObserver:observer forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:nil];
    
    SJObserverHelper *helper = [SJObserverHelper new];
    SJObserverHelper *sub = [SJObserverHelper new];
    
    sub.target = helper.target = self;
    sub.observer = helper.observer = observer;
    sub.keyPath = helper.keyPath = keyPath;
    helper.factor = sub;
    sub.factor = helper;
    
    const char *helpeKey = [NSString stringWithFormat:@"%zd", [observer hash]].UTF8String;
    objc_setAssociatedObject(self, helpeKey, helper, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    objc_setAssociatedObject(observer, helpeKey, sub, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

複製代碼

項目源碼框架

下面來講說一步一步的實現吧:工具

初步思路實現

咱們都知道, 對象被釋放以前, 會調用dealloc方法, 其持有的實例變量也會被釋放.oop

我就這樣想, 在監聽註冊時, 爲selfObserver關聯個臨時對象, 當二者在釋放實例變量時, 我藉助這個時機, 在臨時對象的dealloc方法中, 移除Observer就好了.atom

想法很好, 可總不能每一個類裏都加一個臨時對象的屬性吧. 那如何在不改變原有類的狀況下, 爲其關聯一個臨時對象呢?spa

關聯屬性

不改變原有類, 這時候確定是要用Category了, 系統框架裏面有不少的分類, 而且有不少的關聯屬性, 以下圖 UIView 頭文件第180行: 指針

ex.png


依照上圖, 咱們先看一個示例, 爲NSObject的添加一個Category, 並添加了一個property, 在.m中實現了它的settergetter方法.

#import <objc/message.h>

@interface NSObject (Associate)
@property (nonatomic, strong) id tmpObj;
@end
@implementation NSObject (Associate)

static const char *testKey = "TestKey";
- (void)setTmpObj:(id)tmpObj {
    // objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
    objc_setAssociatedObject(self, testKey, tmpObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)tmpObj {
    // objc_getAssociatedObject(id object, const void *key)
    return objc_getAssociatedObject(self, testKey);
}
@end
複製代碼

很明確, objc_setAssociatedObject 即是關聯屬性的setter方法, 而objc_getAssociatedObject即是關聯屬性的getter方法. 最須要關注的就是setter方法, 由於咱們要用來添加關聯屬性對象.

初步思路探索

初步嘗試: 既然屬性能夠隨時使用objc_setAssociatedObject關聯了, 那我就嘗試先爲self關聯一個臨時對象, 在其dealloc中, 將Observer移除.

@interface SJObserverHelper : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic, weak) id observer;
@property (nonatomic, strong) NSString *keyPath;
@end

@implementation SJObserverHelper
- (void)dealloc {
    [_target removeObserver:_observer forKeyPath:_keyPath];
}
@end

- (void)addObserver {
    NSString *keyPath = @"name";
    [_xiaoM addObserver:_observer forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:nil];
    
    SJObserverHelper *helper_obj = [SJObserverHelper new];
    helper_obj.target = _xiaoM;
    helper_obj.observer = _observer;
    helper_obj.keyPath = keyPath;

    const char *helpeKey = [NSString stringWithFormat:@"%zd", [_observer hash]].UTF8String;
    // 關聯
    objc_setAssociatedObject(_xiaoM, helpeKey, helper_obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
複製代碼

因而, 美滋滋的運行了一下程序, 當將_xiaoM 置爲 nil 時, App Crash......

reason: 'An instance 0x12cd1c370 of class Person was deallocated while key value observers were still registered with it. 複製代碼

分析: 臨時對象的dealloc, 確確實實的跑了. 爲何會還有registered? 因而我嘗試在臨時對象dealloc中, 打印實例變量target, 發現其爲nil. 好吧, 這就是Crash問題緣由!


嘗試 unsafe_unretained

經過上面操做, 咱們知道self在被釋放以前, 會先釋放其持有的關聯屬性, 在析構期間, 能夠肯定self仍是存在的, 並未徹底釋放, 可在臨時對象中target卻成了nil. 那如何保持不爲nil呢?

咱們看看OC中的兩個修飾符weakunsafe_unretained:

  • weak: 持有者的實例變量不會對目標進行retain, 當目標銷燬時, 持有者的實例變量會被置空
  • unsafe_unretained: 持有者的實例變量不會對目標進行retain, 當目標釋放後, 持有者的實例變量還會依然指向以前的內存空間(野指針)

由上, unsafe_unretained很好的解決了咱們的問題. 因而我作了以下修改:

@interface SJObserverHelper : NSObject
@property (nonatomic, unsafe_unretained) id target;
@property (nonatomic, unsafe_unretained) id observer;
@property (nonatomic, strong) NSString *keyPath;
@end
複製代碼

再次運行程序, 還行, 觀察者移除了.


最終實現

還存在的問題

目前, 咱們只是實現了, 如何在self釋放的時候, 移除本身身上的Observer. 但若是Observer提早釋放了呢? 而添加關聯屬性, 二者還不能同時持有臨時對象, 不然臨時對象也不會及時的釋放.


好吧, 既然一個不行, 那就各自關聯一個:

- (void)addObserver {
    ..... 
    
    SJObserverHelper *helper_obj = [SJObserverHelper new];
    SJObserverHelper *sub_obj = [SJObserverHelper new];

    sub_obj.target = helper_obj.target = _xiaoM;
    sub_obj.observer = helper_obj.observer = _observer;
    sub_obj.keyPath = helper_obj.keyPath = keyPath;

    const char *helpeKey = [NSString stringWithFormat:@"%zd", [_observer hash]].UTF8String;
    // 關聯
    objc_setAssociatedObject(_xiaoM, helpeKey, helper_obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    // 關聯
    objc_setAssociatedObject(_observer, helpeKey, sub_obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
複製代碼

如上, 仔細想一想, 存在一個很明顯的問題, 兩個關聯屬性釋放的同時, 進行了兩次觀察移除的操做. 爲避免這個問題, 我又作了以下修改:

@interface SJObserverHelper : NSObject
@property (nonatomic, unsafe_unretained) id target;
@property (nonatomic, unsafe_unretained) id observer;
@property (nonatomic, strong) NSString *keyPath;
@property (nonatomic, weak) SJObserverHelper *factor;  // 1. 新增一個 weak 變量
@end

@implementation SJObserverHelper
- (void)dealloc {
    if ( _factor ) {
        [_target removeObserver:_observer forKeyPath:_keyPath];
    }
}
@end

- (void)addObserver {
    ..... 
    
    SJObserverHelper *helper_obj = [SJObserverHelper new];
    SJObserverHelper *sub_obj = [SJObserverHelper new];

    sub_obj.target = helper_obj.target = _xiaoM;
    sub_obj.observer = helper_obj.observer = _observer;
    sub_obj.keyPath = helper_obj.keyPath = keyPath;
    // 2. 互相 weak 引用
    helper_obj.factor = sub_obj;  
    sub_obj.factor = helper_obj;

    const char *helpeKey = [NSString stringWithFormat:@"%zd", [_observer hash]].UTF8String;
    // 關聯
    objc_setAssociatedObject(_xiaoM, helpeKey, helper_obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    // 關聯
    objc_setAssociatedObject(_observer, helpeKey, sub_obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
複製代碼

在以前的操做中, 咱們知道, weak 修飾的變量, 在目標釋放時,持有者的實例變量都會自動置爲nil, 所以如上dealloc方法中, 咱們只須要判斷weak引用的實例變量factor是否爲空便可.

抽取

以上操做, 咱們就能夠解決偶爾忘記寫移除Observer的代碼了. 如今只須要把實現抽取出來, 作成一個通用的工具方法:

我新建了一個NSObjectCategory, 並添加了一個方法, 以下:

ex1.png

而後將上述的實現進行了整合放到了.m中:

ex2.png

到此, 之後只須要調用- (void)sj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;這個方法便可, 移除就交給臨時變量本身搞定.

結語: 可以看到這裏, 老鐵是真愛了, 能夠幫小弟去點個Star. Over...

相關文章
相關標籤/搜索