iOS 之鍵值編碼(KVC)與鍵值監聽(KVO)

蘇小妖發表於2017-05-06

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的變化這種情況,就像上邊的例子那樣,當更改屬性的值後,監聽物件會立即得到通知。

希望對您有幫助,如果文章中有問題,歡迎評論留言~,謝謝支援~歡迎關注,我會在空餘時間更新技術文章~

相關文章