1. 程式人生 > >iOS-知識梳理(觀察者模式-KVO、NSNotification的實現原理.KVC原理)

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:。