宣告
有人說這是所謂的黑魔法, 本人在此宣告: 本專案無任何黑魔法, 對原始碼無任何侵害, 只是對註冊方法的封裝.
問題
我們都知道, 使用KVO模式, 對某個屬性進行監聽時, Observer 需要在必要的時刻進行移除, 否則 App 必然會 Crash. 這個問題有點煩人, 因為偶爾會忘記寫移除 Observer 的程式碼...
我一直想要這樣一個效果: 只管監聽, 並處理監聽方法. 不去分心, 管何時移除 Observer , 讓其能夠適時自動處理.
所幸, 它能夠實現, 先預覽一下:
@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
方法, 其持有的例項變數也會被釋放.
我就這樣想, 在監聽註冊時, 為self
和Observer
關聯個臨時物件, 當兩者在釋放例項變數時, 我藉助這個時機, 在臨時物件的dealloc
方法中, 移除Observer
就行了.
想法很好, 可總不能每個類裡都加一個臨時物件的屬性吧. 那如何在不改變原有類的情況下, 為其關聯一個臨時物件呢?
關聯屬性
不改變原有類, 這時候肯定是要用Category
了, 系統框架裡面有很多的分類, 並且有很多的關聯屬性, 如下圖 UIView 標頭檔案第180行:
依照上圖, 我們先看一個示例, 為NSObject
的新增一個Category
, 並新增了一個property
, 在.m
中實現了它的setter
和getter
方法.
#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中的兩個修飾符weak
與unsafe_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
的程式碼了. 現在只需要把實現抽取出來, 做成一個通用的工具方法:
我新建了一個NSObject
的Category
, 並新增了一個方法, 如下:
然後將上述的實現進行了整合放到了.m
中:
到此, 以後只需要呼叫- (void)sj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
這個方法即可, 移除就交給臨時變數自己搞定.
結語: 能夠看到這裡, 老鐵是真愛了, 可以幫小弟去點個Star. Over...