本文主要內(nèi)容來(lái)自于對(duì)官方文檔 Key-Value Observing Programming Guide 的翻譯担敌,以及一部分我自己的理解和解釋?zhuān)绻姓f(shuō)錯(cuò)的地方請(qǐng)及時(shí)聯(lián)系我啼器。
At a Glance
KVO 也就是 鍵值觀察 考抄,它提供了一種機(jī)制危喉,使得當(dāng)某個(gè)對(duì)象特定的屬性發(fā)生改變時(shí)能夠通知到別的對(duì)象摩幔。這經(jīng)常用于 model 和 controller 之間的通信藻茂。KVO主要的優(yōu)點(diǎn)是你不需要在每次屬性改變時(shí)手動(dòng)去發(fā)送通知凸郑。并且它支持為一個(gè)屬性注冊(cè)多個(gè)觀察者裳食。
注冊(cè) KVO
- 被觀察對(duì)象 的屬性必須是 [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)
- 必須用 被觀察對(duì)象 的
addObserver:forKeyPath:options:context:
方法注冊(cè)觀察者 -
觀察者 必須實(shí)現(xiàn)
observeValueForKeyPath:ofObject:change:context:
方法
注冊(cè)成為觀察者
為了能夠在屬性改變時(shí)被通知到,一個(gè) 觀察者對(duì)象 必須通過(guò) 被觀察對(duì)象 的addObserver:forKeyPath:options:context:
方法注冊(cè)成為觀察者芙沥。
observer
參數(shù)也就是一個(gè)觀察者對(duì)象keyPath
表示要觀察的屬性-
options
決定了提供給觀察者change字典中的具體信息有哪些诲祸。(change字典是一個(gè)提供給觀察者的參數(shù),后面會(huì)提到)-
NSKeyValueObservingOptionOld
表示在change字典中包含了改變前的值而昨。 -
NSKeyValueObservingOptionNew
表示在change字典中包含新的值救氯。 -
NSKeyValueObservingOptionInitial
在注冊(cè)觀察者的方法return的時(shí)候就會(huì)發(fā)出一次通知。 -
NSKeyValueObservingOptionPrior
會(huì)在值發(fā)生改變前發(fā)出一次通知歌憨,當(dāng)然改變后的通知依舊還會(huì)發(fā)出着憨,也就是每次change都會(huì)有兩個(gè)通知。
-
context
這個(gè)參數(shù)可以是一個(gè) C指針务嫡,也可以是一個(gè) 對(duì)象引用甲抖,它可以作為這個(gè)context的唯一標(biāo)識(shí),也可以提供一些數(shù)據(jù)給觀察者心铃。
注意:
addObserver:forKeyPath:options:context:
方法不會(huì)持有觀察者對(duì)象准谚,被觀察對(duì)象,以及context的強(qiáng)引用去扣。你要確保自己持有了他們的強(qiáng)引用柱衔。
屬性變化時(shí)接收通知
當(dāng)一個(gè)被觀察屬性的值發(fā)生改變時(shí),觀察者會(huì)收到 observeValueForKeyPath:ofObject:change:context:
的消息厅篓。所有的觀察者必須實(shí)現(xiàn)這個(gè)方法秀存。這個(gè)方法中的參數(shù)和注冊(cè)觀察者方法的參數(shù)基本相同,只有一個(gè) change
不同羽氮。 change
是一個(gè)字典或链,它里面包含了的信息由注冊(cè)時(shí)的 options
決定。
官方提供了這些key給我們來(lái)取到 change
中的value:
NSString *const NSKeyValueChangeKindKey;
NSString *const NSKeyValueChangeNewKey;
NSString *const NSKeyValueChangeOldKey;
NSString *const NSKeyValueChangeIndexesKey;
NSString *const NSKeyValueChangeNotificationIsPriorKey;
-
NSKeyValueChangeKindKey
這個(gè)key包含的value是一個(gè) NSNumber 里面是一個(gè) int档押,與之對(duì)應(yīng)的是NSKeyValueChange
的枚舉
enum {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;
當(dāng) change[NSKeyValueChangeKindKey]
是 NSKeyValueChangeSetting
的時(shí)候澳盐,說(shuō)明被觀察屬性的setter方法被調(diào)用了祈纯。
而下面三種,根據(jù)官方文檔的意思是叼耙,當(dāng)被觀察屬性是集合類(lèi)型腕窥,且對(duì)它進(jìn)行了 insert,remove筛婉,replace 操作的時(shí)候會(huì)返回這三種Key簇爆,但是我自己測(cè)試的時(shí)候沒(méi)有測(cè)試出來(lái)??不知道是不是我理解錯(cuò)了。
NSKeyValueChangeNewKey
爽撒,NSKeyValueChangeOldKey
顧名思義入蛆,當(dāng)你在注冊(cè)的時(shí)候options
參數(shù)中填了對(duì)應(yīng)的NSKeyValueObservingOptionNew
和NSKeyValueObservingOptionOld
,并且NSKeyValueChangeKindKey
的值是NSKeyValueChangeSetting
硕勿,你就可以通過(guò)這兩個(gè)key取到 舊值和新值哨毁。NSKeyValueChangeIndexesKey
, 當(dāng)NSKeyValueChangeKindKey
的結(jié)果是NSKeyValueChangeInsertion
,NSKeyValueChangeRemoval
或NSKeyValueChangeReplacement
的時(shí)候源武,這個(gè)key的value是一個(gè)NSIndexSet扼褪,包含了發(fā)生insert,remove粱栖,replace的對(duì)象的索引集合NSKeyValueChangeNotificationIsPriorKey
话浇,這個(gè)key包含了一個(gè) NSNumber,里面是一個(gè)布爾值查排,如果在注冊(cè)時(shí)options
中有NSKeyValueObservingOptionPrior
凳枝,那么在前一個(gè)通知中的change
中就會(huì)有這個(gè)key的value, 我們可以這樣來(lái)判斷是不是在改變前的通知[change[NSKeyValueChangeNotificationIsPriorKey] boolValue] == YES;
移除一個(gè)觀察者
你可以通過(guò) removeObserver:forKeyPath:
方法來(lái)移除一個(gè)觀察跋核。如果你的 context
是一個(gè) 對(duì)象,你必須在移除觀察之前持有它的強(qiáng)引用叛买。當(dāng)移除了觀察后砂代,觀察者對(duì)象再也不會(huì)受到這個(gè) keyPath 的通知。
KVO Compliance
有兩種方式能夠保證 change notification 能夠被發(fā)出率挣。
- 自動(dòng)通知刻伊,繼承自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)這樣就不用寫(xiě)額外的代碼去實(shí)現(xiàn)自動(dòng)通知椒功。
- 手動(dòng)通知捶箱,讓你的子類(lèi)實(shí)現(xiàn)
automaticallyNotifiesObserversForKey:
方法,來(lái)決定是否需要自動(dòng)通知动漾,如果是手動(dòng)通知需要額外的代碼丁屎。
自動(dòng)通知
NSObject 已經(jīng)實(shí)現(xiàn)了自動(dòng)通知,只要通過(guò) setter 方法去賦值旱眯,或者通過(guò) KVC 就可以通知到觀察者晨川。自動(dòng)通知也支持集合代理對(duì)象证九,比如 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];
手動(dòng)通知
手動(dòng)通知提供了更自由的方式去決定什么時(shí)間共虑,什么方式去通知觀察者愧怜。這可以幫助你最少限度觸發(fā)不必要的通知,或者一組改變值發(fā)出一個(gè)通知妈拌。想要使用手動(dòng)通知必須實(shí)現(xiàn)automaticallyNotifiesObserversForKey:
方法拥坛。(或者automaticallyNotifiesObserversOfS<Key>
)在一個(gè)類(lèi)中同時(shí)使用自動(dòng)和手動(dòng)通知是可行的。對(duì)于想要手動(dòng)通知的屬性尘分,可以根據(jù)它的keyPath返回NO猜惋,而其對(duì)于其他位置的keyPath,要返回父類(lèi)的這個(gè)方法音诫。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"openingBalance"]) {
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
要實(shí)現(xiàn)手動(dòng)通知惨奕,你需要在值改變前調(diào)用 willChangeValueForKey:
方法,在值改變后調(diào)用 didChangeValueForKey:
方法竭钝。你可以在發(fā)送通知前檢查值是否改變梨撞,如果沒(méi)有改變就不發(fā)送通知
- (void)setOpeningBalance:(double)theBalance {
if (theBalance != _openingBalance) {
[self willChangeValueForKey:@"openingBalance"];
_openingBalance = theBalance;
[self didChangeValueForKey:@"openingBalance"];
}
}
如果一個(gè)操作會(huì)導(dǎo)致多個(gè)屬性改變,你需要嵌套通知香罐,像下面這樣:
- (void)setOpeningBalance:(double)theBalance {
[self willChangeValueForKey:@"openingBalance"];
[self willChangeValueForKey:@"itemChanged"];
_openingBalance = theBalance;
_itemChanged = _itemChanged+1;
[self didChangeValueForKey:@"itemChanged"];
[self didChangeValueForKey:@"openingBalance"];
}
在一個(gè)一對(duì)多的關(guān)系中卧波,你必須注意不僅僅是這個(gè)key改變了,還有它改變的類(lèi)型以及索引庇茫。
- (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"];
}
鍵之間的依賴(lài)
在很多種情況下一個(gè)屬性的值依賴(lài)于在其他對(duì)象中的屬性港粱。如果一個(gè)依賴(lài)屬性的值改變了,這個(gè)屬性也需要被通知到旦签。
To-one Relationships
比如有一個(gè)教 fullName
的屬性查坪,依賴(lài)于 firstName
和 lastName
,當(dāng) firstName
或者 lastName
改變時(shí)宁炫,這個(gè) fullName
屬性需要被通知到偿曙。
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
你可以重寫(xiě) keyPathsForValuesAffectingValueForKey:
方法。其中要先調(diào)父類(lèi)的這個(gè)方法拿到一個(gè)set羔巢,再做接下來(lái)的操作望忆。
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
你也可以通過(guò)實(shí)現(xiàn) keyPathsForValuesAffecting<Key>
方法來(lái)達(dá)到前面同樣的效果,這里的<Key>就是屬性名竿秆,不過(guò)第一個(gè)字母要大寫(xiě)启摄,用前面的例子來(lái)說(shuō)就是這樣:
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
To-many Relationships
keyPathsForValuesAffectingValueForKey:
方法不能支持 to-many 的關(guān)系。舉個(gè)例子幽钢,比如你有一個(gè) Department 對(duì)象歉备,和很多個(gè) Employee 對(duì)象。而 Employee 有一個(gè) salary 屬性搅吁。你可能希望 Department 對(duì)象有一個(gè) totalSalary 的屬性威创,依賴(lài)于所有的 Employee 的 salary 落午。
你可以注冊(cè) Department 成為所有 Employee 的觀察者。當(dāng) Employee 被添加或者被移除時(shí)肚豺,你必須要添加和移除觀察者溃斋。然后在 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 指針就是指向類(lèi)的指針,當(dāng)一個(gè)對(duì)象的一個(gè)屬性注冊(cè)了觀察者后截碴,被觀察對(duì)象的isa指針的就指向了一個(gè)系統(tǒng)為我們生成的中間類(lèi)梳侨,而不是我們自己創(chuàng)建的類(lèi)。在這個(gè)類(lèi)中日丹,系統(tǒng)為我們重寫(xiě)了被觀察屬性的setter方法走哺。你可以通過(guò) object_getClass(id obj)
方法獲得對(duì)象真實(shí)的類(lèi),在 addObserver 前后分別打印,就可以看到isa指針被指向了一個(gè)中間類(lèi)哲虾。似乎都是在原來(lái)的類(lèi)名前面加上 NSKVONotifying_
isa指針不總是指向真實(shí)的類(lèi)丙躏,所以你不應(yīng)該依賴(lài)于 isa 指針來(lái)判斷這個(gè)對(duì)象的類(lèi)型,而應(yīng)該通過(guò) class
方法來(lái)判斷對(duì)象的類(lèi)型束凑。如果你還不知道什么是isa指針晒旅,可以看我之前寫(xiě)的博客 Objective-C runtime 的簡(jiǎn)單理解與使用