KVC/KVO 總結

MrZander發表於2018-11-07

KVC

Key-Value Coding基本原則

訪問物件屬性

@interface BankAccount: NSobject

@property (nonatomic) NSNumber *currentBalance; // An attribute
@property (nonatomic) Person *owner; // A to-one relation
@property (nonatomic) NSArray <Transaction *>*transactions; // A to-many relation

@end
複製程式碼

currentBalance/owner/transactions都是BankAccount的屬性。owner屬性是一個物件,和BankAccount構成一對一的關係,owner物件中的屬性改變後並不會影響到owner本身。

為了保持封裝,物件通常為其介面上的屬性提供訪問器方法(accessor methods)。在使用訪問器方法時必須在編譯之前將屬性名稱寫入程式碼中。訪問器方法的名稱成為使用它的程式碼的靜態部分。例如: [myAccount setCurrentBalance:@(100.0)]; 這樣缺乏靈活性,KVC提供了使用字串識別符號訪問物件屬性的更通用的機制。

使用key和key path 標識物件的屬性

key: 標識特定屬性的字串。通常表示屬性的key是程式碼中顯示的屬性本身的名稱。 key必須使用ASCII編碼,可能不包含空格,並且通常是以小寫字母開頭(URL除外)。 上面的賦值過程使用KVC表示: [myAccount setValue:@(100.0) forKey:@"currentBalance"];

key path: 用來指定要遍歷的物件屬性序列的一串使用“.”分隔的key。序列中的第一個鍵的屬性是相對於接受者的,並且每個後續鍵是相對於前一個屬性的值的。當需要使用一個方法來向下逐級獲取物件層次結構時,key path特別有用。 例如,owner.address.street應用於銀行賬戶例項的key path是指儲存在銀行賬戶所有者地址中的street字串的值。

使用key獲取屬性值

- (void)getAttributeValuesUsingKeys {
    Account *myAccount = [[Account alloc] init];
    myAccount.currBalance = @100;
    
    Person *owner = [[Person alloc] init];
    Address *address = [[Address alloc] init];
    address.street = @"第三大道";
    owner.address = address;
    myAccount.owner = owner;
    
    Transaction *t1 = [[Transaction alloc] init];
    Person *p1 = [[Person alloc] init];
    p1.name = @"p1";
    t1.payee = p1;
    
    Transaction *t2 = [[Transaction alloc] init];
    Person *p2 = [[Person alloc] init];
    p2.name = @"p2";
    t2.payee = p2;
    
    NSArray *ts = @[t1, t2];
    myAccount.transactions = ts;
    
    NSNumber *currBalance = [myAccount valueForKey:@"currBalance"];
    NSLog(@"currBalance = %@", currBalance); // currBalance = 100
    
    NSString *street = [myAccount valueForKeyPath:@"owner.address.street"];
    NSLog(@"street = %@", street); // street = 第三大道
    
    NSDictionary *values = [myAccount dictionaryWithValuesForKeys:@[@"currBalance", @"owner"]];
    NSLog(@"values = %@", values); // values = {currBalance = 100; owner = "<Person: 0x60000179af40>";}
    
    NSArray *payees = [myAccount valueForKeyPath:@"transactions.payee.name"];
    NSLog(@"payees = %@", payees); // payees = (p1, p2)
    
    // Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<Account 0x600002685ee0> valueForUndefinedKey:]'
    
    //    [myAccount valueForKey:@"owner.address.street"];
    //    [myAccount valueForKey:@"test"];
    //    [myAccount dictionaryWithValuesForKeys:@[@"currBalance", @"transactions.payee.name"]];
}
複製程式碼

使用key設定屬性值

