KVC
鍵值編碼的基本概念
- KVC 是 KeyValue Coding 的簡稱贱迟,它是一種可以直接通過字符串的名字(key)來訪問屬性的機制塞关。使用該機制不需要調用存取方法和變量實例就可訪問對象屬性抬探。本質上講,鍵-值編碼定義了你的程序存取方法需要實現(xiàn)的樣式及方法簽名帆赢。
- 在應用程序中實現(xiàn)鍵-值編碼兼容性是一項重要的設計原則小压。存取方法可以加強合適的數據封裝,而鍵-值編碼方法在多數情況下可簡化程序代碼椰于。
- 鍵-值編碼方法在 Objective-C 非標準協(xié)議(類目)NSKeyValueCoding中被聲明怠益,默認的實現(xiàn)方法由 NSObject 提供,所以凡是繼承自 NSObject 的類都具備 KVC 功能瘾婿。
- 鍵-值編碼支持帶有對象值的屬性蜻牢,同時也支持純數值類型和結構。非對象參數和返回類型會被識別并自動封裝/解封偏陪。
設置和訪問
鍵/值編碼中的基本調用包括 -valueForKey:
和 -setValue:forkey:
這兩個方法抢呆,它們以字符串的形式向對象發(fā)送消息,字符串是我們關注屬性的關鍵笛谦。
示例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路徑
- 除了通過鍵設置值外祭犯,鍵/值編碼還支持指定路徑,像文件系統(tǒng)一樣滚停,用“點”號隔開键畴。
- 使用 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"]);
實現(xiàn)簡單的運算
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
不同,鍵-值觀察中并沒有所謂的中心對象來為所有觀察者提供變化通知蛇更。取而代之地瞻赶,當有變化發(fā)生時,通知被直接發(fā)送至處于觀察狀態(tài)的對象派任。NSObject
提供這種基礎的鍵-值觀察實現(xiàn)方法砸逊。 - 你可以觀察任意對象屬性,包括簡單屬性掌逛,對一或是對多關系师逸。對多關系的觀察者將會被告知發(fā)生變化的類型-也就是任意發(fā)生變化的對象。
- 鍵-值觀察為所有對象提供自動觀察兼容性豆混。你可以通過禁用自動觀察通知并實現(xiàn)手動通知來篩選通知篓像。
注冊觀察者
為了正確接收屬性的變更通知,觀察對象必須首先發(fā)送一個addObserver:forKeyPath:options:context:
消息至被觀察對象皿伺,用以傳送觀察對象和需要觀察的屬性的關鍵路徑员辩,以便與其注冊。選項參數指定了發(fā)送變更通知時提供給觀察者的信息鸵鸥。 使用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 何時發(fā)生變化,都要通知我它改變的新值以及改變之前的舊值
[logger addObserver:observer
forKeyPath:@"lastTime"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
[[NSRunLoop mainRunLoop] run];
接受變更后通知
當被觀察對象的一個被觀察的屬性發(fā)生變動時宰翅,觀察者會收到 observeValueForKeyPath:ofObject:change:context:
消息弃甥。所有觀察者都必須實現(xiàn)這一方法。觸發(fā)觀察通知的對象和鍵路徑汁讼、包含變更細節(jié)的字典淆攻,以及觀察者注冊時提交的上下文指針均被提交給觀察者,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
會隨通知一起發(fā)送。context
可以用來回答:“這真的是我需要的通知嗎?”(你可以理解為 context
可以作為此觀察事件的唯一標識符)唱较。例如扎唾,你的父類可能使用 KVO,如果覆蓋了 observeValueForKeyPath:ofObject:change:context:
方法南缓,如何知道該將哪條消息轉發(fā)給父類的實現(xiàn)胸遇?可以創(chuàng)建一個單獨的指針,在開始觀察的時候將它作為 context
汉形,每次收到通知的時候將它和 context
進行對比纸镊。靜態(tài)變量的地址可以很好地工作。因此概疆,如果子類化某個使用了 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 {
// 這是我的快遞凯旭,處理變化
}
}
顯式觸發(fā)通知
如果使用存取方法來設置屬性,那么系統(tǒng)會自動通知觀察者楣颠。但如果處于某些原因尽纽,你選擇不使用存取方法呢?比如童漩,直接存取實例變量弄贿。這時可以通過 willChangeValueForKey:
和 didChangeValueForKey:
方法通知系統(tǒng)某個屬性的值即將/已經發(fā)生變化。
- (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];
但是系統(tǒng)不知道當 _lastTime
屬性發(fā)生變化時差凹,_lastTimeString
也會隨之發(fā)生變化。為了修復這個問題侧馅,你可以通過實現(xiàn)一個類方法顯式的告訴系統(tǒng)危尿,_lastTime
會影響 _lastTimeString
:
+ (NSSet *)keyPathsForValuesAffectingLastTimeString {
return [NSSet setWithObject:@"lastTime"];
}
注意這個方法的名字,它是 keyPathsForValuesAffecting
加上首字母大寫的鍵的名字馁痴。另外谊娇,沒有必要在類的頭文件中聲明這個方法,系統(tǒng)會在運行時找到它罗晕。
移除觀察者身份
你可以發(fā)送一條指定觀察方對象和鍵路徑的 removeObserver:forKeyPath:
消息至被觀察的對象济欢,來移除一個鍵-值觀察者(當我們達到目的時)。
[child removeObserver:self forKeyPath:@"key"];