
本文主要內(nèi)容來自于對官方文檔 Key-Value Observing Programming Guide 的翻譯,以及一部分我自己的理解和解釋,如果有說錯的地方請及時聯(lián)系我。
At a Glance
KVO 也就是 鍵值觀察 ,它提供了一種機(jī)制,使得當(dāng)某個對象特定的屬性發(fā)生改變時能夠通知到別的對象。這經(jīng)常用于 model 和 controller 之間的通信。KVO主要的優(yōu)點(diǎn)是你不需要在每次屬性改變時手動去發(fā)送通知。并且它支持為一個屬性注冊多個觀察者。
注冊 KVO
- 被觀察對象 的屬性必須是 [KVO Compliant](file:///Users/hcy/Library/Developer/Shared/Documentation/DocSets/com.apple.adc.documentation.iOS.docset/Contents/Resources/Documents/documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVOCompliance.html#//apple_ref/doc/uid/20002178-BAJEAIEE)
- 必須用 被觀察對象 的
addObserver:forKeyPath:options:context:方法注冊觀察者 -
觀察者 必須實(shí)現(xiàn)
observeValueForKeyPath:ofObject:change:context:方法
注冊成為觀察者
為了能夠在屬性改變時被通知到,一個 觀察者對象 必須通過 被觀察對象 的addObserver:forKeyPath:options:context: 方法注冊成為觀察者。
observer參數(shù)也就是一個觀察者對象keyPath表示要觀察的屬性-
options決定了提供給觀察者change字典中的具體信息有哪些。(change字典是一個提供給觀察者的參數(shù),后面會提到)-
NSKeyValueObservingOptionOld表示在change字典中包含了改變前的值。 -
NSKeyValueObservingOptionNew表示在change字典中包含新的值。 -
NSKeyValueObservingOptionInitial在注冊觀察者的方法return的時候就會發(fā)出一次通知。 -
NSKeyValueObservingOptionPrior會在值發(fā)生改變前發(fā)出一次通知,當(dāng)然改變后的通知依舊還會發(fā)出,也就是每次change都會有兩個通知。
-
context這個參數(shù)可以是一個 C指針,也可以是一個 對象引用,它可以作為這個context的唯一標(biāo)識,也可以提供一些數(shù)據(jù)給觀察者。
注意:
addObserver:forKeyPath:options:context:方法不會持有觀察者對象,被觀察對象,以及context的強(qiáng)引用。你要確保自己持有了他們的強(qiáng)引用。
屬性變化時接收通知
當(dāng)一個被觀察屬性的值發(fā)生改變時,觀察者會收到 observeValueForKeyPath:ofObject:change:context: 的消息。所有的觀察者必須實(shí)現(xiàn)這個方法。這個方法中的參數(shù)和注冊觀察者方法的參數(shù)基本相同,只有一個 change 不同。 change 是一個字典,它里面包含了的信息由注冊時的 options 決定。
官方提供了這些key給我們來取到 change 中的value:
NSString *const NSKeyValueChangeKindKey;
NSString *const NSKeyValueChangeNewKey;
NSString *const NSKeyValueChangeOldKey;
NSString *const NSKeyValueChangeIndexesKey;
NSString *const NSKeyValueChangeNotificationIsPriorKey;
-
NSKeyValueChangeKindKey這個key包含的value是一個 NSNumber 里面是一個 int,與之對應(yīng)的是NSKeyValueChange的枚舉
enum {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;
當(dāng) change[NSKeyValueChangeKindKey] 是 NSKeyValueChangeSetting 的時候,說明被觀察屬性的setter方法被調(diào)用了。
而下面三種,根據(jù)官方文檔的意思是,當(dāng)被觀察屬性是集合類型,且對它進(jìn)行了 insert,remove,replace 操作的時候會返回這三種Key,但是我自己測試的時候沒有測試出來??不知道是不是我理解錯了。
NSKeyValueChangeNewKey,NSKeyValueChangeOldKey顧名思義,當(dāng)你在注冊的時候options參數(shù)中填了對應(yīng)的NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld,并且NSKeyValueChangeKindKey的值是NSKeyValueChangeSetting,你就可以通過這兩個key取到 舊值和新值。NSKeyValueChangeIndexesKey, 當(dāng)NSKeyValueChangeKindKey的結(jié)果是NSKeyValueChangeInsertion,NSKeyValueChangeRemoval或NSKeyValueChangeReplacement的時候,這個key的value是一個NSIndexSet,包含了發(fā)生insert,remove,replace的對象的索引集合NSKeyValueChangeNotificationIsPriorKey,這個key包含了一個 NSNumber,里面是一個布爾值,如果在注冊時options中有NSKeyValueObservingOptionPrior,那么在前一個通知中的change中就會有這個key的value, 我們可以這樣來判斷是不是在改變前的通知[change[NSKeyValueChangeNotificationIsPriorKey] boolValue] == YES;
移除一個觀察者
你可以通過 removeObserver:forKeyPath: 方法來移除一個觀察。如果你的 context 是一個 對象,你必須在移除觀察之前持有它的強(qiáng)引用。當(dāng)移除了觀察后,觀察者對象再也不會受到這個 keyPath 的通知。
KVO Compliance
有兩種方式能夠保證 change notification 能夠被發(fā)出。
- 自動通知,繼承自NSObject,并且所有的屬性符合[KVC規(guī)范](file:///Users/hcy/Library/Developer/Shared/Documentation/DocSets/com.apple.adc.documentation.iOS.docset/Contents/Resources/Documents/documentation/Cocoa/Conceptual/KeyValueCoding/Articles/Compliant.html#//apple_ref/doc/uid/20002172)這樣就不用寫額外的代碼去實(shí)現(xiàn)自動通知。
- 手動通知,讓你的子類實(shí)現(xiàn)
automaticallyNotifiesObserversForKey:方法,來決定是否需要自動通知,如果是手動通知需要額外的代碼。
自動通知
NSObject 已經(jīng)實(shí)現(xiàn)了自動通知,只要通過 setter 方法去賦值,或者通過 KVC 就可以通知到觀察者。自動通知也支持集合代理對象,比如 mutableArrayValueForKey: 方法。
// Call the accessor method.
[account setName:@"Savings"];
// Use setValue:forKey:.
[account setValue:@"Savings" forKey:@"name"];
// Use a key path, where 'account' is a kvc-compliant property of 'document'.
[document setValue:@"Savings" forKeyPath:@"account.name"];
// Use mutableArrayValueForKey: to retrieve a relationship proxy object.
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
手動通知
手動通知提供了更自由的方式去決定什么時間,什么方式去通知觀察者。這可以幫助你最少限度觸發(fā)不必要的通知,或者一組改變值發(fā)出一個通知。想要使用手動通知必須實(shí)現(xiàn)automaticallyNotifiesObserversForKey: 方法。(或者automaticallyNotifiesObserversOfS<Key>)在一個類中同時使用自動和手動通知是可行的。對于想要手動通知的屬性,可以根據(jù)它的keyPath返回NO,而其對于其他位置的keyPath,要返回父類的這個方法。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"openingBalance"]) {
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
要實(shí)現(xiàn)手動通知,你需要在值改變前調(diào)用 willChangeValueForKey: 方法,在值改變后調(diào)用 didChangeValueForKey: 方法。你可以在發(fā)送通知前檢查值是否改變,如果沒有改變就不發(fā)送通知
- (void)setOpeningBalance:(double)theBalance {
if (theBalance != _openingBalance) {
[self willChangeValueForKey:@"openingBalance"];
_openingBalance = theBalance;
[self didChangeValueForKey:@"openingBalance"];
}
}
如果一個操作會導(dǎo)致多個屬性改變,你需要嵌套通知,像下面這樣:
- (void)setOpeningBalance:(double)theBalance {
[self willChangeValueForKey:@"openingBalance"];
[self willChangeValueForKey:@"itemChanged"];
_openingBalance = theBalance;
_itemChanged = _itemChanged+1;
[self didChangeValueForKey:@"itemChanged"];
[self didChangeValueForKey:@"openingBalance"];
}
在一個一對多的關(guān)系中,你必須注意不僅僅是這個key改變了,還有它改變的類型以及索引。
- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
[self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"];
// Remove the transaction objects at the specified indexes.
[self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"];
}
鍵之間的依賴
在很多種情況下一個屬性的值依賴于在其他對象中的屬性。如果一個依賴屬性的值改變了,這個屬性也需要被通知到。
To-one Relationships
比如有一個教 fullName 的屬性,依賴于 firstName 和 lastName,當(dāng) firstName 或者 lastName 改變時,這個 fullName 屬性需要被通知到。
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
你可以重寫 keyPathsForValuesAffectingValueForKey: 方法。其中要先調(diào)父類的這個方法拿到一個set,再做接下來的操作。
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
你也可以通過實(shí)現(xiàn) keyPathsForValuesAffecting<Key> 方法來達(dá)到前面同樣的效果,這里的<Key>就是屬性名,不過第一個字母要大寫,用前面的例子來說就是這樣:
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
To-many Relationships
keyPathsForValuesAffectingValueForKey:方法不能支持 to-many 的關(guān)系。舉個例子,比如你有一個 Department 對象,和很多個 Employee 對象。而 Employee 有一個 salary 屬性。你可能希望 Department 對象有一個 totalSalary 的屬性,依賴于所有的 Employee 的 salary 。
你可以注冊 Department 成為所有 Employee 的觀察者。當(dāng) Employee 被添加或者被移除時,你必須要添加和移除觀察者。然后在 observeValueForKeyPath:ofObject:change:context: 方法中,根據(jù)改變做出反饋。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == totalSalaryContext) {
[self updateTotalSalary];
}
else
// deal with other observations and/or invoke super...
}
- (void)updateTotalSalary {
[self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
}
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
if (totalSalary != newTotalSalary) {
[self willChangeValueForKey:@"totalSalary"];
_totalSalary = newTotalSalary;
[self didChangeValueForKey:@"totalSalary"];
}
}
- (NSNumber *)totalSalary {
return _totalSalary;
}
KVO的實(shí)現(xiàn)細(xì)節(jié)
KVO 的實(shí)現(xiàn)用了一種叫 isa-swizzling 的技術(shù)。isa 指針就是指向類的指針,當(dāng)一個對象的一個屬性注冊了觀察者后,被觀察對象的isa指針的就指向了一個系統(tǒng)為我們生成的中間類,而不是我們自己創(chuàng)建的類。在這個類中,系統(tǒng)為我們重寫了被觀察屬性的setter方法。你可以通過 object_getClass(id obj) 方法獲得對象真實(shí)的類,在 addObserver 前后分別打印,就可以看到isa指針被指向了一個中間類。似乎都是在原來的類名前面加上 NSKVONotifying_
isa指針不總是指向真實(shí)的類,所以你不應(yīng)該依賴于 isa 指針來判斷這個對象的類型,而應(yīng)該通過 class 方法來判斷對象的類型。如果你還不知道什么是isa指針,可以看我之前寫的博客 Objective-C runtime 的簡單理解與使用