本文轉自:Objective-C中的KVC和KVO.
- KVC
- KVO
2.1. Registering for Key-Value Observing
2.1.1. 注冊成為觀察者
2.1.2. 接收變更通知
2.1.3. 移除觀察者
2.2. KVO Compliance(KVO兼容)
2.2.1. Automatic Change Notification(自動通知)
2.2.2. Manual Change Notification(手動通知)
2.3. Registering Dependent Keys(注冊依賴鍵)
2.3.1. To-one Relationships
2.3.2. To-many Relationships
2.4. 調試KVO
KVC
鍵/值編碼中的基本調用包括-valueForKey:
和-setValue:forKey:
。以字符串的形式向對象發送消息,這個字符串是我們關注的屬性的關鍵。
valueForKey:
首先查找以鍵-key或-isKey命名的getter方法。如果不存在getter方法(假如我們沒有通過@synthesize提供存取方法),它將在對象內部查找名為_key或key的實例變量。
對于KVC,Cocoa自動放入和取出標量值(int,float和struct)放入NSNumber或NSValue中;當使用-setValue:ForKey:
時,它自動將標量值從這些對象中取出。僅KVC具有這種自動包裝功能,常規方法調用和屬性語法不具備該功能。
-setValue:ForKey:
的工作方式和-valueForKey:
相同。它首先查找名稱的setter方法,如果不存在setter方法,它將在類中查找名為_key或key的實例變量。
使用KVC訪問屬性的代價比直接使用存取方法要大,所以只在需要的時候才用。
最簡單的 KVC 能讓我們通過以下的形式訪問屬性:
@property (nonatomic, copy) NSString *name;
取值:
NSString *n = [object valueForKey:@"name"];
設定:
[object setValue:@"Daniel" forKey:@"name"];
值得注意的是這個不僅可以訪問作為對象屬性,而且也能訪問一些標量(例如 int 和 CGFloat)和 struct(例如 CGRect)。Foundation 框架會為我們自動封裝它們。舉例來說,如果有以下屬性:
@property (nonatomic) CGFloat height;
我們可以這樣設置它:
[object setValue:@(20) forKey:@"height"];
有關KVC的更多用法,參看下面的文章:
KVO
KVO是Cocoa提供的一種稱為“鍵-值”觀察的機制,對象可以通過它得到其他對象特性屬性的變更通知。這種機制在MVC模式的場景中很重要,因為它讓視圖對象可以經由控制器層觀察模型對象的變更。
這一機制基于NSKeyValueObserving
非正式協議,Cocoa通過這個協議為所有遵守協議的對象提供了一種自動化的屬性觀察能力。要實現自動觀察,參與KVO的對象需要符合KVC的要求和存取方法,也可以手動實現觀察者通知,也可以兩者都保留。
KVO是Cocoa框架使用觀察者模式的一種途徑。
設置一個屬性的觀察者需要三步,理解這些步驟可以更清楚的知道KVO的工作框圖.
首先看看你當前的場景如果使用KVO是否更妥當,比如,當一個實例的某個具體屬性有任何變更的時候,另一個實例需要被通知。
比如,BankObject中的accountBalance屬性有任何變更時,某個PersonObject對象都要覺察到。
這個PersonObject對象必須注冊成為BankObject的accountBalance屬性的觀察者,可以通過發送addObserver: forKeyPath: options: context:
消息來實現。
注意:addObserver: forKeyPath: options: context:
方法在你指定的兩個實例間建立聯系,而不是在兩個類之間.
為了回應變更通知,觀察者必須實現observeValueForKeyPath: ofObject: change: context:
方法。這個方法的實現決定了觀察者如何回應變更通知。你可以在這個方法里自定義如何回應被觀察屬性的變更。
當一個被觀察屬性的值以符合KVO方式變更或者當它依賴的鍵變更時,observeValueForKeyPath: ofObject: change: context:
方法會被自動執行。
注冊成為觀察者(Registering for Key-Value Observing)
你可以通過發送addObserver: forKeyPath: options: context:
消息來注冊觀察者:
- (void)registerAsObserver {
/*
Register 'inspector' to receive change notifications for the "openingBalance" property of
the 'account' object and specify that both the old and new values of "openingBalance"
should be provided in the observe… method.
*/
[account addObserver:inspector
forKeyPath:@"openingBalance"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:NULL];
}
inspector注冊成為了account的觀察者,被觀察屬性的KeyPath是@"openingBalance",也就是account的openingBalance屬性,NSKeyValueObservingOptionNew
和NSKeyValueObservingOptionOld
選項分別標識在觀察者接收通知時change字典對應入口提供更改后的值和更改前的值。更簡單的辦法是用 NSKeyValueObservingOptionPrior
選項,隨后我們就可以用以下方式提取出改變前后的值:(change是個字典,詳細介紹請看下節)
id oldValue = change[NSKeyValueChangeOldKey];
id newValue = change[NSKeyValueChangeNewKey];
我們常常需要當一個值改變的時候更新 UI,但是我們也要在第一次運行代碼的時候更新一次 UI。我們可以用 KVO 并添加 NSKeyValueObservingOptionInitial
的選項 來一箭雙雕地做好這樣的事情。這將會讓 KVO 通知在調用-addObserver:forKeyPath:...
到時候也被觸發。
當我們注冊 KVO 通知的時候,我們可以添加 NSKeyValueObservingOptionPrior
選項,這能使我們在鍵值改變之前被通知。這和-willChangeValueForKey:
被觸發的時間相對應。
如果我們注冊通知的時候附加了NSKeyValueObservingOptionPrior
選項,我們將會收到兩個通知:一個在值變更前,另一個在變更之后。變更前的通知將會在 change 字典中有不同的鍵。
context是一個指針,當observeValueForKeyPath: ofObject: change: context:
方法執行時context會提供給觀察者。context可以是C指針或者一個對象引用,既可以當作一個唯一的標識來分辨被觀察的變更,也可以向觀察者提供數據。
接收變更通知
當被觀察的屬性變更時,觀察者會接到observeValueForKeyPath: ofObject: change: context:
消息,所有的觀察者都必須實現這個方法。
觀察者會被提供觸發通知的對象和keyPath,一個包含變更詳細信息的字典,還有一個注冊觀察者時提供的context指針。
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([keyPath isEqual:@"openingBalance"])
{
[openingBalanceInspectorField setObjectValue:
[change objectForKey:NSKeyValueChangeNewKey]];
}
/*
Be sure to call the superclass's implementation *if it implements it*.
NSObject does not implement the method.
*/
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
關于change參數,它是一個字典,有五個常量作為它的鍵:
NSString *const NSKeyValueChangeKindKey;
NSString *const NSKeyValueChangeNewKey;
NSString *const NSKeyValueChangeOldKey;
NSString *const NSKeyValueChangeIndexesKey;
NSString *const NSKeyValueChangeNotificationIsPriorKey;
- NSKeyValueChangeKindKey
指明了變更的類型,值為“NSKeyValueChange”枚舉中的某一個,類型為NSNumber。
enum {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;
NSKeyValueChangeNewKey
如果NSKeyValueChangeKindKey
的值為NSKeyValueChangeSetting
,并且NSKeyValueObservingOptionNew
選項在注冊觀察者時也指定了,那么這個鍵的值就是屬性變更后的新值。
對于NSKeyValueChangeInsertion
或者NSKeyValueChangeReplacement
,如果NSKeyValueObservingOptionNew
選項在注冊觀察者時也指定了,這個鍵的值是一個數組,其包含了插入或替換的對象。NSKeyValueChangeOldKey
如果NSKeyValueChangeKindKey
的值為NSKeyValueChangeSetting
,并且NSKeyValueObservingOptionOld
選項在注冊觀察者時也指定了,那么這個鍵的值就是屬性變更前的舊值。
對于NSKeyValueChangeRemoval
或者NSKeyValueChangeReplacement
,如果NSKeyValueObservingOptionOld
選項在注冊觀察者時也指定了,這個鍵的值是一個數組,其包含了被移除或替換的對象。NSKeyValueChangeIndexesKey
如果NSKeyValueChangeKindKey
的值為NSKeyValueChangeInsertion
,NSKeyValueChangeRemoval
, 或者NSKeyValueChangeReplacement
,這個鍵的值是一個NSIndexSet對象,包含了增加,移除或者替換對象的index。NSKeyValueChangeNotificationIsPriorKey
如果注冊觀察者時NSKeyValueObservingOptionPrior
選項被指明了,此通知會在變更發生前被發出。其類型為NSNumber,包含的值為YES。我們可以像以下這樣區分通知是在改變之前還是之后被觸發的:
if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) {
// 改變之前
} else {
// 改變之后
}
移除觀察者
你可以通過發送removeObserver: forKeyPath:
消息來移除觀察者,你需要指明觀察對象和路徑。
- (void)unregisterForChangeNotification {
[observedObject removeObserver:inspector forKeyPath:@"openingBalance"];
}
上面的代碼將openingBalance屬性的觀察者inspector移除,移除后觀察者再也不會收到observeValueForKeyPath: ofObject: change: context:
消息。
在移除觀察者之前,如果context是一個對象的引用,那么必須保持對它的強引用直到觀察者被移除。
KVO Compliance(KVO兼容)
有兩種方法可以保證變更通知被發出。自動發送通知是NSObject提供的,并且一個類中的所有屬性都默認支持,只要是符合KVO的。一般情況你使用自動變更通知,你不需要寫任何代碼。
人工變更通知需要些額外的代碼,但也對通知發送提供了額外的控制。你可以通過重寫子類automaticallyNotifiesObserversForKey:
方法的方式控制子類一些屬性的自動通知。
Automatic Change Notification(自動通知)
下面代碼中的方法都能導致KVO變更消息發出
// 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];
Manual Change Notification(手動通知)
下面的代碼為openingBalance屬性開啟了人工通知,并讓父類決定其他屬性的通知方式。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"openingBalance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
要實現人工觀察者通知,你要執行在變更前執行willChangeValueForKey:
方法,在變更后執行didChangeValueForKey:
方法:
- (void)setOpeningBalance:(double)theBalance {
[self willChangeValueForKey:@"openingBalance"];
_openingBalance = theBalance;
[self didChangeValueForKey:@"openingBalance"];
}
為了使不必要的通知最小化我們應該在變更前先檢查一下值是否變了:
- (void)setOpeningBalance:(double)theBalance {
if (theBalance != _openingBalance) {
[self willChangeValueForKey:@"openingBalance"];
_openingBalance = theBalance;
[self didChangeValueForKey:@"openingBalance"];
}
}
如果一個操作導致了多個鍵的變化,你必須嵌套變更通知:
- (void)setOpeningBalance:(double)theBalance {
[self willChangeValueForKey:@"openingBalance"];
[self willChangeValueForKey:@"itemChanged"];
_openingBalance = theBalance;
_itemChanged = _itemChanged+1;
[self didChangeValueForKey:@"itemChanged"];
[self didChangeValueForKey:@"openingBalance"];
}
在to-many關系操作的情形中,你不僅必須表明key是什么,還要表明變更類型和影響到的索引。變更類型是一個 NSKeyValueChange值,被影響對象的索引是一個 NSIndexSet對象。
下面的代碼示范了在to-many關系transactions對象中的刪除操作:
- (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(注冊依賴鍵)
有一些屬性的值取決于一個或者多個其他對象的屬性值,一旦某個被依賴的屬性值變了,依賴它的屬性的變化也需要被通知。
To-one Relationships
要自動觸發to-one
關系,有兩種方法:
重寫keyPathsForValuesAffectingValueForKey:
方法 或者
定義名稱為keyPathsForValuesAffecting<Key>
的方法。
例如一個人的全名是由姓氏和名子組成的:
- (NSString *)fullName
{
return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
一個觀察fullName的程序在firstName或者lastName變化時也應該接收到通知。
一種解決方法是重寫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;
}
相當于在影響fullName值的keypath中新加了兩個key:lastName和firstName,很容易理解。
另一種實現同樣結果的方法是實現一個遵循命名方式為keyPathsForValuesAffecting<Key>
的類方法,<Key>
是依賴于其他值的屬性名(首字母大寫),用上面代碼的例子來重新實現一下:
+ (NSSet *)keyPathsForValuesAffectingFullName
{
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
有時在類別中我們不能添加keyPathsForValuesAffectingValueForKey:
方法,因為不能再類別中重寫方法,所以這時可以實現keyPathsForValuesAffecting<Key>
方法來代替。
注意:你不能在keyPathsForValuesAffectingValueForKey:
方法中設立to-many關系的依賴,相反,你必須觀察在to-many
集合中的每一個對象中相關的屬性并通過親自更新他們的依賴來回應變更。下一節將會講述對付此情形的策略。
To-many Relationships
keyPathsForValuesAffectingValueForKey:
方法不支持包含to-many
關系的keypath。比如,假如你有一個Department類,它有一個針對Employee類的to-many關系(雇員),Employee類有salary屬性。你希望Department類有一個totalSalary屬性來計算所有員工的薪水,也就是在這個關系中Department的totalSalary依賴于所有Employee的salary屬性。你不能通過實現keyPathsForValuesAffectingTotalSalary
方法并返回employees.salary。
有兩種解決方法:
你可以用KVO將parent(比如Department)作為所有children(比如Employee)相關屬性的觀察者。你必須在把child添加或刪除到parent時也把parent作為child的觀察者添加或刪除。在observeValueForKeyPath:ofObject:change:context:
方法中我們可以針對被依賴項的變更來更新依賴項的值:
- (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;
}
2.如果你在使用Core Data,你可以在應用的notification center
中將parent注冊為它的 managed object context的觀察者,parent應該回應相應的變更通知,這些通知是children以類似KVO的形式發出的。
其實這也是Objective-C中利用Cocoa實現觀察者模式的另一種途徑:NSNotificationCenter.
調試KVO
你可以在 lldb 里查看一個被觀察對象的所有觀察信息。
(lldb) po [observedObject observationInfo]
這會打印出有關誰觀察誰之類的很多信息。
這個信息的格式不是公開的,我們不能讓任何東西依賴它,因為蘋果隨時都可以改變它。不過這是一個很強大的排錯工具。
KVO優點
首先,不需要自己去實現這樣的方案,這個獲得框架級支持,可以方便地采用。不需要設計自己的觀察者模型,直接可以在工程里使用。
其次,KVO的架構非常的強大,可以很容易的支持多個觀察者觀察同一個屬性,以及相關的值。