- (void)settingAttributeValuesUsingKeys {
    Account *myAccount = [[Account alloc] init];
    [myAccount setValue:@100.0 forKey:@"currBalance"];
    NSLog(@"currBalance = %@", myAccount.currBalance); // currBalance = 100
    
    // operationTimes是非引用型別,這裡進行了和NSNumber的自動轉換
    [myAccount setValue:@10 forKey:@"operationTimes"];
    NSLog(@"operationTimes = %ld", myAccount.operationTimes); // operationTimes = 10
    
    Person *owner = [[Person alloc] init];
    Address *address = [[Address alloc] init];
   
    [myAccount setValue:address forKeyPath:@"owner.address"]; // 這時候owner還是null
    NSLog(@"address = %@", myAccount.owner.address); // address = (null)
    
    [myAccount setValue:owner forKeyPath:@"owner"];
    [myAccount setValue:address forKeyPath:@"owner.address"];
    NSLog(@"address = %@", myAccount.owner.address); // address = <Address: 0x600001a43550>
    
    [myAccount setValuesForKeysWithDictionary:@{@"currBalance": @200.0, @"owner": owner}];
    NSLog(@"currBalance = %@, owner = %@", myAccount.currBalance, myAccount.owner); // currBalance = 200, owner = <Person: 0x600001478ee0>
    
    // Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<Account 0x6000029c2490> setValue:forUndefinedKey:]: xxx'
    //    [myAccount setValue:@"value" forUndefinedKey:@"undefinedKey"];
    //    [myAccount setValuesForKeysWithDictionary:@{@"currBalance": @200.0, @"owner.address.street": @"第一大道"}];
}
複製程式碼

訪問集合屬性

符合鍵值編碼的物件以與公開其他屬性相同的方式公開其多對多屬性。您可以使用valueForKey:setValue:forKey:來獲取或設定集合屬性。但是,當你想要操作這些集合內容的時候,使用協議定義的可變代理方法通常是最有效的。 該協議為集合物件訪問定義了三種不同的代理方法,每種方法都有一個key和key path變數:

  • mutableArrayValueForKey:mutableArrayValueForKeyPath: 返回一個行為類似NSMutableArray的代理物件
  • mutableSetValueForKey:mutableSetValueFOrKeyPath: 返回一個行為類似NSMutableSet的代理物件
  • mutableOrderedSetValueForKey:mutableOrderedSetValueForKeyPath: 返回一個行為類似NSMutableOrderedSet的代理物件 當您對代理物件進行操作,向物件新增元素,從中刪除元素或者替換其中的元素時,協議的預設實現會相應地修改基礎屬性。這比使用valueForKey:獲取一個不可變的集合物件,再建立一個可修改的集合,然後把修改後的集合通過setValue:forKey:更有效。在許多情況下,它比直接使用可變屬性也是更有效的。這些方法為持有集合物件的物件們提供了維護KVO特性的好處。
