原創文章首發本人博客:blog.cocosdever.com/2019/07/03/…git
OC爲用戶提供了一套觀察者模式(KVO), 當對象的某些屬性發生變化以後, 就會向全部觀察者(observer)廣播消息, 具體的KVO基本用法這裏就不說了. 下面主要說一下爲系統的KVO功能添加block的思路, 先看一下最終的API:github
UIView *v = [[UIView alloc] init];
NSObject *obj = [[NSObject alloc] init];
[obj cc_easyObserve:v forKeyPath:@"backgroundColor" options:NSKeyValueObservingOptionNew block:^(id object, NSDictionary<NSKeyValueChangeKey,id> *change) {
NSLog(@"hello");
}];
複製代碼
要添加block功能到系統的KVO中, 首先要作的事情是傳這個block指針能傳入KVO中, 在消息廣播的時候又能把這個block帶回來.先看一下系統的API: api
// NSObject類
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
// 觀察者(observer)必須實現下面方法才能接收到廣播
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
複製代碼
其中有一個參數是content, 容許傳入void *
類型的指針, 因此咱們能夠直接把用戶傳入的block轉成void *
類型, 傳入KVO中, 這樣當消息進行廣播的時候, 就能夠從這個context中獲得block的地址, 再調用block便可.安全
通過上面分析可知, 要爲系統的KVO功能添加block特性理論上是可行的, 下面就開始代碼的實現部分.函數
添加block屬性就是爲了方便使用系統的KVO功能, 因此咱們首選分類(Category)來實現, 直接擴展NSObject, 這樣全部的對象都有便捷的操做了.ui
// NSObject+CCEasyKVO.h
/** @abstract 回調函數 @param object 狀態發生變化的對象(被觀察者) @param change 發生變化的信息 */
typedef void (^CC_EasyBlock)(id object, NSDictionary<NSKeyValueChangeKey, id> *change);
@interface NSObject (CCEasyKVO)
/** 簡易KVO @param observe 被觀察者 @param keyPath key @param options options @param block 回調函數 */
- (void)cc_easyObserve:(id)observe forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(CC_EasyBlock) block;
- (void)cc_easyRemoveAllKVO;
@end
複製代碼
上面就是咱們的頭文件部分, 比較簡單, 主要就是提供了一套便捷KVO的api, 其中CC_EasyBlock
就是用戶須要傳入的block.spa
接下來要解決一個重要的問題. 咱們可否直接使用當前被分類的對象做爲觀者者直接觀察observe
呢? 答案是否認的, 這個你能夠本身嘗試一下. 緣由就是當用戶在類裏也實現了系統KVO接受廣播的方法observeValueForKeyPath...
時, 調用的其實是分類的代碼, 用戶的類裏就沒法再收到系統的廣播了. 爲了解決這個問題, 咱們能夠在分類裏使用自定義的類(CCInternalObserver)來做爲觀察者, 這樣就算用戶給本身的類實現了接受廣播的方法, 也不影響咱們的代碼. 咱們在CCInternalObserver裏實現observeValueForKeyPath...
, 當廣播到來時, 調用context指向的block.指針
如何避免用戶傳入的block內存被釋放? 簡單說就是如何管理block內存? oc的block一共有三種, 分別是全局塊NSGlobalBlock
, 堆塊NSMallocBlock
, 棧塊NSStackBlock
. 這裏順便簡單介紹一下他們的區別:code
(1) block類型區別
沒有引用外部任何變量(static變量除外), 建立的就是NSGlobalBlock;
除了NSGlobalBlock, 其餘建立的時候就是NSStackBlock, 賦值給strong類型的變量以後就是NSMallocBlock, 這裏也稱之爲copy操做;
在符合NSStatckBlock的條件下, 能夠經過兩種方法獲取NSStatckBlock:
1. 在調用方法時建立匿名block, 在方法內部獲得的block變量是NSStatckBlock
2. 建立的block賦值給__weak變量.
(2) 內存管理
NSStackBlock類型的塊, 會隨棧內存釋放而釋放, 使用的時候須要先用strong變量存儲起來, 不然將crash;
NSGlobalBlock類型的塊, 不會被釋放; NSMallocBlock類型和其餘引用類型同樣, 沒人引用就會被釋放;
除了NSStackBlock類型, 其餘類型賦值給變量的時候都不會重複copy.
複製代碼
用戶傳入的block多是三種類型之一, 爲了不內存出問題, 在轉成void *的時候就須要作一點額外的處理, 才能傳給系統的KVO:server
// 用戶傳入的block多是NSStackBlock, 因此在轉爲無類型指針的時候必須將全部權轉移給CoreFoundatin層, 這樣一來block類型會轉爲NSMallocBlock並被持有, 也就安全了
[observe addObserver:self.observer forKeyPath:keyPath options:options context:(__bridge_retained void *)block];
複製代碼
順便說一句, self.observer
就是上面說的CCInternalObserver
: )
第三個問題就是如何註銷觀察者. 系統的KVO功能還有一個麻煩的地方就是每次用完都須要手動註銷, 不然被觀察的對象一會向那些已經註冊過的觀察者廣播消息時, 若是觀察者被內存被釋放了就會引起EXC_BAD_ACCESS
, 因此當觀察者被釋放時, 要及時把觀察者(observer)從被觀察者(observe)身上移除. 爲了解決這個問題, 能夠在CCInternalObserver
建立一個哈希表, 存放全部被觀察者(observe), 並重寫CCInternalObserver
的dealoc
方法, 移除全部觀察.
上面已經把核心的代碼細節都說完了. 完整的代碼我已經作成一個Category NSObject+CCEasyKVO.h
, 直接引入項目就可使用了. CCEasyKVO源碼