iOS-觀察者模式-KVO、NSNotification的實現原理.KVC原理

觀察者模式的定義:一個目標對象管理所有依賴於它的觀察者對象,並在它自身的狀態改變時主動通知觀察者對象。這個主動通知通常是通過調用各觀察者對象所提供的接口方法來實現的。觀察者模式較完美地將目標對象與觀察者對象解耦。


KVO基於runtime實現,當你觀察一個對象的時候,一個新類被動態創建繼承於被觀察對象的類,並重寫所被觀察屬性的setter方法,並在賦值語句前後分別加上valueWillChange:和 valueDidChange方法和響應通知,最後把被觀察對象的isa指針指向了這個新類。(關於isa指針的問題可以瞭解下iOS-知識梳理(類探究、isa)

雖然修改了實例對象的isa指針指向,但是調用class 方法的時候依舊返回的之前的類信息

+ class是保存在之前類的元類裏,應該是不會變的 肯定是返回之前的類。

- class 按理說isa指向了新類,應該是調用了新類methodList的class,但是返回的卻是父類的Class,所以我猜測這裏應該是對class方法沒有進行重寫直接調用到了父類裏面(沒有考證)。

看上去沒有什麼問題,但是好多大牛一直在吐槽KVO,爲啥子嘞?

1,

- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context {

    if(object == someObj && [keyPath isEqualToString:@"keypath"]) {

    [self doSomeThing];

}

上面是我們用kvo時的回調方式,當我們添加好多observer時這裏的代碼將會非常長,極不優雅。

2,keyPath 必須是NSString, 很容易寫錯,編譯的時候並不能找出這個錯誤。

3,需要自己處理superClass 裏面的observe,

這裏稍微解釋一下什麼意思,舉個例子:

A類裏有個scrollview,在A類裏對scrollview的 contentOffset進行觀察,通常我們還需要在dealloc裏面 removeObserver.

現在B繼承於A,也對scrollView進行了contentOffset觀察,爲了保證父類的方法能被執行到我們必須這麼寫

if(object == obj && [keyPath isEqualToString:@"contentOffset"]) {

    [self doSomeThing];

}else{

    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}

麻煩不?

並且我們會在B類裏的dealloc裏面removeObserver。

dealloc是在A類和B類都要調用的 也就是remove了兩次, 閃退了。。。

4,還有人在使用的時候吐槽 change, 和 context 這兩個參數

首先看change:

NSKeyValueObservingOptionNew: 指示change字典中包含新屬性值;

NSKeyValueObservingOptionOld: 指示change字典中包含舊屬性值;

NSKeyValueObservingOptionInitial: 相對複雜一些,NSKeyValueObserving.h文件中有詳細說明,此處略過;

NSKeyValueObservingOptionPrior: 相對複雜一些,NSKeyValueObserving.h文件中有詳細說明,此處略過;

有沒有覺得超級複雜。。。

再來想一下context,我就想問問用過kvo的小夥伴們都傳過什麼context,大家是不是都在用NULL?

其實是不對的,下面有正確的使用方法。

既然有這麼多槽點,那麼如何使用呢

首先keypath這個參數 我們可以用NSStringFromSelector來避免書寫錯誤。

解決superClass的那個問題就得設置一個獨一無二的context,回調的時候進行校驗,當然會很麻煩。

 

再回頭說一下觀察者模式。。。

以NSNotification爲例,試着自己實現一個通知中心。大概想一下的它的實現思路:

首先要創建個單例

添加觀察者的時候 要保存對象、方法,並且與Name關聯,應該是把target-acttion放在數組中 然後以name爲key放入字典中。

PostName的時候 尋找name對應的target-action數組,遍歷執行。。。(當然中間還有帶參數的處理)

現在拋出一個問題,既然是數組和字典存儲那肯定是強引用,那麼如何保證單例對觀察者的引用是弱引用呢(如果不是弱引用觀察者永遠不釋放)?或者如果不是這種實現方式,還有別的好辦法嗎?

 

新建一個對象,對象裏面添加 property(weak) id target(觀察者); 保存方法 及參數。然後將該對象放入數組當中。這樣就實現了弱引用target。

PostName的時候遍歷執行的時候,如果該對象的target爲nil,說明觀察者已經釋放了,此時將該對象移除數組,也就省了iOS9之前添加觀察者 在觀察者dealloc的時候還要removeObserver。iOS9之前使用的unsafe_unretained修飾的target,所以釋放的時候要求removeObserver,要不然會造成野指針。。。

以上爲猜測,如果有錯誤或者好的方案請大神指點一下,感興趣的也可以自己實現一下(系統的api還有block的使用,也可以試一下)。

這種方案我在項目實現一個白天夜景的切換的時候封裝一個category時使用過,使用起來很方便。(ios-白天夜景切換方案

 

最後說一下KVC的實現原理

首先查找是否實現了setter方法嗎,如果有,優先調用完成賦值。

如果沒有setter方法,調用 accessInstanceVariablesDirectly詢問,如果返回的YES,則順序匹配變量名與 _<key>,_is<Key>,<key>,is<Key>,匹配到則設定其值。如果返回NO,結束查找。並調用 setValue:forUndefinedKey:報異常

如果既沒有setter也沒有實例變量,調用 setValue:forUndefinedKey:。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章