- (void)accessingCollectionProperties {
    Transaction *t1 = [[Transaction alloc] init];
    Transaction *t2 = [[Transaction alloc] init];
    Account *myAccount = [[Account alloc] init];
    
    [myAccount addObserver:self forKeyPath:@"transactions" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
    
    
    [myAccount setValue:@[t1, t2] forKey:@"transactions"];
    NSLog(@"1st transactions = %@", myAccount.transactions); // 1st transactions = ("<Transaction: 0x6000009d1400>","<Transaction: 0x6000009d1420>")
    NSMutableArray <Transaction *>*transactions = [myAccount mutableArrayValueForKey:@"transactions"];
    
    [transactions addObject:[[Transaction alloc] init]];
    NSLog(@"2nd transactions = %@", myAccount.transactions); // 2nd transactions = ("<Transaction: 0x6000009d1400>","<Transaction: 0x6000009d1420>","<Transaction: 0x6000009cabf0>")
    
    [transactions removeLastObject];
    NSLog(@"3th transactions = %@", myAccount.transactions); // 3th transactions = ("<Transaction: 0x6000009d1400>","<Transaction: 0x6000009d1420")
}
複製程式碼

使用集合操作符

當您向valueForKeyPath:訊息傳送符合鍵值編碼的物件時,可以在key path中嵌入集合運算子。集合運算子是一個小的關鍵字列表之一,前面是一個@符號,它指定了getter應該執行的操作,以便在返回之前以某種方式運算元據。NSObjectvalueForKeyPath:提供了預設實現。 當key path包含集合運算子時,運算子之前的部分稱為左鍵路徑,指示相對於訊息接受者操作的集合,當你直接向一個集合(例如NSArray)傳送訊息時左鍵路徑或許可以省略。操作符之後的部分稱為右鍵路徑,指定操作符應處理的集合中的屬性,除了@count之外的所有操作符都需要一個右鍵路徑。

KVC/KVO 總結
集合運算子表現出三種基本型別的行為:

  • 聚合運算子以某種方式合併集合的物件,並返回通常與右鍵路徑中指定的屬性的資料型別匹配的單個物件。@count是一個例外,它沒有正確的關鍵路徑並始終將返回一個NSNumber例項。包括:@avg/@count/@max/@min/@sum
  • 陣列運算子返回一個NSArray例項,該例項包含命名集合中儲存的物件的某個子集。包含:@distinctUnionOfObjects/@unionOfObjects
  • 巢狀運算子處理包含其他集合的集合,並根據操作符返回一個NSArrayNSSet例項,它以某種方式組合巢狀集合的物件。包含:@distinctUnionOfArrays/@unionOfArrays/@distinctUnionOfSets

示例

- (void)usingCollectionOperators {
    Transaction *t1 = [Transaction transactionWithPayee:@"Green Power" amount:@(120.00) date:[NSDate dateWithTimeIntervalSinceNow:100]];
    Transaction *t3 = [Transaction transactionWithPayee:@"Green Power" amount:@(170.00) date:[NSDate dateWithTimeIntervalSinceNow:300]];
    Transaction *t5 = [Transaction transactionWithPayee:@"Car Loan" amount:@(250.00) date:[NSDate dateWithTimeIntervalSinceNow:500]];
    Transaction *t6 = [Transaction transactionWithPayee:@"Car Loan" amount:@(250.00) date:[NSDate dateWithTimeIntervalSinceNow:600]];
    Transaction *t13 = [Transaction transactionWithPayee:@"Animal Hospital" amount:@(600.00) date:[NSDate dateWithTimeIntervalSinceNow:500]];
    
    NSArray *transactions = @[t1, t3, t5, t6, t13];
    
    /* 聚合運算子
     * 聚合運算子可以處理陣列或屬性集,從而生成反映集合某些方面的單個值。
     */
    // @avg 平均值
    NSNumber *transactionAverage = [transactions valueForKeyPath:@"@avg.amount"];
    NSLog(@"transactionAverage = %@", transactionAverage); // transactionAverage = 278
    // @count 個數
    NSNumber *numberOfTransactions = [transactions valueForKeyPath:@"@count"];
    NSLog(@"numberOfTransactions = %@", numberOfTransactions); // numberOfTransactions = 5
    // @max 最大值 使用compare:進行比較
    NSDate *latestDate = [transactions valueForKeyPath:@"@max.date"];
    NSLog(@"latestDate = %@", latestDate); // latestDate = Thu Nov  1 15:05:59 2018
    // @min 最小值 使用compare:進行比較
    NSDate *earliestDate = [transactions valueForKeyPath:@"@min.date"];
    NSLog(@"earliestDate = %@", earliestDate);// earliestDate = Thu Nov  1 14:57:39 2018
    // @sum 總和
    NSNumber *amountSum = [transactions valueForKeyPath:@"@sum.amount"];
    NSLog(@"amountSum = %@", amountSum); // amountSum = 1390
    
    /* 陣列運算子
     *
     * 陣列運算子導致valueForKeyPath:返回與右鍵路徑指示的特定物件集相對應的物件陣列。
     * 如果使用陣列運算子時任何葉物件為nil,則valueForKeyPath:方法會引發異常。
     **/
    // @distinctUnionOfObjects 建立並返回一個陣列,該陣列包含與右鍵路徑指定的屬性對應的集合的不同物件。會刪除重複物件。
    NSArray *distinctPayees = [transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];
    NSLog(@"distinctPayees = %@", distinctPayees); // distinctPayees = ("Green Power", "Animal Hospital", "Car Loan")
    
    // @unionOfObjects 建立並返回一個陣列,該陣列包含與右鍵路徑指定的屬性對應的集合的所有物件。不刪除重複物件
    NSArray *payees = [transactions valueForKeyPath:@"@unionOfObjects.payee"];
    NSLog(@"payees = %@", payees); // payees = ("Green Power", "Green Power", "Car Loan", "Car Loan", "Animal Hospital")
    
    /** 巢狀運算子
     *
     * 巢狀運算子對巢狀集合進行操作,集合中的每個條目都包含一個集合。
     * 如果使用陣列運算子時任何葉物件為nil,則valueForKeyPath:方法會引發異常。
     **/
    Transaction *moreT1 = [Transaction transactionWithPayee:@"General Cable - Cottage" amount:@(120.00) date:[NSDate dateWithTimeIntervalSinceNow:10]];
    Transaction *moreT2 = [Transaction transactionWithPayee:@"General Cable - Cottage" amount:@(1550.00) date:[NSDate dateWithTimeIntervalSinceNow:3]];
    Transaction *moreT7 = [Transaction transactionWithPayee:@"Hobby Shop" amount:@(600.00) date:[NSDate dateWithTimeIntervalSinceNow:160]];
    NSArray *moreTransactions = @[moreT1, moreT2, moreT7];
    NSArray *arrayOfArrays = @[transactions, moreTransactions];
    // @distinctUnionOfArrays  指定@distinctUnionOfArrays運算子時,valueForKeyPath:建立並返回一個陣列,該陣列包含與右鍵路徑指定的屬性對應的所有集合的組合的不同物件。
    NSArray *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.payee"];
    NSLog(@"collectedDistinctPayees = %@", collectedDistinctPayees); // collectedDistinctPayees = ( "General Cable - Cottage", "Animal Hospital", "Hobby Shop", "Green Power", "Car Loan")
    // @unionOfArrays 與@distinctUnionOfArrays 不同的是不會刪除相同的元素
    NSArray *collectedPayees = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.payee"];
    NSLog(@"collectedPayees = %@", collectedPayees); // collectedPayees = ("Green Power", "Green Power", "Car Loan", "Car Loan", "Animal Hospital", "General Cable - Cottage", "General Cable - Cottage", "Hobby Shop")
    
    // @distinctUnionOfSets 與@distinctUnionOfArrays作用相同,只是用於NSSet物件而不是NSArray
}
複製程式碼

訪問者搜尋模式

NSObject提供的NSkeyValueCoding協議的預設實現使用明確定義的規則集將基於鍵的訪問器呼叫對映到物件的基礎屬性。這些協議方法使用“key”在其自己的物件例項中搜尋訪問器、例項變數以及遵循某個命名規則的相關方法。雖然您很少修改此預設搜尋,但瞭解它的工作方式會有所幫助,既可以跟蹤鍵值編碼物件的行為,也可以使您自己的物件相容KVC。

Getter的搜尋模式

valueForKey:的預設實現是,給定key引數作為輸入,通過下面的過程,在接收valueForKey:呼叫的類例項中操作。

  1. 按順序搜尋訪問器方法get<Key>/<key>/is<Key>/_<key>。如果找到,呼叫該方法並且帶著方法的呼叫結果調轉到第5步執行;否則,繼續下一步。
  2. 如果沒有找到簡單的訪問方法,搜尋其名稱匹配某些模式的方法的例項。其中匹配模式包含countOf<Key>objectIn<Key>AtIndex:(對應於NSArray定義的基本方法),和<key>AtIndexs:(對應於NSArray的方法objectsAtIndexs:) 一旦找到第一個和其他兩個中的至少一個,則建立一個響應所以NSArray方法並返回該方法的集合代理物件。否則,執行第3步。 代理物件隨後將任何NSArray接收到的一些組合的訊息。**實際上,與符合鍵值編碼物件一起工作的代理物件允許底層屬性的行為就像它是NSArray一樣,即便它不是。
  3. 如果沒有找到簡單的訪問器方法或陣列訪問方法組,則尋找三個方法countOf<Key>/enumeratorOf<Key>/memberOf<Key>:,對應NSSet類的基本方法。 如果三個方法全找到了,則建立一個集合代理物件來響應所有的NSSet方法並返回。否則,執行第4步。
  4. 如果上面的方法都沒有找到,並且接受者的類方法accessInstanceVariablesDirectly返回YES(預設YES),則按序搜尋以下例項變數:_<key>/_is<Key>/<key>/is<Key>。如果找到其中之一,直接獲取例項變數的值並跳轉到第5步;否則執行第6步。
  5. 如果檢索到的屬性值是物件指標,則只返回結果;如果值是受NSNumber支援的標量,則將其儲存在NSNumber例項中並返回;如果結果是NSNumber不支援的標量,則轉換成NSValue物件並返回
  6. 如果以上所有的嘗試都失敗了,則呼叫valueForUndefinedKey:,這個方法預設丟擲異常,NSObject的子類可以重寫來自定義行為。

Setter的搜尋模式

setValue:forKey:的預設實現是給定keyvalue作為引數輸入,嘗試把value設定給以key命名的屬性。過程如下:

  1. 按序搜尋set<Key>:_set<Key>,如果找到,則使用輸入引數呼叫並結束。
  2. 如果沒有找到簡單的訪問器方法,並且如果類方法accessInstanceVariablesDirectly返回YES(預設為YES),則按序搜尋以下例項變數: _<key>/_is<Key>/<key>/is<Key>,如果找到了則直接進行賦值並結束。
  3. 以上方法皆失敗則呼叫setValue:forUndefinedKey:,這個方法預設丟擲異常,NSObject的子類可以自定義。

KVO

Key-value observing提供了一種機制,允許物件把自身屬性的更改通知給其他屬性。它對應用程式中model和controller層之間的通訊特別有用。通常,控制器物件觀察模型物件的屬性,檢視物件通過控制器觀察模型物件的屬性。另外,一個模型物件或許會觀察另一個模型物件(通常用與確認從屬值何時改變)或甚至自身(再次確認從屬值何時改變)。 你可以觀察屬性,包括簡單屬性,一對一關係和多對多關係。多對多關係的觀察者被告知所作出的改變的型別——以及改變中涉及哪些物件。

註冊KVO

  • 使用addObserver:forKeyPath:options:content:方法來給observer註冊一個observed object
  • 在observer內部實現observerValueForKeyPath:ofObject:change:context:來接收更改的通知訊息。
  • 當不再應該接收訊息時,使用removeObserver:forKeyPath:方法來反註冊觀察者。起碼也要在observer被移除前呼叫這個方法。

註冊Observer

addObserver:forKeyPath:options:content:
複製程式碼

options

options引數指定了一個按位OR的常量選項,會影響通知中提供的更改字典的內容和生成通知的方式。 你可以選擇使用NSKeyValueObservingOptionOld選項,在被觀察的屬性修改前收到舊值;也可以使用NSKeyValueObservingOptionNew來獲取修改後的新值。通過NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew獲取兩者。 使用NSKeyValueObservingOptionInitial選項,讓被觀察的屬性在addObserver:forKeyPath:options:context方法返回前傳送即時通知。你可以使用此附加的一次性通知來在觀察者中建立屬性的初始值。 通過包含NSKeyValueObservingOptionPrior來指示被觀察物件在屬性更改之前傳送通知(除了在更改之後傳送通知)。在更改之前傳送的通知中的change字典始終包含NSKeyValueChangeNotificationIsPriorKey,其值是包含布林值YES的NSNumber物件,但不包含NSKeyValueChangeNewKey的內容。如果指定此選項,則更改後傳送的通知中的change字典的內容和未指定此選項時包含的內容相同。當觀察者自己的鍵值觀察相容性要求它為自己的一個屬性呼叫-willChangexxx方法之一時,可以使用此選項,並且該屬性的值取決於被觀察物件的屬性的值。

context

addObserver:forKeyPath:options:context:訊息中的上下文指標包含將在相應的更改通知中傳遞迴觀察者的任意資料。您可以使用NULL來完全指定並依賴於key path字串來確定更改通知的來源,但是這種方法可能會導致其超類也因不同原因觀察到相同金鑰路徑的物件出現問題。

一個更安全且具有擴充套件性的方法是使用content來確保你收到的通知就是發給你的而不是超類的。

類中唯一命名的靜態(static)變數的地址是一個很好的content。在超類或子類中以類似的方式選擇的上下文不太可能重疊。您可以為整個類選擇同一個上下文,並根據通知訊息中的key path字串來確定更改的內容;或者,您可以為每個觀察到的金鑰路徑建立不同的上下文,從而完全繞過字串比較的需要,從而實現更有效的通知解析。

- (void)registerAsObserver {
    BankAccount *myAccount = [[BankAccount alloc] init];
    [myAccount addObserver:self forKeyPath:@"currBalance" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionPrior context:PersonAccountBalanceContext];
    myAccount.currBalance = @100;
}
複製程式碼

注意,鍵值觀察addObserver:forKeyPath:options:context:方法不對觀察者、被觀察的物件、上下文保持強引用。如需要,你應該對它們保持強引用。

接受改變的通知

當物件的被觀察屬性值改變的時候,觀察者物件會收到observeValueForKeyPath:ofObject:change:context:訊息。所有的觀察者必須實現這個方法。

觀察物件提供觸發通知的key path,自身作為objectchange字典包含改變的細節,並且context指標就是觀察者被註冊時提供的。

NSKeyValueChangeKindKey提供改變型別的資訊。NSKeyValueChangeKindKey表示觀察物件的值已更改。如果觀察的屬性是一個對多的關係,NSKeyValueChangeInsertion/NSKeyValueChangeRemoval/NSKeyValueChangeReplacement分別表示集合的插入、刪除、替換操作。NSKeyValueChangeIndexesKey表示集合中已更改內容的NSIndexSet

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (context == PersonAccountBalanceContext) {
        NSLog(@"PersonAccountBalanceContext 對應的屬性改變了");
    } else if (context == PersonAccountTransactionContext) {
        if ([change[NSKeyValueChangeKindKey] unsignedIntValue] == NSKeyValueChangeSetting) {
            NSLog(@"集合內容賦值 索引為:%@", change[NSKeyValueChangeIndexesKey]);
        } else if ([change[NSKeyValueChangeKindKey] unsignedIntValue] == NSKeyValueChangeInsertion) {
            NSLog(@"集合內容插入 索引為:%@", change[NSKeyValueChangeIndexesKey]);
        } else if ([change[NSKeyValueChangeKindKey] unsignedIntValue] == NSKeyValueChangeRemoval) {
            NSLog(@"集合內容刪除 索引為:%@", change[NSKeyValueChangeIndexesKey]);
        } else if ([change[NSKeyValueChangeKindKey] unsignedIntValue] == NSKeyValueChangeReplacement) {
            NSLog(@"集合內容替換 索引為:%@", change[NSKeyValueChangeIndexesKey]);
        }
    }
}
複製程式碼

