文章結構
前言
在 iOS 開發中,常常需要在不同的對象、不同的視圖(View)或不同的視圖控制器(ViewController)之間通信,傳遞數據。主要的實現方法有:
- 直接通過 superView 或 subView 傳遞數據,或者在類中添加其他對象的引用。方法直接但效率低、容易使代碼混亂,難以處理復雜的關系。
- 通過自帶的或自定義的 delegate 協議通信。效率較高,能完成復雜的通信及執行復雜的操作,代碼結構較好,但是代碼量比較大。
- 使用 KVO(Key-value observing)。能夠穿越復雜的關系網,直接觀察其他對象的屬性,獲取信息,根據所觀察對象的變化進行響應,代碼量少。
這些方法各有優劣,在不同的情況下選用合適的方法是最好的。因此掌握這些方法,才能更好地應對各種開發難題。KVO 是本文關注的重點。
KVO 簡介
在 Apple 的應用開發里 KVO 提供了一個途徑,使對象(觀察者)能夠觀察其他對象(被觀察者)的屬性,當被觀察者的屬性發生變化時,觀察者就會被告知該變化。這其實就對應設計模式中的觀察者模式。
觀察者:Observer,the observing object;被觀察者:the observed object
前提條件
在實現 KVO 之前,需要確保被觀察的對象是支持 KVO 的。通常繼承自 NSObject 的對象都會自動支持 KVO。對于非繼承自 NSObject 的類,也可以手動實現 KVO 支持。
適用場景
KVO 能很方便地實現模型(Model)和控制器(Controller)之間的通信。主要的應用場景有:
KVO 能夠實現一對多、多對多、多對一的觀察。也就是說,KVO 沒有限制觀察者和被觀察者的數量。當同時觀察多個對象時,不但對象本身發生改變時會告知觀察者,而且被觀察對象發生替換、刪除或插入等操作時也會告知觀察者。
實現 KVO:注冊觀察者和觀察者方法
基本流程
- 添加觀察者:
addObserver:forKeyPath:options:context:
; - 實現觀察響應方法:
observeValueForKeyPath:ofObject:change:context:
; - 在觀察者 deallocted 之前移除觀察者:
removeObserver:forKeyPath:
;
添加觀察者
observedObject.addObserver:forKeyPath:options:context:
注意:
- 調用該函數的 observedObject 是被觀察者,參數 addObserver 后面的是觀察者;
- forKeyPath 參數是 String 類型的,代表 observedObject 的屬性,私有屬性也可以觀察,但是在 Swift 中需要把被觀察對象的屬性用
dynamic
標記,如:
class ObservedObjectClass: NSObject {
//在 Swift 中要用 dynamic 標記被觀察的屬性
dynamic private var observedProperty = ""
...
}
-
options:可以選擇獲取的數據包含哪些內容,獲取的數據是以字典的形式傳遞的。
- NSKeyValueObservingOptionOld: 獲取變化前的數據
- NSKeyValueObservingOptionNew: 獲取變化后的數據
- NSKeyValueObservingOptionInitial: 獲取設置觀察者時被觀察者的初始數據,即在 addObserver 函數調用完成前,被觀察者的數據。
- NSKeyValueObservingOptionPrior: 在變化前后分別發送消息(共發送兩次消息)
context:可選的參數,會隨著觀察消息傳遞,用于區分接收該消息的觀察者。一般情況下,只需通過 keyPath 就可以判斷接收消息的觀察者。但是當父類子類都觀察了同一個 keyPath 時,僅靠 keyPath 就無法判斷消息該傳給子類,還是傳給父類。
addObserver 并不會維持對觀察者、被觀察者和 Context 的強引用。如果需要的話,要自行維持對它們的強引用。
觀察響應方法:
所有的觀察者都必須實現觀察響應方法:
observeValueForKeyPath:ofObject:change:context:
-
change 是一個字典,包含了一系列鍵-值。
- NSKeyValueChangeKindKey: 變化的類型
- NSKeyValueChangeOldKey: 變化前的值
- NSKeyValueChangeNewKey: 變化后的值
- NSKeyValueChangeIndexesKey: 在所有變化中的坐標
-
NSKeyValueChangeKindKey 又包含了:
- NSKeyValueChangeSetting
- NSKeyValueChangeInsertion
- NSKeyValueChangeRemoval
- NSKeyValueChangeReplacement
如果接收到的觀察者消息于當前的 Context 不符,就需要把消息傳給 父類,直到尋找到對應的 Context。
如果一個消息傳到了 NSObject 仍然沒有找到它的觀察者,那么就會拋出異常:NSInternalInconsistencyException。
移除觀察者
當一個對象不再需要觀察另一個對象時,就需要移除觀察。
observedObject.removeObserver:forKeyPath:context:
這個方法和添加觀察者的方法是對應的。
移除觀察者需要注意以下幾點:
- 一個對象如果沒有注冊成為觀察者,那么當調用 removeObserver 移除它時,就會拋出異常。所以想要安全地移除觀察者,可以使用 do、try、catch 來調用 removeObserver。
- 觀察者不會在 dealloc 的時候自動移除。因此最晚必須在觀察者 dealloc 時移除它。
- 系統沒有自帶的方法用于判斷一個對象是否注冊為觀察者,因此盡量在初始化的時候注冊觀察者,在 dealloc 時移除。
總結
KVO 能夠在復雜的關系網中直接觀察某個對象,合理的使用 KVO 能夠簡化代碼。但是 KVO 也有很多坑,稍有不慎就會拋出異常或者無法建立觀察。在實踐中,還是應該選擇合適的方法來完成對象間的通信,熟練應對各種情況。
歡迎訪問我的Github:LinShiwei (Lin Shiwei) · GitHub
有任何疑問的話,歡迎在下方評論區討論。