為系統(tǒng)的KVO功能添加Block(閉包)特性

原文鏈接: 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), 并重寫CCInternalObserverdealoc方法, 移除所有觀察.

完整的代碼

上面已經(jīng)把核心的代碼細(xì)節(jié)都說(shuō)完了. 完整的代碼我已經(jīng)做成一個(gè)Category NSObject+CCEasyKVO.h, 直接引入項(xiàng)目就可以使用了. CCEasyKVO源碼

推薦閱讀

更復(fù)雜的KVO解決方案

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • KVO 作為 iOS 中一種強(qiáng)大并且有效的機(jī)制,為 iOS 開發(fā)者們提供了很多的便利;我們可以使用 KVO 來(lái)檢測(cè)...
    JzRo閱讀 1,088評(píng)論 0 2
  • 該文章屬于劉小壯原創(chuàng),轉(zhuǎn)載請(qǐng)注明:劉小壯[http://m.itdecent.cn/u/2de707c93d...
    劉小壯閱讀 49,058評(píng)論 35 227
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,689評(píng)論 1 32
  • 3.block Block是最近才加入Objective-C的,它首次出現(xiàn)在OSX10.6和iOS4平臺(tái)上。Blo...
    Saxon_Geoffrey閱讀 1,285評(píng)論 1 8
  • 時(shí)間隨呼吸在不經(jīng)意間悄悄溜走,漸漸炎熱的即將結(jié)束的五月,總是在刺眼的陽(yáng)光下容易迷失自己,慵懶的思想給自己一次又一次...
    夏目Kiki閱讀 163評(píng)論 0 1

友情鏈接更多精彩內(nèi)容