移除觀察者物件

通過想觀察者傳送removeObserver:forKeyPath:context訊息來移除觀察者物件。收到該訊息後,觀察者物件將不再接收任何observerValueForKeyPath:ofObject:change:context中指定key path/object的訊息。

刪除觀察者時,注意:

  • 移除和新增的方法要保持對稱,否則會引發異常。如果無法保持對稱,則把移除的方法放到try/catch塊中。
  • 物件釋放時,不會自動把自己從觀察者中移除,此時被觀察者繼續傳送通知。但是就像任何其他訊息一樣,改變的通知傳送給了已經釋放的物件會觸發記憶體訪問異常。因此,務必在觀察者從記憶體中消失前,將其移除
  • 協議沒有提供方法來查詢一個物件是否是觀察者或被觀察者。你必須在程式碼中自行避免錯誤。典型的方案是在觀察者初始化期間(init或dealloc)註冊為觀察者,並在釋放時(dealloc)登出。

相容KVO

為了讓特定屬性符合KVO標準,class必須滿足一下內容:

  • 該類必須是符合該屬性的KVC
  • 該類會為該屬性觸發KVO通知
  • 相關的key已經被成功註冊

有兩種技術可確保發出KVO通知。NSObject提供自動支援,預設情況下可用於符合鍵值編碼的類的所有屬性。通常,如果你遵守Cocoa編碼和命名約定,則可以使用自動通知,而不必編寫任何程式碼。

