爲系統的KVO功能添加Block(閉包)特性

原創文章首發本人博客:blog.cocosdever.com/2019/07/03/…git

文檔更新說明

  • 最後更新 2019年07月05日
  • 首次更新 2019年07月03日

前言

  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");
}];

複製代碼

在KVO中傳送block的方法

  要添加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便可.安全

利用內部觀察者建立便捷API

  通過上面分析可知, 要爲系統的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), 並重寫CCInternalObserverdealoc方法, 移除全部觀察.

完整的代碼

  上面已經把核心的代碼細節都說完了. 完整的代碼我已經作成一個Category NSObject+CCEasyKVO.h, 直接引入項目就可使用了. CCEasyKVO源碼   

推薦閱讀

更復雜的KVO解決方案

相關文章
相關標籤/搜索