KVC
鍵值編碼的基本概念
- KVC 是 KeyValue Coding 的簡稱,它是一種可以直接通過字符串的名字(key)來訪問屬性的機制。使用該機制不需要調用存取方法和變量實例就可訪問對象屬性。本質上講,鍵-值編碼定義了你的程序存取方法需要實現的樣式及方法簽名。
- 在應用程序中實現鍵-值編碼兼容性是一項重要的設計原則。存取方法可以加強合適的數據封裝,而鍵-值編碼方法在多數情況下可簡化程序代碼。
- 鍵-值編碼方法在 Objective-C 非標準協議(類目)NSKeyValueCoding中被聲明,默認的實現方法由 NSObject 提供,所以凡是繼承自 NSObject 的類都具備 KVC 功能。
- 鍵-值編碼支持帶有對象值的屬性,同時也支持純數值類型和結構。非對象參數和返回類型會被識別并自動封裝/解封。
設置和訪問
鍵/值編碼中的基本調用包括 -valueForKey:
和 -setValue:forkey:
這兩個方法,它們以字符串的形式向對象發送消息,字符串是我們關注屬性的關鍵。
示例1:
BNRAppliance *a = [[BNRAppliance alloc] init];
[a setProductName:@"Washing Machine"];
// 使用 KVC 重寫以上代碼
[a setValue:@"Washing Machine" forKey:@"productName"];
[a setVoltage:240];
// 使用 KVC 重寫以上代碼
[a setValue:[NSNumber numberWithInt:240] forKey:@"voltage"];
NSLog(@"a is %@",a);
// 使用 KVC 重寫以上代碼
NSLog(@"the product name is %@",[a valueForKey:@"productName"]);
示例2:
Person *jack = [[Person alloc] init];
NSMutableString *name = [[NSMutableString alloc] initWithFormat:@"jack"];
[jack setValue:name forKey:@"name"];
NSLog(@"jack name : %@", [jack valueForKey:@"name"]);
- 使用 KVC,編譯器會查找是否存在 setter、getter 方法,如果不存在,它將在內部查找名為 _key 或 key 的實例變量。通過 KVC,可以獲取不存在getter方法的對象值,無需通過對象指針直接訪問。也就是說,KVC 能夠在沒有存取方法的情況下直接存取實例變量。
-
KVC 只對對象類型有效。當我們通過
setValue:forKey:
設置對象的值,或通過valueForKey
來獲取對象的值時,如果對象的實例變量為基本數據類型(char
、int
、float
、BOOL
)時,我們需要使用NSNumber
對象對數據進行封裝。
key路徑
- 除了通過鍵設置值外,鍵/值編碼還支持指定路徑,像文件系統一樣,用“點”號隔開。
- 使用 key path 可以一次性遍歷復雜的對象表。
- 注意順序,第一個想要遍歷的對象放在第一個。
??????你可以這樣理解:KVC 支持類似于「鏈式語法」的特性!
示例:
BNRDepartment. manager
指向 BNREmployee. emergencyContact
指向 BNRPerson. phoneNumber
BNRDepartment *sales = ...;
BNREmployee *sickEmployee = [sales valueForKey:@"manager"];
BNRPerson *personToCall = [sickEmployee valueForKey:@"emergencyContact"];
[personToCall setValue:@"555-606-0842" forKey:@"phoneNumber"];
// 使用 Key路徑 重寫以上代碼
BNRDepartment *sales = ...;
[personToCall setValue:@"555-606-0842"
forKeyPath:@"manager.emergencyContact.phoneNumber"];
一對多的關系
如果向 NSArray
請求一個鍵值,它實際上會查詢數組中的每個對象來查找這個鍵值,然后將查詢結果打包到另一個數組中并返回給你。
NSArray *booksArray = [NSArray arrayWithObjects:book1, book2, nil];
[book1 release];
[book2 release];
[book setValue:booksArray forKey:@"relativeBooks"];
NSLog(@"books 2: %@", [book valueForKeyPath:@"relativeBooks.price"]);
實現簡單的運算
NSString *count = [book valueForKeyPath:@"relativeBooks.@count"];
NSLog(@"count : %@", count);
NSString *sum = [book valueForKeyPath:@"relativeBooks.@sum._price"];
NSLog(@"sum : %@", sum);
NSString *avg = [book valueForKeyPath:@"relativeBooks.@avg._price"];
NSLog(@"avg : %@", avg);
NSString *min = [book valueForKeyPath:@"relativeBooks.@min._price"];
NSLog(@"min : %@", min);
NSString *max = [book valueForKeyPath:@"relativeBooks.@max._price"];
NSLog(@"max : %@", max);
KVO
鍵值觀察的基本概念
- Key Value Observing,直譯為:基于鍵值的觀察者。它提供一種機制,當指定的對象的屬性被修改后,則對象就會接受到通知。簡單的說,就是每次指定的被觀察對象的屬性被修改后,KVO 就會自動通知相應的觀察者。
- 與
NSNotification
不同,鍵-值觀察中并沒有所謂的中心對象來為所有觀察者提供變化通知。取而代之地,當有變化發生時,通知被直接發送至處于觀察狀態的對象。NSObject
提供這種基礎的鍵-值觀察實現方法。 - 你可以觀察任意對象屬性,包括簡單屬性,對一或是對多關系。對多關系的觀察者將會被告知發生變化的類型-也就是任意發生變化的對象。
- 鍵-值觀察為所有對象提供自動觀察兼容性。你可以通過禁用自動觀察通知并實現手動通知來篩選通知。
注冊觀察者
為了正確接收屬性的變更通知,觀察對象必須首先發送一個addObserver:forKeyPath:options:context:
消息至被觀察對象,用以傳送觀察對象和需要觀察的屬性的關鍵路徑,以便與其注冊。選項參數指定了發送變更通知時提供給觀察者的信息。 使用NSKeyValueObservingOptionOld
選項可以將初始對象值以變更字典中的一個項的形式提供給觀察者。指定NSKeyValueObservingOptionNew
選項可以將新的值以一個項的形式添加至變更字典。你可以使用逐位“|”這兩個常量來指定接收上述兩種類型的值。
BNRLogger *logger = [[BNRLogger alloc] init];
__unused NSTimer *timer =
[NSTimer scheduledTimerWithTimeInterval:2.0
target:logger
selector:@selector(updateLastTime:)
userInfo:nil
repeats:YES];
BNRObserver *observer = [[BNRObserver alloc] init];
// 讓 BNRObserver 實例觀察 BNRLogger 的 lastTime 屬性!
// 無論 lastTime 何時發生變化,都要通知我它改變的新值以及改變之前的舊值
[logger addObserver:observer
forKeyPath:@"lastTime"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
[[NSRunLoop mainRunLoop] run];
接受變更后通知
當被觀察對象的一個被觀察的屬性發生變動時,觀察者會收到 observeValueForKeyPath:ofObject:change:context:
消息。所有觀察者都必須實現這一方法。觸發觀察通知的對象和鍵路徑、包含變更細節的字典,以及觀察者注冊時提交的上下文指針均被提交給觀察者,context
可以為任意類型參數。
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {
NSString *oldValue = [change objectForKey:NSKeyValueChangeOldKey];
NSString *newValue = [change objectForKey:NSKeyValueChangeNewKey];
NSLog(@"Observed:%@ of %@ was changed from %@ to %@",
keyPath,object,oldValue,newValue);
}
注意,當在代碼中將某個對象注冊為觀察者時,你需要傳遞指針作為 context
。當接收變化的通知時,context
會隨通知一起發送。context
可以用來回答:“這真的是我需要的通知嗎?”(你可以理解為 context
可以作為此觀察事件的唯一標識符)。例如,你的父類可能使用 KVO,如果覆蓋了 observeValueForKeyPath:ofObject:change:context:
方法,如何知道該將哪條消息轉發給父類的實現?可以創建一個單獨的指針,在開始觀察的時候將它作為 context
,每次收到通知的時候將它和 context
進行對比。靜態變量的地址可以很好地工作。因此,如果子類化某個使用了 KVO 的類時,可以編寫如下代碼:
static int contextForKVO;
[petOwner addObserver:self
forKeyPath:@"fido"
options:NSKeyValueObservingOptionNew
context:&contextForKVO];
...
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context
{
// 判斷這是不是我的快遞?
if (context !=&contextForKVO) {
// 不,這是我爹(父類)的快遞
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
} else {
// 這是我的快遞,處理變化
}
}
顯式觸發通知
如果使用存取方法來設置屬性,那么系統會自動通知觀察者。但如果處于某些原因,你選擇不使用存取方法呢?比如,直接存取實例變量。這時可以通過 willChangeValueForKey:
和 didChangeValueForKey:
方法通知系統某個屬性的值即將/已經發生變化。
- (void)updateLastTime:(NSTimer *)timer {
NSDate *now = [NSDate date];
[self willChangeValueForKey:@"lastTime"];
_lastTime = now;
[self didChangeValueForKey:@"lastTime"];
}
獨立的屬性
如果你不想觀察 _lastTime
而想觀察 _lastTimeString
,即:
BNRObserver *observer = [[BNRObserver alloc] init];
[logger addObserver:observer
forKeyPath:@"lastTimeString"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
但是系統不知道當 _lastTime
屬性發生變化時,_lastTimeString
也會隨之發生變化。為了修復這個問題,你可以通過實現一個類方法顯式的告訴系統,_lastTime
會影響 _lastTimeString
:
+ (NSSet *)keyPathsForValuesAffectingLastTimeString {
return [NSSet setWithObject:@"lastTime"];
}
注意這個方法的名字,它是 keyPathsForValuesAffecting
加上首字母大寫的鍵的名字。另外,沒有必要在類的頭文件中聲明這個方法,系統會在運行時找到它。
移除觀察者身份
你可以發送一條指定觀察方對象和鍵路徑的 removeObserver:forKeyPath:
消息至被觀察的對象,來移除一個鍵-值觀察者(當我們達到目的時)。
[child removeObserver:self forKeyPath:@"key"];