手動方式為通知觸發時提供了更多的控制權,並且需要額外編碼。你可以通過實現automaticallyNotifiesObserversForKey:來控制子類屬性的自動通知。

自動通知

下列方法列舉了會觸發自動通知的一些場景:

//呼叫訪問器方法。
[account setName:@“Savings”];
 
//使用setValue:forKey:。
[account setValue:@“Savings”forKey:@“name”];
 
//使用金鑰路徑,其中'account'是'document'的kvc相容屬性。
[document setValue:@“Savings”forKeyPath:@“account.name”];
 
//使用mutableArrayValueForKey:檢索關係代理物件。
Transaction * newTransaction = <#為帳戶#>建立新交易;
NSMutableArray * transactions = [account mutableArrayValueForKey:@“transactions”];
[transactions addObject:newTransaction];
複製程式碼

手動通知

有些情況下,你可能想要控制通知的過程,例如,最大限度減少因應用程式特定原因而不必要的觸發通知,或把一組通知整合到一個。

手動通知和自動通知不是互斥的。手動和自動的通知可以同時觸發。如果你只想要手動觸發,則需要通過重寫automaticallyNotifiesObserversForKey:方法來禁止自動通知。

+ (BOOL)automaticNotifiesObserversForKey:(NSString *)theKey {
    BOOL automatic = NO;
    if ([theKey isEqualToString:@“balance”]) {
        automatic = NO;
    }
    else {
        automatic = [super automaticNotifiesObserversForKey: theKey];
    }
    return automatic;
}
複製程式碼

