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

KVC簡介

咱們知道能夠經過setter、getter方法來設置和修改對象的屬性,也知道如何經過簡化的點語法來設置、修改對象的屬性。實際上,Objective-C還支持一種更靈活的操做方式,這種方式容許以字符串形式間接操做對象的屬性,這種方式的全稱是Key Value Coding(簡稱KVC),即鍵值編碼。javascript

簡單的KVC

最基本的KVC由NSKeyValueCoding協議提供支持,最基本的操做屬性的兩個方法以下:java

  • 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";」代碼,底層執行機制以下:ide

  • (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,是合法的,程序能夠正常運行。atom

可是若是屬性的類型是基本類型(如int、float、double),嘗試將屬性設置爲nil,程序將會崩潰引起如下異常'NSInvalidArgumentException',而且從提示信息能夠知道setNilValueForKey:方法致使了這個異常。當程序嘗試爲某個屬性設置nil值時,若是該屬性並不接受nil值,那麼程序將會自動執行該對象的setNilValueForKey:方法。咱們一樣能夠重寫這個方法:spa

- (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的變化這種狀況,就像上邊的例子那樣,當更改屬性的值後,監聽對象會當即獲得通知。

但願對您有幫助,若是文章中有問題,歡迎評論留言~,謝謝支持~歡迎關注,我會在空餘時間更新技術文章~

相關文章
相關標籤/搜索