ObjC中KVO原理簡析

SoC發表於2019-03-01

KVO的全稱是Key-Value Observing,也稱“鍵值監聽”,可以用於監聽某個物件屬性值的改變。

使用KVO

通常我們通過addObserver:forKeyPath:options:context來監聽某個例項的某個屬性變化。當該屬性的值發生變化的時候會通過- (**void**)observeValueForKeyPath:(NSString *)keyPath ofObject:(**id**)object change:(NSDictionary<NSKeyValueChangeKey,**id**> *)change context:(**void** *)context通知我們。

顯然實現這個功能的基礎是屬性值得改變,而屬性值得改變需要呼叫setter方法。可是即使setter方法是我們重寫的,並且在其實現中並沒有編寫關於監聽屬性改變通知監聽的程式碼,KVO依然可以起作用。這是為什麼呢?

KVO實現原理

我們都知道一個類的例項方法存在於其類物件中,當一個類的例項向例項方法傳送呼叫訊息時候,會通過類例項的isa指標去其類物件中找到其對應的例項方法執行。setter方法就存在於類物件中。

新的類物件

我們通過runtime的object_getClass()直接利用例項物件的isa指標查詢例項的類物件。

KVO監聽的例項

這裡之所以使用runtime獲取類物件而不使用訊息傳送機制,是因為class是通過訊息傳送查詢到class方法,在class方法中返回的類物件,而class方法可能被重寫。

通過上圖我們可以看出,使用KVO監聽的屬性的isa指向的類物件已經不是Person了,而是一個繼承自Person的一個新的類NSKVONotifying_Person,我們並沒有建立過這個類,它是objc的runtime動態生成的。

NSKVONotifying_Person的例項方法列表

NSKVONotifying_Person重寫了父類的setAge:classdealloc方法,並且新增了一個_isKVOA方法。

NSKVONotifying_Person的setter方法

獲取setter方法實現

我們可以看出在為person例項新增observer之前和之後的setAge:方法的實現發生了變化,之前是直接呼叫類物件中儲存的例項方法,新增之後呼叫了Foundation框架中_NSSetIntValueAndNotify函式。

之所以是_NSSet*IntValue*AndNotify是因為age屬性我們定義的是int型別,若是其他型別則會相應更改

_NSSetIntValueAndNotify方法中,會呼叫willChangeValueForKey,然後呼叫父類的setAge方法,再呼叫didChangeValueForKey方法。在didChangeValueForKey中通知屬性改變,從而使得observeValueForKeyPath得到訊息。

NSKVONotifying_Person的class方法

在我們獲取person例項的類物件的時候,我們發現使用runtime的object_getClass和使用訊息傳送機制[_person class]得到的結果不一樣,這是因為NSKVONotifying_Person重寫了class方法的實現,在實現中呼叫了[super class],而NSKVONotifying_Person又是Person的子類,所以獲取到的Person。

NSKVONotifying_Person的dealloc方法

由於runtime在例項物件新增了KVO之後動態建立了類和一些物件,所以可能會在dealloc中回收這些資源。

NSKVONotifying_Person的_isKVOA方法

_isKVOA 應該是會返回一個BOOL型別的值,表示是否使用了KVO。

手動觸發KVO

手動出發KVO

通過上面我們可以得知,KVO的實現主要是因為在setter方法中呼叫_NSSetIntValueAndNotify,而它呼叫willChangeValueForKey,然後呼叫父類的setAge方法,再呼叫didChangeValueForKey方法,didChangeValueForKey告訴observeValueForKeyPath屬性改變。所以我們可以通過實現will和did方法手動出發KVO。

成員變數會不會觸發KVO

通過上面原理我們可以知道,KVO主要是通過重寫setter方法實現的,而成員變數並不會實現setter方法,所以成員變數的改變並不會觸發KVO。

相關文章