**要實現手動觀察者通知,你要在值改變前呼叫willChangeValueForKey:,並在值改變後呼叫didChangeValueForKey:。有三組類似的方法:

  • willChangeValueForKey:didChangeValueForKey:。用於單個物件
  • willChange:valuesAtIndexes:forKey:didChange:valuesAtIndexes:forKey:。用於有序集合
  • willChangeValueForKey:withSetMutation:usingObjects:willChangeValueForKey:withSetMutation:usingObjects:。用於無須集合

下面在訪問器方法中手動觸發:

- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    _balance = theBalance;
    [self didChangeValueForKey:@"balance"];
}
複製程式碼

為了減少不必要的通知,可以先檢查值是否改變了,然後決定是否發通知:

- (void)setBalance:(double)theBalance {
    if (theBalance != _balance) {
        [self willChangeValueForKey:@"balance"];
        _balance = theBalance;
        [self didChangeValueForKey:@"balance"];
    }
}
複製程式碼

如果一個操作導致多個key發生改變,必須巢狀傳送通知:

- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    [self willChangeValueForKey:@"itemChanged"];
    _balance = theBalance;
    _itemChanged = _itemChanged+1;
    [self didChangeValueForKey:@"itemChanged"];
    [self didChangeValueForKey:@"balance"];
}
複製程式碼

