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指標查詢例項的類物件。
這裡之所以使用runtime獲取類物件而不使用訊息傳送機制,是因為class是通過訊息傳送查詢到class方法,在class方法中返回的類物件,而class方法可能被重寫。
通過上圖我們可以看出,使用KVO監聽的屬性的isa指向的類物件已經不是Person了,而是一個繼承自Person的一個新的類NSKVONotifying_Person,我們並沒有建立過這個類,它是objc的runtime動態生成的。
NSKVONotifying_Person重寫了父類的setAge:
、class
、dealloc
方法,並且新增了一個_isKVOA
方法。
NSKVONotifying_Person的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的實現主要是因為在setter方法中呼叫_NSSetIntValueAndNotify
,而它呼叫willChangeValueForKey
,然後呼叫父類的setAge
方法,再呼叫didChangeValueForKey
方法,didChangeValueForKey
告訴observeValueForKeyPath
屬性改變。所以我們可以通過實現will和did方法手動出發KVO。
成員變數會不會觸發KVO
通過上面原理我們可以知道,KVO主要是通過重寫setter方法實現的,而成員變數並不會實現setter方法,所以成員變數的改變並不會觸發KVO。