原文鏈接: http://blog.cocosdever.com/2019/07/03/Let-the-system-s-kvo-also-support-block/
文檔更新說(shuō)明
- 最后更新 2019年07月05日
- 首次更新 2019年07月03日
前言
OC為用戶提供了一套觀察者模式(KVO), 當(dāng)對(duì)象的某些屬性發(fā)生變化之后, 就會(huì)向所有觀察者(observer)廣播消息, 具體的KVO基本用法這里就不說(shuō)了. 下面主要說(shuō)一下為系統(tǒng)的KVO功能添加block的思路, 先看一下最終的API:
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");
}];
在KVO中傳送block的方法
要添加block功能到系統(tǒng)的KVO中, 首先要做的事情是傳這個(gè)block指針能傳入KVO中, 在消息廣播的時(shí)候又能把這個(gè)block帶回來(lái).先看一下系統(tǒng)的API:
// NSObject類
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
// 觀察者(observer)必須實(shí)現(xiàn)下面方法才能接收到廣播
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
其中有一個(gè)參數(shù)是content, 允許傳入void *類型的指針, 所以我們可以直接把用戶傳入的block轉(zhuǎn)成void *類型, 傳入KVO中, 這樣當(dāng)消息進(jìn)行廣播的時(shí)候, 就可以從這個(gè)context中得到block的地址, 再調(diào)用block即可.
利用內(nèi)部觀察者創(chuàng)建便捷API
經(jīng)過(guò)上面分析可知, 要為系統(tǒng)的KVO功能添加block特性理論上是可行的, 下面就開始代碼的實(shí)現(xiàn)部分.
添加block屬性就是為了方便使用系統(tǒng)的KVO功能, 所以我們首選分類(Category)來(lái)實(shí)現(xiàn), 直接擴(kuò)展NSObject, 這樣所有的對(duì)象都有便捷的操作了.
// NSObject+CCEasyKVO.h
/**
@abstract 回調(diào)函數(shù)
@param object 狀態(tài)發(fā)生變化的對(duì)象(被觀察者)
@param change 發(fā)生變化的信息
*/
typedef void (^CC_EasyBlock)(id object, NSDictionary<NSKeyValueChangeKey, id> *change);
@interface NSObject (CCEasyKVO)
/**
簡(jiǎn)易KVO
@param observe 被觀察者
@param keyPath key
@param options options
@param block 回調(diào)函數(shù)
*/
- (void)cc_easyObserve:(id)observe forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(CC_EasyBlock) block;
- (void)cc_easyRemoveAllKVO;
@end
上面就是我們的頭文件部分, 比較簡(jiǎn)單, 主要就是系統(tǒng)了一個(gè)便捷KVO的api, 其中CC_EasyBlock就是用戶需要傳入的block.
遇到的第一個(gè)問(wèn)題
接下來(lái)要解決一個(gè)重要的問(wèn)題. 我們能否直接使用當(dāng)前被分類的對(duì)象作為觀者者直接觀察observe呢? 答案是否定的, 這個(gè)你可以自己嘗試一下. 原因就是當(dāng)用戶在被分類的類里也實(shí)現(xiàn)了系統(tǒng)KVO接受廣播的方法observeValueForKeyPath...時(shí), 分類代碼里就無(wú)法再收到系統(tǒng)的廣播了.
為了解決這個(gè)問(wèn)題, 我們可以在分類里使用自定義的類(CCInternalObserver)來(lái)作為觀察者, 這樣就算用戶給自己的類實(shí)現(xiàn)了接受廣播的方法, 也不影響我們的代碼. 我們?cè)贑CInternalObserver里實(shí)現(xiàn)observeValueForKeyPath..., 當(dāng)廣播到來(lái)時(shí), 調(diào)用context指向的block.
遇到的第二個(gè)問(wèn)題
如何避免用戶傳入的block內(nèi)存被釋放? 簡(jiǎn)單說(shuō)就是如何管理block內(nèi)存? oc的block一共有三種, 分別是全局塊NSGlobalBlock, 堆塊NSMallocBlock, 棧塊NSStackBlock. 這里順便簡(jiǎn)單介紹一下他們的區(qū)別:
(1) block類型區(qū)別
沒有引用外部任何變量(static變量除外), 創(chuàng)建的就是NSGlobalBlock;
除了NSGlobalBlock, 其他創(chuàng)建的時(shí)候就是NSStackBlock, 賦值給strong類型的變量之后就是NSMallocBlock, 這里也稱之為copy操作;
在符合NSStatckBlock的條件下, 可以通過(guò)兩種方法獲取NSStatckBlock:
1. 在調(diào)用方法時(shí)創(chuàng)建匿名block, 在方法內(nèi)部得到的block變量是NSStaticBlock
2. 創(chuàng)建的block賦值給__weak變量.
(2) 內(nèi)存管理
NSStackBlock類型的塊, 會(huì)隨棧內(nèi)存釋放而釋放, 使用的時(shí)候需要先用strong變量存儲(chǔ)起來(lái), 否則將crash;
NSGlobalBlock類型的塊, 不會(huì)被釋放; NSMallocBlock類型和其他引用類型一樣, 沒人引用就會(huì)被釋放;
除了NSStackBlock類型, 其他類型賦值給變量的時(shí)候都不會(huì)重復(fù)copy.
用戶傳入的block可能是三種類型之一, 為了避免內(nèi)存出問(wèn)題, 在轉(zhuǎn)成void *的時(shí)候就需要做一點(diǎn)額外的處理, 才能傳給系統(tǒng)的KVO:
// 用戶傳入的block可能是NSStackBlock, 所以在轉(zhuǎn)為無(wú)類型指針的時(shí)候必須將所有權(quán)轉(zhuǎn)移給CoreFoundatin層, 這樣一來(lái)block類型會(huì)轉(zhuǎn)為NSMallocBlock并被持有, 也就安全了
[observe addObserver:self.observer forKeyPath:keyPath options:options context:(__bridge_retained void *)block];
順便說(shuō)一句, self.observer就是上面說(shuō)的CCInternalObserver : )
遇到的第三個(gè)問(wèn)題
第三個(gè)問(wèn)題就是如何注銷觀察者. 系統(tǒng)的KVO功能還有一個(gè)麻煩的地方就是每次用完都需要手動(dòng)注銷, 否則被觀察的對(duì)象一會(huì)向那些已經(jīng)注冊(cè)過(guò)的觀察者廣播消息時(shí), 如果觀察者被內(nèi)存被釋放了就會(huì)引發(fā)EXC_BAD_ACCESS , 所以當(dāng)觀察者被釋放時(shí), 要及時(shí)把觀察者(observer)從被觀察者(observe)身上移除.
為了解決這個(gè)問(wèn)題, 可以在CCInternalObserver創(chuàng)建一個(gè)哈希表, 存放所有被觀察者(observe), 并重寫CCInternalObserver的dealoc方法, 移除所有觀察.
完整的代碼
上面已經(jīng)把核心的代碼細(xì)節(jié)都說(shuō)完了. 完整的代碼我已經(jīng)做成一個(gè)Category NSObject+CCEasyKVO.h, 直接引入項(xiàng)目就可以使用了. CCEasyKVO源碼