在有序的to-many關係中,除了指定更改的key,還不許指定更改的型別和所涉及物件的索引。

- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
    [self willChange:NSKeyValueChangeRemoval
        valuesAtIndexes:indexes forKey:@"transactions"];
 
    // Remove the transaction objects at the specified indexes.
 
    [self didChange:NSKeyValueChangeRemoval
        valuesAtIndexes:indexes forKey:@"transactions"];
}
複製程式碼

註冊從屬keys

在許多情況下,一個屬性的值取決於另一個物件中的一個或多個其他屬性的值。如果一個屬性的值發生更改,則還應標記派生屬性的值以進行更改。

To-One 關係

要為一對一關係自動觸發通知,應該重寫keyPathsForValuesAffectingValueForKey或實現一個合適的方法,該方法遵循它為註冊依賴鍵定義的模式。

例如,fullName取決於firstNamelastName。返回fullName的方法可以寫成如下:

- (NSString *)fullName {
    return [NSString stringWithFormat:@“%@%@”,firstName,lastName];
}
複製程式碼

firstNamelastName發生改變時,必須通知觀察fullName屬性的程式,因為它們影響這個屬性的值。

一個解決方案是重寫keyPathsForValuesAffectingValueForKey來指定fullName屬性依賴於lastNamefirstName

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
 
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
 
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
複製程式碼

重寫通常應該呼叫super並返回一個集合,以免影響超類中對此方法的重寫。

通過重寫keyPathsForValuesAffecting<Key>也可以達到相同的效果。

+ (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
複製程式碼

To-many 關係

keyPathsForValuesAffectingValueForKey:方法不支援to-many關係的key paths。可以使用下面兩種方案來處理to-many 關係:

  1. 使用key-value observing註冊父項作為子項的相關屬性觀察者。當子物件新增到關係或從關係中刪除的時候,你必須新增或刪除父物件。在observeValueForKeyPath:ofObject:change:context:方法中,你可以更新依賴值以相應更改,如下:
[self addObserver:self forKeyPath:@"transactions" options:NSKeyValueObservingOptionNew context:NULL];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"amount = %@", [self valueForKeyPath:@"transactions.@sum.amount"]);
    [self setTotalConsumption:[self valueForKeyPath:@"transactions.@sum.amount"]];
}
複製程式碼
  1. 在Core Data中,則可以將父項作為其託管物件上下文的觀察者註冊到應用程式的通知中心。父項應以類似於鍵值觀察的方式迴應孩子們釋出的相關變更通知。

Key-Value Observing 的實現細節

自動key-value observing 是使用一種叫做isa-swizzling的技術實現的。

isa指標指向維護一個排程表(dispatch table)的物件的類。該排程表包含了指向該類實現的方法的指標,以及其他資料。

當觀察者註冊物件的屬性時,觀察物件的isa指標被修改,指向中間類而不是真正的類。因此,isa指標的值不一定反映例項的實際類。

永遠不要依賴isa指標來確定類成員。而應該使用class方法來決定例項所屬的類。

參考連線

示例程式碼

KVC

KVO