1.KVO簡介
鍵值觀察是一種機制,它允許對象在其他對象的指定屬性發生更改時得到通知。為了了解鍵值觀察,必須首先了解鍵值編碼。
鍵值觀察提供了一種機制,允許對象在其他對象的特定屬性發生更改時得到通知。它對于應用程式中的模型層和控制器層之間的通信特别有用。控制器對象通常觀察模型對象的屬性,視圖對象通過控制器觀察模型對象的屬性。此外,模型對象可以觀察其他模型對象(通常用于确定依賴值何時發生變化),甚至可以觀察自身(同樣用于确定依賴值何時發生變化)。
也可以觀察屬性,包括簡單屬性、對一關系和對多關系。對多關系的觀察者被告知所做更改的類型,以及更改中涉及的對象。
一個簡單的例子說明了KVO如何在您的應用程式中發揮作用。假設一個Person對象與一個Account對象互動,該對象表示該個人在銀行的儲蓄帳戶。Person的一個執行個體可能需要知道帳戶執行個體的某些方面何時發生變化,比如餘額或利率。

如果這些屬性是Account的公共屬性,使用者可以通過定期輪詢帳戶來發現更改,但這是低效的,通常不切實際。更好的方法是使用KVO,當賬戶資訊有更改時,會通知使用者發生的更改。
要使用KVO,首先必須確定觀察到的對象(本例中的帳戶)是符合KVO的。通常,如果對象繼承自NSObject,并且以通常的方式建立屬性,那麼将自動符合KVO,當然也可以手動實作遵從性。
接下來,必須将觀察者執行個體Person注冊到觀察到的執行個體Account中。Person向帳戶發送一個addObserver:forKeyPath:options:context:消息,對于每個觀察到的密鑰路徑隻發送一次,并将自己命名為觀察者。
為了接收來自帳戶的更改通知,所有觀察者都需要(Person)實作observeValueForKeyPath:ofObject:change:context:方法。每當注冊的密鑰路徑發生更改時,該帳戶将向該人員發送此消息。然後,該人員可以根據更改通知采取适當的操作。
最後,當它不再需要觀察時,并且至少在釋放通知之前,Person執行個體必須通過向帳戶發送消息removeObserver:forKeyPath:來登出。
上面是KVO實作的一個邏輯流程.
2.KVO的使用
1.使用addObserver:forKeyPath:options:context:方法将觀察者注冊到觀察對象中。
- 1.options參數被指定為按位或選項常量,影響通知中提供的更改字典的内容和生成通知的方式.
NSKeyValueObservingOptionNew //更改字典應包含被監聽屬性的新值 NSKeyValueObservingOptionOld //更改字典應包含被監聽屬性的舊值 NSKeyValueObservingOptionInitial //當指定了這個選項時: 在addObserver:forKeyPath:options:context:消息被發出去後,甚至不用等待這個消息傳回,觀察者會馬上收到一個通知。 觀察者隻會收到一次該通知,這可以用來确定被觀察屬性的初始值。 當同時指定New | Old | OptionInitial這3個選項時,觀察者接收到的change字典中隻會包含新值,而不會包含舊值。但是這個值對于觀察者來說是新的。 NSKeyValueObservingOptionPrior //當指定了這個選項時: 在被觀察的屬性改變前,觀察者就會收到一個通知(一般的通知發出時機都是在屬性改變後,雖然change字典中包含了新值和舊值,但是通知還是在屬性改變後才發出), change會包含一個NSKeyValueChangeNotificationIsPriorKey的鍵,值為一個NSNumber類型的YES。同時指定OptionPrior | New | Old時,change字典會包含舊值而不會包含新值。 你可以在這個通知中調用- (void)willChangeValueForKey:(NSString *)key;
- 2.context 上下文參數
1.可以設值為NULL,但是這樣會存在一些問題,該對象的父類也會因為不同的原因觀察相同的鍵路徑. 舉個例子:A類同時觀察了B類和C類中相同名稱屬性的值的改變,那麼使用相同的keyPath就沒有那麼友善與安全了! 2.更安全,可擴充的使用方法是使用類中唯一命名的靜态變量的位址作為上下文,可以為整個類選擇一個上下文, 并依賴于通知消息中的鍵路徑字元串來确定更改了什麼 也可以為每個觀察到的鍵路徑建立不同的上下文,進而完全繞過字元串比較的需要,進而實作更有效的通知解析。
- 3.注意
觀察鍵值的addObserver:forKeyPath:options:context:方法不維護對觀察對象、觀察對象或上下文的強引用。 確定在必要時維護對觀察對象、被觀察對象和上下文的強引用。
- 4.示例:
//上下文 static void *PersonAccountBalanceContext = &PersonAccountBalanceContext; static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext; //注冊觀察者 - (void)registerAsObserverForAccount:(Account*)account { [account addObserver:self forKeyPath:@"balance" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:PersonAccountBalanceContext]; [account addObserver:self forKeyPath:@"interestRate" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:PersonAccountInterestRateContext]; }
2.實作observeValueForKeyPath:ofObject:change:context:在觀察者内部接受更改通知消息。
- 1.當觀察的對象屬性值發生變化時,觀察者會收到observeValueForKeyPath:ofObject:change:context:消息。所有觀察者都必須實作這個方法。
- 2.keyPath:觀察對象提供觸發通知的鍵路徑; object:本身作為相關的對象,change:一個包含有關更改的詳細資訊的字典,context:當觀察者為這個鍵路徑注冊時提供的上下文指針。
- 3.change字典五個常量鍵的含義:
NSString *const NSKeyValueChangeKindKey; //提供了有關所發生更改的類型的資訊。 //如果觀察到的對象的值改變了,NSKeyValueChangeKindKey傳回值NSKeyValueChangeSetting。 //根據觀察者注冊時指定的選項,change字典中的NSKeyValueChangeOldKey和NSKeyValueChangeNewKey包含了變更之前和之後屬性的值。 //如果屬性是對象,則直接提供值。如果屬性是标量或C結構,則值被包裝在NSValue對象中(與鍵值編碼一樣)。 //如果觀察到的屬性是一對多關系,NSKeyValueChangeKindKey還記錄了被觀察的對象是被插入(NSKeyValueChangeInsertion)、删除(NSKeyValueChangeRemoval),還是被替換(NSKeyValueChangeReplacement)。 NSString *const NSKeyValueChangeNewKey; // 被觀察屬性改變後新值的key,當觀察的屬性為一個集合對象, //且NSKeyValueChangeKindKey不為NSKeyValueChangeSetting時, //該值傳回的是一個數組,包含插入,替換後的新值(删除操作不會傳回新值) NSString *const NSKeyValueChangeOldKey; //被觀察屬性改變前舊值的key,當觀察的屬性為一個集合對象, //且NSKeyValueChangeKindKey不為NSKeyValueChangeSetting時, //該值傳回的是一個數組,包含删除,替換前的舊值(插入操作不會傳回舊值) NSString *const NSKeyValueChangeIndexesKey; //如果NSKeyValueChangeKindKey的值為NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, 或者 NSKeyValueChangeReplacement, //這個鍵的值是一個NSIndexSet對象,包含了增加,移除或者替換對象的index。 NSString *const NSKeyValueChangeNotificationIsPriorKey; //如果注冊觀察者時options指明了NSKeyValueObservingOptionPrior, //change字典中就會帶有這個key,值為NSNumber類型的YES.
- 4.示例:Person觀察者的observeValueForKeyPath:ofObject:change:context:的實作,該觀察者記錄屬性balance和interestRate的新舊值。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context==PersonAccountBalanceContext) { //餘額改變的監聽 } else if (context == PersonAccountInterestRateContext) { //利率改變的監聽 } else { //其他無法識别的context [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } }
- 5.如果在注冊觀察者時指定上下文為NULL,那麼就需要将通知的鍵路徑與正在觀察的鍵路徑進行比較,來确定發生了什麼變化。如果對所有觀察到的鍵路徑使用單一上下文,那麼首先要根據通知的上下文對其進行測試,并找到比對項,然後使用鍵路徑字元串比較來确定具體更改了哪些内容。如果是為每個鍵路徑提供了惟一的上下文,就像示例一樣,隻需要比較上下文指針來确定更改的建路徑的值。
- 6.在任何情況下,如果context(鍵路徑)是一個無法識别的上下文(鍵路徑),觀察者應該調用observeValueForKeyPath:ofObject:change:context的父類實作,因為這意味着一個父類也注冊了該通知。
3.使用removeObserver:forKeyPath方法登出觀察者:當它不再應該接收消息時。至少,在觀察者從記憶體釋放之前調用這個方法。
- 1.如果注冊了觀察者,那麼就需要在觀察者記憶體釋放之前移除它!
- 2.通過向觀察對象發送removeObserver:forKeyPath:context:消息,指定觀察對象、鍵路徑和上下文,可以删除鍵值觀察者。
- 3.在接收到removeObserver:forKeyPath:context:消息後,觀察對象将不再接收到任何指定鍵值路徑和上下文的observeValueForKeyPath:ofObject:change:context:消息。
- 4.移除觀察者時,要記住以下幾點:
1.當你向一個不是觀察者的對象發送remove消息的時候(或者你重複發送remove消息時,再或者在注冊觀察者前就調用了remove), 則會抛出一個NSRangeException異常,是以,保險的做法是,把remove操作放在try/catch中。 2.一個監聽者在其被銷毀時,并不會自己登出監聽,而給一個已經銷毀的監聽者發送通知,會造成野指針錯誤。是以至少保證,在監聽者被釋放前,将其監聽登出。保證有一個add方法,就有一個remove方法 3.觀察者在釋放時不會自動移除自身。被觀察的對象發送通知的時候,不會理會觀察者的狀态。 是以與發送到已釋放對象的任何其他消息一樣,更改通知會觸發記憶體通路異常。 是以,要確定觀察者在從記憶體中消失之前删除自己。 4.該協定沒有提供詢問對象是否是觀察者或被觀察者的方法,是以我們添加觀察者與移除觀察者需要成對出現。 典型的模式是: 在觀察者的初始化過程中注冊觀察者(例如在init或viewDidLoad中), 在deallocation過程中取消注冊(通常在dealloc中), 確定正确地比對和有序地添加和删除消息,并且在觀察者從記憶體中釋放之前取消注冊。 - (void)unregisterAsObserverForAccount:(Account*)account { [account removeObserver:self forKeyPath:@"balance" context:PersonAccountBalanceContext]; [account removeObserver:self forKeyPath:@"interestRate" context:PersonAccountInterestRateContext]; }
觸發KVO的兩種方式:
1.自動觸發
了解了KVC,本質上是要調用setter,KVO是基于KVC實作的,是以我們想要觸發KVO,就需要調用被觀察對象的setter:
//以下四種方法均可觸發KVO發送通知
[account setName:@"Savings"];
[account setValue:@"Savings" forKey:@"name"];
[document setValue:@"Savings" forKeyPath:@"account.name"];
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
//mutableArrayValueForKey:也是KVC的方法!如果用KVO監聽了一個集合對象(比如一個數組),
//如果使用addObject:發送消息,是不會觸發KVO通知的。
//但是通過mutableArrayValueForKey:這個方法對集合對象進行的相關操作(增加,删除,替換元素)就會觸發KVO通知。(具體為什麼,後期文章會說到)
2.手動觸發
重寫NSObject類方法:+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey,根據theKey來判斷對應鍵值路徑的屬性是否開啟自動通知。這樣我們可以靈活的區分哪些被觀察屬性需要監聽,哪些不需要監聽。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
要實作手動的觸發通知,我們還需要實作:
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
這樣就基本實作了一個KVO的手動通知,當該屬性值改變時,觀察者就能收到改變通知了。