版本記錄
版本號 | 時間 |
---|---|
V1.0 | 2017.09.14 |
前言
KVO
具有更強大的功能,是蘋果給我們的一個回調機制,在某個對象注冊監聽者后,在被監聽的對象發生改變時,對象會發送一個通知給監聽者,以便監聽者執行回調操作。接下來幾篇就詳細的解析一下KVO。感興趣的可以看上面幾篇。
1. KVO解析(一) —— 基本了解
鍵值觀察步驟
為確保對象可以接受鍵值觀察的通知,必須執行如下步驟:
為被觀察的對象注冊觀察者,使用方法addObserver:forKeyPath:options:context:
實現方法observeValueForKeyPath:ofObject:change:context:,在觀察者的內部接收改變的通知消息。
當不再需要接收消息的時候,使用方法removeObserver:forKeyPath:移除通知,還有一點,就是要在觀察者從內存釋放之前調用此方法。
還要注意:并非所有類都符合所有屬性的KVO。 您可以按照KVO合規性中描述的步驟確保您自己的類符合KVO。 通常,如果Apple提供的框架中的屬性通常被記錄,那么它們符合KVO的合規性。
注冊觀察者
觀察對象首先通過發送一個addObserver:forKeyPath:options:context:
消息來注冊觀察對象,將自身傳遞給觀察者和要觀察的屬性的關鍵路徑。 觀察者另外指定一個選項參數option
和一個上下文指針context
來管理通知的各個方面。
1. Options
指定為選項常量的按位OR的options參數影響通知中提供的change
字典的內容以及生成通知的方式。
您選擇通過指定選項NSKeyValueObservingOptionOld
從更改之前接收觀察到的屬性的值。 您使用選項NSKeyValueObservingOptionNew
請求屬性的新值。 您可以使用這些選項的按位OR來收到舊值和新值。
您指示被觀察的對象發送立即更改通知(在addObserver:forKeyPath:options:context:returns
之前),其選項為NSKeyValueObservingOptionInitial
。 您可以使用此額外的一次性通知來建立觀察者中的屬性的初始值。
您可以通過包含NSKeyValueObservingOptionPrior
選項來指示觀察對象在屬性更改之前發送通知(除更改之后的通常通知之外)。 change
更改字典表示通過將NSKeyValueChangeNotificationIsPriorKey
的關鍵字與NSNumber
包裝YES的值相關聯的代替通知。 那個key是不存在的。 當觀察者自己的KVO合規性要求它調用一個取決于所觀察屬性的其中一個屬性的-willChange ...
方法時,可以使用預置通知。 通常的修改后通知太遲了,無法及時調用willChange ...
。
2. Context
addObserver:forKeyPath:options:context:
方法中的上下文指針包含將在相應的更改通知中傳回給觀察者的任意數據。 您可以指定NULL
并完全依賴于鍵路徑字符串來確定更改通知的來源,但是由于不同的原因,此方法可能會導致超類也遵循相同鍵路徑的對象的問題。
更安全和更可擴展的方法是使用上下文來確保您收到的通知注定給您的觀察者,而不是超類。
你的類中唯一命名的靜態變量的地址是一個很好的上下文。 在超類或子類中以類似方式選擇的上下文將不太可能重疊。 您可以為整個類選擇單個上下文,并依靠通知消息中的鍵路徑字符串來確定更改的內容。 或者,您可以為每個觀察到的鍵路徑創建一個獨特的上下文,這樣可以避免完全需要字符串比較,從而實現更有效的通知解析。 下面顯示了以這種方式選擇的balance
和interestRate
屬性的示例上下文。
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
下面的例子,展示的是一個Person對象是如何利用給定的上下文指針注冊自己作為Account
對象balance
和interestRate
屬性的觀察者的。
- (void)registerAsObserverForAccount:(Account*)account
{
[account addObserver:self
forKeyPath:@"balance"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountBalanceContext];
[account addObserver:self
forKeyPath:@"interestRate"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountInterestRateContext];
}
這里還要注意:鍵值觀察addObserver:forKeyPath:options:context:
方法不保持對觀察對象,被觀察對象或上下文的強引用。如果需要的話, 您應該確保在必要時保持對觀察,被觀察,對象和上下文的強烈引用。
接收Change的通知
當被觀察對象屬性的值發生變化時,觀察者會接收到observeValueForKeyPath:ofObject:change:context:
消息。 所有觀察者必須實現這種方法。
被觀察對象提供了鍵值路徑并觸發通知,并把自己作為關聯對象,還有關于改變的詳細內容的字典、當觀察者注冊此鍵值路徑時提供的上下文指針。
更改字典條目NSKeyValueChangeKindKey
提供有關發生的更改類型的信息。 如果觀察到的對象的值已更改,NSKeyValueChangeKindKey
條目將返回NSKeyValueChangeSetting
。 根據注冊觀察者時指定的選項,更改字典中的NSKeyValueChangeOldKey
和NSKeyValueChangeNewKey
條目包含更改前后的屬性值。 如果屬性是對象,則直接提供該值。 如果屬性是標量或C結構體,則該值將包裝在NSValue
對象中(與鍵值編碼KVC一樣)。
如果觀察到的屬性是一對多關系,NSKeyValueChangeKindKey
條目還指示關系中的對象是否分別插入,刪除或替換,分別返回NSKeyValueChangeInsertion
,NSKeyValueChangeRemoval
或NSKeyValueChangeReplacement
。
下面展示的是Person觀察者實現的方法observeValueForKeyPath:ofObject:change:context:
,并輸出balance
和interestRate
屬性代表的新舊值。
- (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];
}
}
當你注冊觀察者時指定的上下文是NULL
時,您將通知的鍵路徑與您觀察到的鍵路徑進行比較,以確定發生了什么變化。 如果您對所有觀察到的鍵路徑使用單個上下文,則首先根據通知的上下文測試,并找到匹配項,使用鍵路徑字符串比較來確定特定的更改。 如果您為每個鍵路徑提供了唯一的上下文,如下所示,一系列簡單的指針比較可以同時告知您該通知是否適用于此觀察者,如果是,則鍵路徑已更改。
在任何情況下,觀察者應該總是調用超類的實現observeValueForKeyPath:ofObject:change:context:
當它不識別上下文(或在簡單的情況下,任何鍵路徑)時,因為這意味著超類也已經注冊通知。
注意:如果通知傳播到類層次結構的頂部,NSObject
將拋出一個NSInternalInconsistencyException
,因為這是一個編程錯誤:一個子類不能使用它注冊的通知。
觀察者的移除
通過向被觀察對象發送消息removeObserver:forKeyPath:context:
,指定觀察對象、鍵路徑和上下文,從而移除鍵值觀察對象,下面列出的是Person對象移除自己作為balance
和interestRate
的觀察者。
- (void)unregisterAsObserverForAccount:(Account*)account
{
[account removeObserver:self
forKeyPath:@"balance"
context:PersonAccountBalanceContext];
[account removeObserver:self
forKeyPath:@"interestRate"
context:PersonAccountInterestRateContext];
}
接收到消息removeObserver:forKeyPath:context:
之后,觀察者不再收到任何指定的鍵和對象的observeValueForKeyPath:ofObject:change:context:
消息。
當移除觀察者時,還需要謹記幾個問題:
如果你在移除觀察者時,這個觀察者尚未注冊,就會導致
NSRangeException
錯誤,相對于方法addObserver:forKeyPath:options:context:
,你可以調用方法removeObserver:forKeyPath:context:
一次,或者如果這個在你的app里面是不可行的,那么就在try/catch block
里面處理這個潛在的錯誤和例外。當
dellocated
自己時,觀察者不會自動移除自己。 觀察到的對象繼續發送通知,忽視觀察者的狀態。 但是,像發送給已釋放對象的任何其他消息一樣,更改通知會觸發內存訪問異常。 因此,您可以確保觀察者在消失之前消除自己。如果該對象是觀察者或觀察者,則該協議無法詢問對象。 構建您的代碼以避免出現相關的錯誤。 典型的模式是在觀察者初始化期間(例如在
init
或viewDidLoad
中)注冊為觀察者,并在釋放(通常在dealloc
中)時注銷,確保正確配對和有序添加和刪除消息,并且觀察者要在自己在內存中釋放之前移除自己作為其他屬性或對象的觀察者。
后記
未完,待續~~~