Introduction
Key-value observing
(KVO) 是一種機(jī)制,它允許對(duì)象在 更改其他對(duì)象的指定屬性時(shí) 得到通知。
At a Glance
在應(yīng)用中在 model
和 controller
層間通訊非常有用(在 OS X中, controller
層 binding
技術(shù)嚴(yán)重依賴 KVO), controller
同時(shí)觀察 model
的屬性, view
通過(guò) controller
觀察 model
的屬性. 另外, model
可能觀察其他 model
甚至是 model
本身(通常由依賴值決定).
可以觀察包括簡(jiǎn)單的屬性, to-one
and to-many
關(guān)系. to-many
的觀察者被通知 change 的類型就像改變涉及的值一樣.
簡(jiǎn)單例子, 假設(shè) Person
有一個(gè) Account
屬性, 表現(xiàn)為 person
保存 account
到銀行. Person
實(shí)例可能需要知道 Account
實(shí)例的某些方面何時(shí)發(fā)生更改弥咪,比如余額或利率涕癣。
如果 Account
的屬性時(shí)公有屬性, 則 Person
可以通過(guò)輪詢查看 changes
, 這種方式顯然低效, 更好的方法是使用 KVO, 在 change
時(shí), 類似 Person
接收一個(gè)分岔.
使用 KVO, 必須確定被觀察對(duì)象, Account
是否符合. 通常, 如果對(duì)象繼承 NSObject
, 使用一般方式創(chuàng)建的屬性, 都自動(dòng)符合 KVO, 也可以手動(dòng)實(shí)現(xiàn)遵從性. KVO Compliance 描述了自動(dòng)和手動(dòng) KVO 以及怎樣實(shí)現(xiàn).
下一步, 必須注冊(cè)觀察者實(shí)例, 例如 Person
, 被觀察者 Account
, Person
發(fā)送一個(gè) addObserver:forKeyPath:options:context:
消息給 Account
, 為每個(gè)被觀察的 key-path
仗岸,并將自己命名為觀察者耿焊。
為了接收 Account
改變的消息, Person
實(shí)現(xiàn) observeValueForKeyPath:ofObject:change:context:
方法, 每個(gè)觀察者都必須實(shí)現(xiàn). Account
在注冊(cè)的 key-path
發(fā)生改變時(shí)發(fā)送消息給 Person
. Person
作相應(yīng)處理
最終, 當(dāng)它不想收到消息, 至少要在它析構(gòu)之前 Person
實(shí)例必須通過(guò)發(fā)送 removeObserver:forKeyPath:
消息給 Account
注銷.
Registering for Key-Value Observing 描述了 KVO 從注冊(cè)到注銷的生命周期.
KVO 主要好處就是不需要實(shí)現(xiàn)自己的方案來(lái)在每次屬性更改時(shí)發(fā)送通知. 具有良好的基礎(chǔ)設(shè)置具有框架級(jí)的支持, 使其便于采用--通常你不需要想工程中添加任何代碼. 此外,基礎(chǔ)設(shè)施已經(jīng)功能齊全怀浆,這使得支持單個(gè)屬性的多個(gè)觀察者以及相關(guān)值變得很容易谊囚。
Registering Dependent Keys 解釋了如何定義一個(gè) key 依賴另一個(gè) key.
不像 NSNotificationCenter
, 它沒有中心對(duì)象給所有觀察者提供 change 消息. 當(dāng)發(fā)生 change 時(shí),消息直接被發(fā)送給觀察的對(duì)象, 這是 NSObject 提供 KVO 的基本實(shí)現(xiàn).
Key-Value Observing Implementation Details KVO 是如何實(shí)現(xiàn)的.
Registering for Key-Value Observing
必須執(zhí)行以下步驟以讓一個(gè)對(duì)象可以接收遵循 KVO 屬性的 KVO 消息:
- 用
addObserver:forKeyPath:options:context:
方法給被觀察者注冊(cè)觀察者. - 在觀察者內(nèi)部實(shí)現(xiàn)
observeValueForKeyPath:ofObject:change:context:
方法接收通知消息. - 使用
removeObserver:forKeyPath:
注銷觀察者當(dāng)它不想收到消息使, 最低限度, 在觀察者從內(nèi)存被釋放之前調(diào)用.
并非所有類的屬性都符合 KVO. KVO Compliance中描述如何確定類是否符合 KVO, 通常蘋果提供的 frameworks 中的屬性只有在被文檔化時(shí)才符合 KVO.
Registering as an Observer
addObserver:forKeyPath:options:context:
:
Options
選項(xiàng)參數(shù)指定為位或選項(xiàng)常量,它影響通知中提供的更改字典的內(nèi)容执赡,以及生成通知的方式镰踏。
NSKeyValueObservingOptionOld
(change 前舊值) |
NSKeyValueObservingOptionNew
(change 后新值) |
NSKeyValueObservingOptionInitial
(addObserver:forKeyPath:options:context:
方法返回前) |
NSKeyValueObservingOptionPrior
(在屬性 change 之前的通知, 普通通知在其后, 通過(guò)NSKeyValueChangeNotificationIsPriorKey
獲取值為NSNumber
轉(zhuǎn)換為 YES, 當(dāng)觀察者自身的 KVO遵從性要求它為其依賴于所觀察屬性的屬性調(diào)用-willChange…
之一時(shí), 一般的通知來(lái)得太晚, 無(wú)法及時(shí)調(diào)用willChange...
)Context
context
指針會(huì)在觀察者對(duì)應(yīng)的消息中傳遞. 可以指定為NULL
也可以完全依賴于keypath
字符串決定通知的初始化. 但是這會(huì)導(dǎo)致對(duì)象父類(也實(shí)現(xiàn)了一個(gè)相同keypath
的觀察)一些不明原因的問題.
一個(gè)更安全可擴(kuò)展的方法就是通過(guò)context
保證通知是你指定的觀察者而不是父類.
類中唯一命名的靜態(tài)變量的地址是一個(gè)很好的上下文∩澈希可以為整個(gè)類選擇上下文, 并依賴keypath
:
static void * PersonAccountBalanceContext =&PersonAccountBalanceContext;
static void * PersonAccountInterestRateContext =&PersonAccountInterestRateContext;
將自身注冊(cè)為 Account
實(shí)例 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:
方法不保持對(duì)觀察者奠伪,被觀察對(duì)象或上下文的強(qiáng)引用。您應(yīng)該確保在必要時(shí)保持對(duì)觀察者首懈,被觀察對(duì)象和上下文的強(qiáng)引用绊率。
Receiving Notification of a Change
當(dāng)對(duì)象的屬性發(fā)生變化時(shí), 觀察者會(huì)接收到 observeValueForKeyPath:ofObject:change:context:
消息.
change 字典 key NSKeyValueChangeKindKey
包含發(fā)生改變的信息. 如果被觀察的值發(fā)生變化, NSKeyValueChangeKindKey
返回 NSKeyValueChangeSetting
. NSKeyValueChangeOldKey
and NSKeyValueChangeNewKey
包含屬性的變化的前后值. 如果屬性是一個(gè)對(duì)象, 值直接被提供, 如果是一個(gè)標(biāo)量或一個(gè) C 結(jié)構(gòu)體, 會(huì)轉(zhuǎn)換為 NSValue
對(duì)象(as KVC).
如果被觀察對(duì)象是一個(gè) to-many 的關(guān)系, NSKeyValueChangeKindKey
會(huì)標(biāo)識(shí)是NSKeyValueChangeInsertion
(插入), NSKeyValueChangeRemoval
(刪除), or NSKeyValueChangeReplacement
(替換).
NSKeyValueChangeIndexesKey
是一個(gè) NSIndexSet 對(duì)象指定變化的關(guān)系索引.
如果注冊(cè)觀察者時(shí)指定 NSKeyValueObservingOptionNew
or NSKeyValueObservingOptionOld
options, 則 change 字典中 NSKeyValueChangeOldKey
and NSKeyValueChangeNewKey
返回關(guān)聯(lián)對(duì)象改變前/后 的值數(shù)組.
- (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];
}
}
注意: 如果通知傳播到類層次結(jié)構(gòu)的頂部,則
NSObject
拋出一個(gè)NSInternalInconsistencyException
因?yàn)檫@是一個(gè)編程錯(cuò)誤:子類無(wú)法使用它注冊(cè)的通知究履。
Removing an Object as an Observer
- (void)unregisterAsObserverForAccount:(Account*)account {
[account removeObserver:self
forKeyPath:@"balance"
context:PersonAccountBalanceContext];
[account removeObserver:self
forKeyPath:@"interestRate"
context:PersonAccountInterestRateContext];
}
移除觀察者記住以下幾點(diǎn):
- 移除一個(gè)未注冊(cè)的觀察者會(huì)導(dǎo)致
NSRangeException
. 只需要為對(duì)應(yīng)于addObserver:forKeyPath:options:context:
調(diào)用一次removeObserver:forKeyPath:context:
, 如果不可行, 則可以將removeObserver:forKeyPath:context:
放到try
/catch block
中處理潛在的異常滤否。 - 當(dāng)觀察者析構(gòu)時(shí)不會(huì)自動(dòng)移除. 被觀察對(duì)象繼續(xù)發(fā)送消息, 像發(fā)送一個(gè)消息給已經(jīng)釋放的對(duì)象, 觸發(fā) exception.
- 無(wú)法判斷一個(gè)對(duì)象是否是觀察者或被觀察對(duì)象. 避免這種情況. 典型的方式是在觀察者初始化的時(shí)候注冊(cè)(
init
orviewDidLoad
), 在對(duì)象析構(gòu)時(shí)注銷(dealloc
), 確保正確配對(duì)和排序添加和刪除消息, 觀察者在釋放之前注銷.
KVO Compliance
遵循 KVO, 類必須確保:
- 指定的屬性遵循 KVC, KVO 同 KVC 一樣支持相同的數(shù)據(jù)類型.
- 由類為屬性變化通知.
- 相關(guān) key 已正確注冊(cè).
NSObject 默認(rèn)自動(dòng)支持類遵循 KVC 的屬性.
子類通過(guò)實(shí)現(xiàn) automaticallyNotifiesObserversForKey:
方法控制是否自動(dòng)發(fā)消息.
Automatic Change Notification
NSObject 提供自動(dòng)鍵值更改通知的基本實(shí)現(xiàn):
//調(diào)用訪問器方法。
[account setName:@"Savings"];
//使用 setValue:forKey:最仑。
[account setValue:@"Savings" forKey:@"name"];
//使用 key-path藐俺,其中'account'是'document' 遵循 KVC 的屬性。
[document setValue:@"Savings" forKeyPath:@"account.name"];
//使用 mutableArrayValueForKey: 檢索 關(guān)系代理對(duì)象泥彤。
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
Manual Change Notification
手動(dòng)通知, 控制指定的 key 發(fā)送 change 通知.
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
手動(dòng)通知需要在屬性值發(fā)生改變前后分別調(diào)用 willChangeValueForKey:
和 didChangeValueForKey:
:
手動(dòng)調(diào)用 balance 屬性的 KVO.
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
判斷屬性是否有變化, 是否需要發(fā)送通知:
- (void)setBalance:(double)theBalance {
if (theBalance != _balance) {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
}
一個(gè)操作改變多個(gè)值:
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
[self willChangeValueForKey:@"itemChanged"];
_balance = theBalance;
_itemChanged = _itemChanged+1;
[self didChangeValueForKey:@"itemChanged"];
[self didChangeValueForKey:@"balance"];
}
有序集的情況下, 必須指定不僅僅是 key 改變, 還有對(duì)象調(diào)用的改變類型以及索引.變化類型是一個(gè) NSKeyValueChange
值, 索引是 NSIndexSet
對(duì)象.
有序集的刪除操作:
- (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"];
}
Registering Dependent Keys
在許多情況下欲芹,一個(gè)屬性的值取決于另一個(gè)對(duì)象中的一個(gè)或多個(gè)其他屬性的值。如果一個(gè)屬性的值發(fā)生了變化吟吝,那么派生屬性的值也應(yīng)該標(biāo)記為變化菱父。如何確保為這些依賴屬性發(fā)布 KVO 通知取決于關(guān)系的基數(shù)。
To-One Relationships
觸發(fā) to-one 關(guān)系對(duì)象自動(dòng)發(fā)出消息, 需要重寫 keyPathsForValuesAffectingValueForKey:
方法或適合的方法,該方法遵循它為注冊(cè)依賴鍵定義的模式。
例如, fullName
依賴 first
and last name
, 獲取 fullName
的方法可能為:
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
觀察 fullName
的對(duì)象, 不管 firstName
還是 lastName
屬性發(fā)生改變時(shí)都應(yīng)該被通知到, 作為他們影響的值.
一個(gè)解決辦法是通過(guò)重寫 keyPathsForValuesAffectingValueForKey:
方法指定 fullName
屬性依賴 firstName
和 lastName
屬性:
+ (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)一個(gè)形如 keyPathForValuesAffecting<Key>
的類方法:
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
不能通過(guò)分類形式給一個(gè)已經(jīng)存在的類重寫 keyPathsForValuesAffectingValueForKey:
方法, 但可以實(shí)現(xiàn) keyPathsForValuesAffecting<Key>
類方法.
To-Many Relationships
keyPathsForValuesAffectingValueForKey:
方法不支持 to-many
關(guān)系對(duì)象, 比如有一個(gè) Department
對(duì)象有一個(gè) to-many
關(guān)系對(duì)象(employees
)包含 Employee
, Employee
有一個(gè) salary 屬性. 你可能想要 Department
對(duì)象有一個(gè) totalSalary
屬性統(tǒng)計(jì)所有 Employees
的 salaries
. 使用 keyPathsForValuesAffectingTotalSalary
返回 employees.salary
是不行的.
有兩種解決方法:
-
可以通過(guò)注冊(cè)父類對(duì)象(
Department
)作為觀察者, 觀察所有子屬性(Employees
). 在關(guān)系對(duì)象中(employees
)中添加或移除子對(duì)象時(shí), 必須響應(yīng)的添加和移除父對(duì)象觀察者. 在observeValueForKeyPath:ofObject:change:context:
方法中根據(jù) changes 更新依賴值:- (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; }
如果是
Core Data
, 則可以通過(guò)NSNotificationCenter
注冊(cè)通知.
Key-Value Observing Implementation Details (KVO 的實(shí)現(xiàn)細(xì)節(jié))
KVO 的實(shí)現(xiàn)使用的是 isa-swizzling
技術(shù).
isa 指向維護(hù)在 dispatch table
中的類的對(duì)象. dispatch table
中包含類實(shí)現(xiàn)的方法以及其他數(shù)據(jù).
當(dāng)一個(gè)對(duì)象的屬性注冊(cè)了一個(gè)觀察者, 被觀察對(duì)象的 isa 會(huì)被修改, 指向一個(gè)中間類而不是真正的類. 因此浙宜,isa 指針的值不一定反映實(shí)例的實(shí)際類官辽。
永遠(yuǎn)不要使用 isa 來(lái)判斷類的從屬關(guān)系. 應(yīng)該使用 class 方法.