《Objective-C 編程》35.KVC、KVO

KVC

鍵值編碼的基本概念

  1. KVCKeyValue Coding 的簡稱,它是一種可以直接通過字符串的名字(key)來訪問屬性的機制。使用該機制不需要調用存取方法和變量實例就可訪問對象屬性。本質上講,鍵-值編碼定義了你的程序存取方法需要實現的樣式及方法簽名。
  2. 在應用程序中實現鍵-值編碼兼容性是一項重要的設計原則。存取方法可以加強合適的數據封裝,而鍵-值編碼方法在多數情況下可簡化程序代碼。
  3. 鍵-值編碼方法在 Objective-C 非標準協議(類目)NSKeyValueCoding中被聲明,默認的實現方法由 NSObject 提供,所以凡是繼承自 NSObject 的類都具備 KVC 功能。
  4. 鍵-值編碼支持帶有對象值的屬性,同時也支持純數值類型和結構。非對象參數和返回類型會被識別并自動封裝/解封。

設置和訪問

鍵/值編碼中的基本調用包括 -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,編譯器會查找是否存在 settergetter 方法,如果不存在,它將在內部查找名為 _keykey 的實例變量。通過 KVC,可以獲取不存在getter方法的對象值,無需通過對象指針直接訪問。也就是說,KVC 能夠在沒有存取方法的情況下直接存取實例變量
  • KVC 只對對象類型有效。當我們通過 setValue:forKey: 設置對象的值,或通過 valueForKey 來獲取對象的值時,如果對象的實例變量為基本數據類型(charintfloatBOOL)時,我們需要使用 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

鍵值觀察的基本概念

  1. Key Value Observing,直譯為:基于鍵值的觀察者。它提供一種機制,當指定的對象的屬性被修改后,則對象就會接受到通知。簡單的說,就是每次指定的被觀察對象的屬性被修改后,KVO 就會自動通知相應的觀察者。
  2. NSNotification 不同,鍵-值觀察中并沒有所謂的中心對象來為所有觀察者提供變化通知。取而代之地,當有變化發生時,通知被直接發送至處于觀察狀態的對象。NSObject 提供這種基礎的鍵-值觀察實現方法。
  3. 你可以觀察任意對象屬性,包括簡單屬性,對一或是對多關系。對多關系的觀察者將會被告知發生變化的類型-也就是任意發生變化的對象。
  4. 鍵-值觀察為所有對象提供自動觀察兼容性。你可以通過禁用自動觀察通知并實現手動通知來篩選通知。

注冊觀察者

為了正確接收屬性的變更通知,觀察對象必須首先發送一個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"];
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 本文轉自:Objective-C中的KVC和KVO. KVC KVO2.1. Registering for Ke...
    0o凍僵的企鵝o0閱讀 428評論 0 3
  • 在iOS開發過程中,我們經常會聽到或者用到KVO,KVC,NSNotificationCenter等,但是很多時候...
    dullgrass閱讀 7,111評論 14 133
  • 一. KVO是鍵值觀察,是Objective-C對觀察者模式的實現,每次當被觀察者對象的某個屬性值發生改變時,注冊...
    魂一飛閱讀 212評論 0 0
  • 一、KVC ====基本概念 它是一種可以直接通過字符串類型的屬性名(key)來訪問某個類屬性的機制。而不是通過調...
    我真的真的是文藝青年閱讀 604評論 2 5
  • 在iOS開發過程中,我們經常會聽到或者用到KVO/KVC,但是對于什么是KVO和KVC,我們可能沒有那么了解。下面...
    問題餓閱讀 472評論 1 0