本文將主要介紹如下內容:html
官方文檔:git
Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.
KVO是一種編程模式,當被觀察的object的指定的屬性發生變化時候,觀察者object將會被告知。
KVO基於KVC,關於KVC可查閱Key-Value Coding Programming Guide。github
先看官文的敘述:
編程
Not all classes are KVO-compliant for all properties. You can ensure your own classes are KVO-compliant by following the steps described in KVO Compliance. Typically properties in Apple-supplied frameworks are only KVO-compliant if they are documented as such.
某些類的某些屬性是不能夠採用KVO進行觀察的。那什麼樣的類的屬性纔是可觀察的呢?
固然,首先該類須要遵照KVC;那什麼樣的類才遵照KVC呢,咱們要不要實現它的基礎細節呢?安全
Objects typically adopt key-value coding when they inherit from NSObject (directly or indirectly), which both adopts the NSKeyValueCoding protocol and provides a default implementation for the essential methods.
以上Key-Value Coding 文檔指出 :其實很簡單,繼承自NSObject的類就可使用KVC,由於NSObject已經實現了NSKeyValueCoding 協議相應的基礎功能;這樣一來就知足KVO使用的要求了。app
官文指出:註冊觀察者方法的調用,並不會強引用所涉及到的參數對象(被觀察對象,觀察者,context)。異步
The key-value observing addObserver:forKeyPath:options:context: method does not maintain strong references to the observing object, the observed objects, or the context. You should ensure that you maintain strong references to the observing, and observed, objects, and the context as necessary.
一般,爲了監聽變化,將實現該方法:
ide
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == PersonAccountBalanceContext) { // Do something with the balance… } else if (context == PersonAccountInterestRateContext) { // Do something with the interest rate… } else { // Any unrecognized context must belong to super [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } }
如上爲了不影響父類,會在分支語句的最後將調用父類方法。
此時若是全部父類(中間層類)都沒有處理該變化,傳到NSObject,後將會拋出一個異常(NSInternalInconsistencyException)。函數
If a notification propagates to the top of the class hierarchy, NSObject throws an NSInternalInconsistencyException because this is a programming error: a subclass failed to consume a notification for which it registered.
理論要求上,註冊觀察者的和移除觀察者,兩個方法應該是全局性成對的。
佈局
When removing an observer, keep several points in mind: Asking to be removed as an observer if not already registered as one results in an NSRangeException. You either call removeObserver:forKeyPath:context: exactly once for the corresponding call to addObserver:forKeyPath:options:context:, or if that is not feasible in your app, place the removeObserver:forKeyPath:context: call inside a try/catch block to process the potential exception. An observer does not automatically remove itself when deallocated. The observed object continues to send notifications, oblivious to the state of the observer. However, a change notification, like any other message, sent to a released object, triggers a memory access exception. You therefore ensure that observers remove themselves before disappearing from memory. The protocol offers no way to ask an object if it is an observer or being observed. Construct your code to avoid release related errors. A typical pattern is to register as an observer during the observer’s initialization (for example in init or viewDidLoad) and unregister during deallocation (usually in dealloc), ensuring properly paired and ordered add and remove messages, and that the observer is unregistered before it is freed from memory.
如上,關於移除觀察者的注意事項,官文給出了3個點:
- 向一個未註冊成爲觀察者的 object 發送 removeObserver 消息,將引起異常(NSRangeException);
官文強調,addObserver 與 removeObserver必須對應起來調用;
若是不成對調用,建議將 removeObserver:forKeyPath:context: 放在 try/catch 中(PS:估計try/catch 用在removeObserver這麼損的建議,基本不會有負責人會贊成組員這麼搞的)。- 觀察者在已dealloc的狀況下,也不會自動移除,此時被觀察者的屬性發生變化後,會繼續發送通知給已經釋放的觀察者,這樣就會觸發異常。要求開發者須要確保,觀察者在內存被回收以前,必須從被觀察者那裏移除。(PS:綜合以上來看,觀察者被引用的不是weak 型指針,應該是unsafe型的)。
- 開發者沒法查詢某個object是不是觀察者或者被觀察者,由於KVO的協議並無提供任何相關的方法;官文建議開發者在object 初始化的時候註冊觀察則,並在dealloc前移除觀察。(PS:若是是cell bind用途,且考慮到重用的話,這種建議無效)。
上面文檔中介紹到,KVO協議沒有任何方法獲取觀察者和被觀察者屬性,可是NSObject(NSKeyValueObservingCustomization)提供了一個看似有用的屬性:
/* Take or return a pointer that identifies information about all of the observers that are registered with the receiver, the options that were used at registration-time, etc. The default implementation of these methods store observation info in a global dictionary keyed by the receivers' pointers. For improved performance, you can override these methods to store the opaque data pointer in an instance variable. Overrides of these methods must not attempt to send Objective-C messages to the passed-in observation info, including -retain and -release. */ @property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;
註釋中提到,該屬性引用了向object實例對象註冊的全部observer,默認狀況下observationInfo是存儲在一個全局字典中,key使用的是觀察者的指針(PS: 還記得上文提到的對觀察者 unsafe 型的引用麼,不難理解,想必就是這裏了);爲了性能起見,建議使用者重寫,並提供自定義的數據類型(PS:非void *類型的,便可以明確知道內部佈局的對象——舉個栗子如容器中對應的泛型)。
Apple官文中這麼介紹的:
Automatic key-value observing is implemented using a technique called isa-swizzling. The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data. When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance. You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.
官文指出KVO的實現,使用了isa-swizzling。當一個object被觀察後,該object的 isa 指針將會被修改指向新生成的中間類,而非以前的類;這樣以來,isa 的值將不能真實的反映出該object的實際類型。開發者不該該依賴 isa 獲取object 所屬的 Class,而是應該調用 object的實例方法[object class] 獲取object的具體類型。(PS:不瞭解 isa 的朋友自行查閱下,這裏很少介紹了)
關於原理描述可參考下圖理解:
雖然沒有Apple的源碼,但若是有同窗真想要詳細瞭解KVO的實現原理,可參考Github上的 Aspect 開源庫,其源碼會給你答案。
FBKVOController源碼比較簡單,感興趣的可閱讀 FBKVOController Github地址,如下主要講下部分細節與你們可能漏掉的點。
類圖以下,結構很簡單:
類圖中,下劃線開頭的類(_FBKVOInfo、_FBKVOSharedController)屬於內部類,即並無對外界public。
先來看看FBKVOController中是怎麼描述的:
Key-value observing is a particularly useful technique for communicating between layers in a Model-View-Controller application. KVOController builds on Cocoa's time-tested key-value observing implementation. It offers a simple, modern API, that is also thread safe. Benefits include: Notification using blocks, custom actions, or NSKeyValueObserving callback. No exceptions on observer removal. Implicit observer removal on controller dealloc. Thread-safety with special guards against observer resurrection
提到四點:
想象一下當使用系統的KVO,咱們須要嚴格的讓addObser與removeObser 成對的調用(而且,大多數時候他們不在一個方法內,而是在兩個不一樣的時機點);在調用removeObser的時候須要判斷是否addObser某對象的某屬性(如上KVO注意事項4-3中,官文明確指出了開發者沒法查詢某個object是不是觀察者或者被觀察者,這無疑增長了本身維護狀態的難度),而且還須要判斷是否已經移除過(移除兩次將會報異常)。
線程安全。
_FBKVOSharedController中如下一段代碼,主要針對當觀察info中包含NSKeyValueObservingOptionInitial的時候作的特殊處理:
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info { if (nil == info) { return; } // register info pthread_mutex_lock(&_mutex); [_infos addObject:info]; pthread_mutex_unlock(&_mutex); // add observer [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info]; if (info->_state == _FBKVOInfoStateInitial) { info->_state = _FBKVOInfoStateObserving; } else if (info->_state == _FBKVOInfoStateNotObserving) { // this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions, // and the observer is unregistered within the callback block. // at this time the object has been registered as an observer (in Foundation KVO), // so we can safely unobserve it. [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info]; } } - (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info { if (nil == info) { return; } // unregister info pthread_mutex_lock(&_mutex); [_infos removeObject:info]; pthread_mutex_unlock(&_mutex); // remove observer if (info->_state == _FBKVOInfoStateObserving) { [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info]; } info->_state = _FBKVOInfoStateNotObserving; }
觸發場景爲——當info中包含NSKeyValueObservingOptionInitial,而且使用者在回調中寫了 remove該觀察對象時:
NSObject *object = xxx; NSObject *objectObservered = xxxx; __weak typeof(objectObservered) wkObservered = objectObservered; __weak typeof(object) wkObject = object; [objectObservered.KVOControllerNonRetaining observe:object keyPath:@"xxx" options:NSKeyValueObservingOptionInitial block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) { if (wkObservered && wkObject) { [wkObservered.KVOControllerNonRetaining unobserve:object keyPath:@"xxx"]; } }];
上面一段代碼執行的時候,_FBKVOSharedController中調用順序以下,並對代碼分析:
-(void)unobserve:(id)object info:(nullable _FBKVOInfo *)info ,
此時info->_state = _FBKVOInfoStateInitial,
因此該方法中的條件語句並不會執行 if (info->_state == _FBKVOInfoStateObserving) {
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
緊接着執行接下來一行代碼 info->_state = _FBKVOInfoStateNotObserving; 此時state 被變動爲_FBKVOInfoStateNotObserving。
if (info->_state == _FBKVOInfoStateInitial) { info->_state = _FBKVOInfoStateObserving; } else if (info->_state == _FBKVOInfoStateNotObserving) { // this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions, // and the observer is unregistered within the callback block. // at this time the object has been registered as an observer (in Foundation KVO), // so we can safely unobserve it. [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info]; }
APPLE KVO官文 — Key-Value Observing Programming Guide
FBKVOController Github地址
Cocoa Bindings Programming Topics
Key-Value Coding Programming Guide
Aspect