KVO的使用和底層實現原理

Simba_LX發表於2018-07-31

####1.什麼是KVO

KVO是Key-Value-Observing的縮寫,通過KVO這種機制物件可以通過它得到其他物件的某個屬性的變更通知。這種機制在MVC模式下顯得更為重要,KVO可以讓檢視物件經過控制器觀察模型物件的變更從而做出更新等操作。

####2.KVO的使用

/** 
* 1. self.person:要監聽的物件 
* 2. 引數說明: 
* @param addObserver 觀察者,負責處理監聽事件的物件 
* @param forKeyPath 要監聽的屬性 
* @param options 觀察的選項(觀察新、舊值,也可以都觀察) 
* @param context 上下文,用於傳遞資料,可以利用上下文區分不同的監聽 
*/
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"Person Name"];

/**
 *  當監控的某個屬性的值改變了就會呼叫
 *
 *  @param keyPath 監聽的屬性名
 *  @param object  屬性所屬的物件
 *  @param change  屬性的修改情況(屬性原來的值`oldValue`、屬性最新的值`newValue`)
 *  @param context 傳遞的上下文資料,與監聽的時候傳遞的一致,可以利用上下文區分不同的監聽
 */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"%@物件的%@屬性改變了:%@", object, keyPath, change);
}

/**
*   移除KVO監聽
*/
-(void)dealloc{
    [self.person removeObserver:self forKeyPath:@"name"];
}
複製程式碼

####3.KVO的使用場景
KVO主要用於使用者介面互動,當多個View共同使用了同一個實體,當這個實體中的某個屬性改變時,如果需要更新多個介面,KVO的作用就發揮出來了。

####4.KVO的底層實現原理

  • KVO是基於runtime機制實現的

  • 當某個類的屬性物件第一次被觀察時,系統就會在執行期動態地建立該類的一個派生類,在這個派生類中重寫基類中任何被觀察屬性的setter 方法。派生類在被重寫的setter方法內實現真正的通知機制

  • 如果原類為Person,那麼生成的派生類名為NSKVONotifying_Person

  • 每個類物件中都有一個isa指標指向當前類,當一個類物件的第一次被觀察,那麼系統會偷偷將isa指標指向動態生成的派生類,從而在給被監控屬性賦值時執行的是派生類的setter方法

  • 鍵值觀察通知依賴於NSObject 的兩個方法: willChangeValueForKey:didChangevlueForKey:;在一個被觀察屬性發生改變之前, willChangeValueForKey:一定會被呼叫,這就 會記錄舊的值。而當改變發生後,didChangeValueForKey:會被呼叫,繼而 observeValueForKey:ofObject:change:context:也會被呼叫。

  • 補充:KVO的這套實現機制中蘋果還偷偷重寫了class方法,讓我們誤認為還是使用的當前類,從而達到隱藏生成的派生類

1429890-b28e010d3a7dbdb8.png

####5.KVO的缺陷

  • API 設計缺陷。NSNotification 的 API 允許我們傳入 selector( -addObserver: selector: name: object:),這樣收到通知的時候就可以呼叫到 selector ;相反使用 KVO 的話,我們必須在觀察者類當中重寫 -observeValueForKeyPath:ofObject:change:context: 方法,我們不得不在這個方法里加很多判斷邏輯寫一坨程式碼來監聽不同的屬性。

  • -addObserver:forKeyPath:options:context: 裡傳入 NSString 物件作為 keyPath,編譯器是無法檢測錯誤的,如果屬性名被更改了,這個觀察就沒意義了。這個問題可以通過 NSStringFromSelector 稍稍改善。另外,傳給 context 的引數大部分情況下都是 nil.

  • 如果父類和子類都監聽了同一個物件的同一個屬性,很容易出現父類中remove了一次,子類又remove了一次的情況,應用程式第二次 remove 就會丟擲異常然後 crash. 解決方案見 KVO Considered Harmful .

  • KVO 可能會導致死迴圈。死迴圈發生的場景是:你在-observeValueForKeyPath 方法裡邊設定了屬性,而這個屬性剛好被自己監聽。

  • 某些情況下使用 KVO 監聽屬性會失效。比如說被監聽屬性的 setter 被 hook 了;還有一種情況是,對 weak 屬性監聽,如果 weak 例項變數指向的物件被釋放了,weak 修飾的例項變數會自動設為 nil,KVO 也就失效了。

相關文章