KVO解析(二) —— 一個簡單的KVO實現

版本記錄

版本號 時間
V1.0 2017.09.14

前言

KVO具有更強大的功能,是蘋果給我們的一個回調機制,在某個對象注冊監聽者后,在被監聽的對象發生改變時,對象會發送一個通知給監聽者,以便監聽者執行回調操作。接下來幾篇就詳細的解析一下KVO。感興趣的可以看上面幾篇。
1. KVO解析(一) —— 基本了解

鍵值觀察步驟

為確保對象可以接受鍵值觀察的通知,必須執行如下步驟:

還要注意:并非所有類都符合所有屬性的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并完全依賴于鍵路徑字符串來確定更改通知的來源,但是由于不同的原因,此方法可能會導致超類也遵循相同鍵路徑的對象的問題。

更安全和更可擴展的方法是使用上下文來確保您收到的通知注定給您的觀察者,而不是超類。

你的類中唯一命名的靜態變量的地址是一個很好的上下文。 在超類或子類中以類似方式選擇的上下文將不太可能重疊。 您可以為整個類選擇單個上下文,并依靠通知消息中的鍵路徑字符串來確定更改的內容。 或者,您可以為每個觀察到的鍵路徑創建一個獨特的上下文,這樣可以避免完全需要字符串比較,從而實現更有效的通知解析。 下面顯示了以這種方式選擇的balanceinterestRate屬性的示例上下文。

static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;

下面的例子,展示的是一個Person對象是如何利用給定的上下文指針注冊自己作為Account對象balanceinterestRate屬性的觀察者的。

- (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。 根據注冊觀察者時指定的選項,更改字典中的NSKeyValueChangeOldKeyNSKeyValueChangeNewKey條目包含更改前后的屬性值。 如果屬性是對象,則直接提供該值。 如果屬性是標量或C結構體,則該值將包裝在NSValue對象中(與鍵值編碼KVC一樣)。

如果觀察到的屬性是一對多關系,NSKeyValueChangeKindKey條目還指示關系中的對象是否分別插入,刪除或替換,分別返回NSKeyValueChangeInsertionNSKeyValueChangeRemovalNSKeyValueChangeReplacement。

下面展示的是Person觀察者實現的方法observeValueForKeyPath:ofObject:change:context:,并輸出balanceinterestRate屬性代表的新舊值。

- (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對象移除自己作為balanceinterestRate的觀察者。

- (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自己時,觀察者不會自動移除自己。 觀察到的對象繼續發送通知,忽視觀察者的狀態。 但是,像發送給已釋放對象的任何其他消息一樣,更改通知會觸發內存訪問異常。 因此,您可以確保觀察者在消失之前消除自己。

  • 如果該對象是觀察者或觀察者,則該協議無法詢問對象。 構建您的代碼以避免出現相關的錯誤。 典型的模式是在觀察者初始化期間(例如在initviewDidLoad中)注冊為觀察者,并在釋放(通常在dealloc中)時注銷,確保正確配對和有序添加和刪除消息,并且觀察者要在自己在內存中釋放之前移除自己作為其他屬性或對象的觀察者。

后記

未完,待續~~~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容