KVC簡介
我們知道可以通過setter、getter方法來設定和修改物件的屬性,也知道如何通過簡化的點語法來設定、修改物件的屬性。實際上,Objective-C還支援一種更靈活的操作方式,這種方式允許以字串形式間接操作物件的屬性,這種方式的全稱是Key Value Coding(簡稱KVC),即鍵值編碼。
簡單的KVC
最基本的KVC由NSKeyValueCoding協議提供支援,最基本的操作屬性的兩個方法如下:
- setValue:屬性值forKey:屬性名:為指定屬性設定值。
- valueForKey:屬性名:獲取指定屬性的值。
用法如下:@property (nonatomic, copy) NSString *name;
設定:[object setValue:@"Suxiaoyao" forKey:@"name"]
取值:NSString *nameStr = [object valueForKey:@"name"]
對於setValue:屬性值 forKey:@"name";程式碼,底層的執行機制如下:
- (1).程式優先呼叫“setName:屬性值;”程式碼通過setter方法完成設定。
- (2).如果該類沒有setName:方法,KVC機制會搜尋該類名為_name的成員變數,找到後對_name成員變數賦值。
- (3).如果該類既沒有setName:方法,也沒有定義_name成員變數,KVC機制會搜尋該類名為name的成員變數,找到後對name成員變數賦值。
- (4).如果上面3條都沒有找到,系統將會執行該物件的
setValue: forUndefinedKey:
方法。預設setValue: forUndefinedKey:
方法會引發一個異常,將會導致程式崩潰。
對於“valueForKey:@"name";”程式碼,底層執行機制如下:
- (1).程式優先呼叫"name;"程式碼來獲取該getter方法的返回值。
- (2).如果該類沒有name方法,KVC機制會搜尋該類名為_name的成員變數,找到後返回_name成員變數的值。
- (3).如果該類既沒有name方法,也沒有定義_name成員變數,KVC機制會搜尋該類名為name的成員變數,找到後返回name成員變數的值。
- (4).如果上面3條都沒有找到,系統將會執行該物件的
valueForUndefinedKey:
方法。預設valueForUndefinedKey:
方法會引發一個異常,將會導致程式崩潰。
處理不存在的key
前面提到過,當使用KVC方式操作屬性時,這些屬性可能不存在,此時系統只是引發了異常,並沒有進行任何特別的處理。但是在我們的實際開發中,結合我們的業務場景,我們總是不希望程式會崩潰,此時我們可以考慮重寫setValue: forUndefinedKey:
方法與valueForUndefinedKey:
方法。
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"您設定的key:[%@]不存在", key);
NSLog(@"您設定的value為:[%@]", value);
}
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"您訪問的key:[%@]不存在", key);
return nil;
}複製程式碼
這樣的話,當KVC操作並不存在的key時,KVC機制總是會呼叫重寫的方法進行處理,通過這種處理機制,可以非常方便的定製自己的處理行為。
處理nil值
當呼叫KVC來設定物件的屬性時,如果屬性的型別是物件型別(如NSString),嘗試將屬性設定為nil,是合法的,程式可以正常執行。
但是如果屬性的型別是基本型別(如int、float、double),嘗試將屬性設定為nil,程式將會崩潰引發以下異常'NSInvalidArgumentException',並且從提示資訊可以知道setNilValueForKey:方法導致了這個異常。當程式嘗試為某個屬性設定nil值時,如果該屬性並不接受nil值,那麼程式將會自動執行該物件的setNilValueForKey:方法。我們同樣可以重寫這個方法:
- (void)setNilValueForKey:(NSString *)key {
//對不能接受nil的屬性進行處理
if ([key isEqualToString:@"price"]) {
//對應你具體的業務來處理
price = 0;
}else {
[super setNilValueForKey:key];
}
}複製程式碼
我們可以通過重寫這個方法,並且根據我們不同的業務場景做單獨處理。
Key路徑(Key Path)
KVC 同樣允許我們通過關係來訪問物件。假設 person 物件有屬性 address,address 有屬性 city,我們可以這樣通過 person 來訪問 city:
[person valueForKeyPath:@"address.city"];複製程式碼
值得注意的是這裡我們呼叫 -valueForKeyPath: 而不是 -valueForKey:。
KVC協議中為操作Key路徑的方法如下:
- setValue:forKeyPath: 根據Key路徑設定屬性值
- valueForKeyPath: 根據Key路徑獲取屬性值
集合的操作
一個常常被忽視的 KVC 特性是它對集合操作的支援。舉個例子,我們可以這樣來獲得一個陣列中最大的值:
NSArray *a = @[@4, @84, @2];
NSLog(@"max = %@", [a valueForKeyPath:@"@max.self"]);複製程式碼
或者說,我們有一個 Transaction 物件的陣列,物件有屬性 amount 的話,我們可以這樣獲得最大的 amount:
NSArray *a = @[transaction1, transaction2, transaction3];
NSLog(@"max = %@", [a valueForKeyPath:@"@max.amount"]);複製程式碼
當我們呼叫[a valueForKeyPath:@"@max.amount"]
的時候,它會在陣列 a的每個元素中呼叫 -valueForKey:@"amount"然後返回最大的那個。KVC 的蘋果官方文件有一個章節 Collection Operators 詳細的講述了類似的用法。
KVC小結
前面介紹了這麼多內容,大家可能感到疑惑,為什麼要用KVC方式來操作呢?直接呼叫物件的setter與getter方法進行操作不可以嗎?是不是KVC方式的效能更好呢?實際上,通過KVC操作物件的效能比通過setter、getter方式操作的效能更差,使用KVC程式設計的優勢是更加簡潔,更適合提煉一些通用性質的程式碼。由於KVC允許通過字串形式來操作物件的屬性,這個字串既可是常量,也可是變數,因此具有極高的靈活性。
鍵值監聽(KVO)
在iOS應用的開發過程中,iOS應用通常會把應用程式元件分開成資料模型元件和檢視元件,其中資料模型元件負責維護應用程式的狀態資料,而檢視元件則負責顯示資料模型元件內部的狀態資料。
對於上面的設計結構,如果程式存在的需求是:在資料模型元件的狀態資料發生改變時,檢視元件能動態地更新自己,及時顯示資料模型元件更新後的資料。
iOS為我們提供了一種優秀的解決方案:利用KVO(Key Value Observing)機制。
KVO機制NSKeyValueObserving協議提供支援,當然,NSObject遵守了該協議,因此,NSObject的子類都可使用該協議中的方法,該協議包含如下常用的方法可用於註冊監聽器:
- addObserver:forKeyPath:options:context: 註冊一個監聽器用於監聽指定Key路徑
- removeObserver:forKeyPath: 為指定Key路徑刪除指定的監聽器
- removeObserver:forKeyPath:context: 為指定Key路徑刪除指定的監聽器,只是多了一個context引數。
對於上面的需求,很容易想到可以讓檢視元件來監聽資料模型元件的改變,當資料模型元件的key路徑對應的屬性發生改變時,作為監聽器的檢視元件將被激發,激發時就會回撥監聽器自身的監聽方法,該監聽方法如下:
observeValueForKeyPath:ofObject:change:context:
由此可見,作為監聽器的檢視元件需要重寫observeValueForKeyPath:ofObject:change:context:方法,重寫該方法時就可以得到最新修改的資料,從而使用最新的資料來更新檢視元件的顯示。
KVO程式設計的步驟如下:
- 為被監聽物件(通常是資料模型元件)註冊監聽器
- 重寫監聽器的observeValueForKeyPath:ofObject:change:context:方法
- 移除監聽器
KVO例項場景
我們要監聽一個人的心跳,並且在螢幕上顯示出來
我們定義出model
@interface Person : NSObject
/**
心跳
*/
@property (nonatomic, copy) NSString *heartbeat;
@end
@implementation Person
@end複製程式碼
定義此model為Controller的屬性,例項化它,監聽它的屬性,並顯示在當前的View裡邊
@interface ViewController ()
@property (nonatomic, strong) Person *person;
@property (nonatomic, strong) UILabel *heartbeatLabel;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.person = [[Person alloc] init];
[self.person setValue:@"72" forKey:@"heartbeat"];
[self.person addObserver:self forKeyPath:@"heartbeat" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
self.heartbeatLabel = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 100, 30)];
self.heartbeatLabel.textColor = [UIColor redColor];
self.heartbeatLabel.text = [self.person valueForKey:@"heartbeat"];
[self.view addSubview:self.heartbeatLabel];
UIButton *runButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
runButton.frame = CGRectMake(0, 0, 100, 30);
runButton.backgroundColor = [UIColor redColor];
[runButton addTarget:self action:@selector(run:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:runButton];
}複製程式碼
當點選button的時候,呼叫run方法,修改物件的屬性
- (void)run:(UIButton *)sender {
[self.person setValue:@"100" forKey:@"heartbeat"];
}複製程式碼
實現回撥方法
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if([keyPath isEqualToString:@"heartbeat"])
{
self.heartbeatLabel.text = [change objectForKey:@"new"];
}
}複製程式碼
增加觀察與取消觀察是成對出現的,所以需要在最後的時候,移除觀察者
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"heartbeat"];
}複製程式碼
KVO小結
KVO這種編碼方式使用起來很簡單,很適用於model修改後,引發的view的變化這種情況,就像上邊的例子那樣,當更改屬性的值後,監聽物件會立即得到通知。
希望對您有幫助,如果文章中有問題,歡迎評論留言~,謝謝支援~歡迎關注,我會在空餘時間更新技術文章~