iOS KVO(鍵值觀察) 總覽

原文鏈接 Cyrus'blog

本文主要內(nèi)容來自于對官方文檔 Key-Value Observing Programming Guide 的翻譯,以及一部分我自己的理解和解釋,如果有說錯的地方請及時聯(lián)系我。

At a Glance

KVO 也就是 鍵值觀察 ,它提供了一種機(jī)制,使得當(dāng)某個對象特定的屬性發(fā)生改變時能夠通知到別的對象。這經(jīng)常用于 model 和 controller 之間的通信。KVO主要的優(yōu)點(diǎn)是你不需要在每次屬性改變時手動去發(fā)送通知。并且它支持為一個屬性注冊多個觀察者。

注冊 KVO

  • 被觀察對象 的屬性必須是 [KVO Compliant](file:///Users/hcy/Library/Developer/Shared/Documentation/DocSets/com.apple.adc.documentation.iOS.docset/Contents/Resources/Documents/documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVOCompliance.html#//apple_ref/doc/uid/20002178-BAJEAIEE)
  • 必須用 被觀察對象addObserver:forKeyPath:options:context: 方法注冊觀察者
  • 觀察者 必須實(shí)現(xiàn) observeValueForKeyPath:ofObject:change:context: 方法

注冊成為觀察者


為了能夠在屬性改變時被通知到,一個 觀察者對象 必須通過 被觀察對象addObserver:forKeyPath:options:context: 方法注冊成為觀察者。

  • observer 參數(shù)也就是一個觀察者對象

  • keyPath 表示要觀察的屬性

  • options 決定了提供給觀察者change字典中的具體信息有哪些。(change字典是一個提供給觀察者的參數(shù),后面會提到)

    • NSKeyValueObservingOptionOld 表示在change字典中包含了改變前的值。
    • NSKeyValueObservingOptionNew 表示在change字典中包含新的值。
    • NSKeyValueObservingOptionInitial 在注冊觀察者的方法return的時候就會發(fā)出一次通知。
    • NSKeyValueObservingOptionPrior 會在值發(fā)生改變前發(fā)出一次通知,當(dāng)然改變后的通知依舊還會發(fā)出,也就是每次change都會有兩個通知。
  • context 這個參數(shù)可以是一個 C指針,也可以是一個 對象引用,它可以作為這個context的唯一標(biāo)識,也可以提供一些數(shù)據(jù)給觀察者。

注意: addObserver:forKeyPath:options:context: 方法不會持有觀察者對象,被觀察對象,以及context的強(qiáng)引用。你要確保自己持有了他們的強(qiáng)引用。

屬性變化時接收通知


當(dāng)一個被觀察屬性的值發(fā)生改變時,觀察者會收到 observeValueForKeyPath:ofObject:change:context: 的消息。所有的觀察者必須實(shí)現(xiàn)這個方法。這個方法中的參數(shù)和注冊觀察者方法的參數(shù)基本相同,只有一個 change 不同。 change 是一個字典,它里面包含了的信息由注冊時的 options 決定。

官方提供了這些key給我們來取到 change 中的value:

NSString *const NSKeyValueChangeKindKey;
NSString *const NSKeyValueChangeNewKey;
NSString *const NSKeyValueChangeOldKey;
NSString *const NSKeyValueChangeIndexesKey;
NSString *const NSKeyValueChangeNotificationIsPriorKey;
  • NSKeyValueChangeKindKey 這個key包含的value是一個 NSNumber 里面是一個 int,與之對應(yīng)的是 NSKeyValueChange 的枚舉
enum {
  NSKeyValueChangeSetting = 1,
  NSKeyValueChangeInsertion = 2,
  NSKeyValueChangeRemoval = 3,
  NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;

當(dāng) change[NSKeyValueChangeKindKey]NSKeyValueChangeSetting 的時候,說明被觀察屬性的setter方法被調(diào)用了。
而下面三種,根據(jù)官方文檔的意思是,當(dāng)被觀察屬性是集合類型,且對它進(jìn)行了 insert,remove,replace 操作的時候會返回這三種Key,但是我自己測試的時候沒有測試出來??不知道是不是我理解錯了。

  • NSKeyValueChangeNewKey,NSKeyValueChangeOldKey 顧名思義,當(dāng)你在注冊的時候 options 參數(shù)中填了對應(yīng)的 NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld ,并且 NSKeyValueChangeKindKey 的值是 NSKeyValueChangeSetting ,你就可以通過這兩個key取到 舊值和新值。

  • NSKeyValueChangeIndexesKey, 當(dāng) NSKeyValueChangeKindKey 的結(jié)果是 NSKeyValueChangeInsertion, NSKeyValueChangeRemovalNSKeyValueChangeReplacement 的時候,這個key的value是一個NSIndexSet,包含了發(fā)生insert,remove,replace的對象的索引集合

  • NSKeyValueChangeNotificationIsPriorKey,這個key包含了一個 NSNumber,里面是一個布爾值,如果在注冊時 options 中有 NSKeyValueObservingOptionPrior,那么在前一個通知中的 change 中就會有這個key的value, 我們可以這樣來判斷是不是在改變前的通知[change[NSKeyValueChangeNotificationIsPriorKey] boolValue] == YES;

移除一個觀察者


你可以通過 removeObserver:forKeyPath: 方法來移除一個觀察。如果你的 context 是一個 對象,你必須在移除觀察之前持有它的強(qiáng)引用。當(dāng)移除了觀察后,觀察者對象再也不會受到這個 keyPath 的通知。

KVO Compliance

有兩種方式能夠保證 change notification 能夠被發(fā)出。

  • 自動通知,繼承自NSObject,并且所有的屬性符合[KVC規(guī)范](file:///Users/hcy/Library/Developer/Shared/Documentation/DocSets/com.apple.adc.documentation.iOS.docset/Contents/Resources/Documents/documentation/Cocoa/Conceptual/KeyValueCoding/Articles/Compliant.html#//apple_ref/doc/uid/20002172)這樣就不用寫額外的代碼去實(shí)現(xiàn)自動通知。
  • 手動通知,讓你的子類實(shí)現(xiàn) automaticallyNotifiesObserversForKey: 方法,來決定是否需要自動通知,如果是手動通知需要額外的代碼。

自動通知


NSObject 已經(jīng)實(shí)現(xiàn)了自動通知,只要通過 setter 方法去賦值,或者通過 KVC 就可以通知到觀察者。自動通知也支持集合代理對象,比如 mutableArrayValueForKey: 方法。

// Call the accessor method.
[account setName:@"Savings"];

// Use setValue:forKey:.
[account setValue:@"Savings" forKey:@"name"];

// Use a key path, where 'account' is a kvc-compliant property of 'document'.
[document setValue:@"Savings" forKeyPath:@"account.name"];

// Use mutableArrayValueForKey: to retrieve a relationship proxy object.
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];

手動通知


手動通知提供了更自由的方式去決定什么時間,什么方式去通知觀察者。這可以幫助你最少限度觸發(fā)不必要的通知,或者一組改變值發(fā)出一個通知。想要使用手動通知必須實(shí)現(xiàn)automaticallyNotifiesObserversForKey: 方法。(或者automaticallyNotifiesObserversOfS<Key>)在一個類中同時使用自動和手動通知是可行的。對于想要手動通知的屬性,可以根據(jù)它的keyPath返回NO,而其對于其他位置的keyPath,要返回父類的這個方法。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
       BOOL automatic = NO;
       if ([theKey isEqualToString:@"openingBalance"]) {
           automatic = NO;
       } else {
           automatic = [super automaticallyNotifiesObserversForKey:theKey];
       }
       return automatic;
}

要實(shí)現(xiàn)手動通知,你需要在值改變前調(diào)用 willChangeValueForKey: 方法,在值改變后調(diào)用 didChangeValueForKey: 方法。你可以在發(fā)送通知前檢查值是否改變,如果沒有改變就不發(fā)送通知

- (void)setOpeningBalance:(double)theBalance {
       if (theBalance != _openingBalance) {
        [self willChangeValueForKey:@"openingBalance"];
        _openingBalance = theBalance;
        [self didChangeValueForKey:@"openingBalance"];
       }
}

如果一個操作會導(dǎo)致多個屬性改變,你需要嵌套通知,像下面這樣:

- (void)setOpeningBalance:(double)theBalance {
       [self willChangeValueForKey:@"openingBalance"];
       [self willChangeValueForKey:@"itemChanged"];
       _openingBalance = theBalance;
       _itemChanged = _itemChanged+1;
       [self didChangeValueForKey:@"itemChanged"];
       [self didChangeValueForKey:@"openingBalance"];
}

在一個一對多的關(guān)系中,你必須注意不僅僅是這個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"];
}

鍵之間的依賴

在很多種情況下一個屬性的值依賴于在其他對象中的屬性。如果一個依賴屬性的值改變了,這個屬性也需要被通知到。

To-one Relationships


比如有一個教 fullName 的屬性,依賴于 firstNamelastName,當(dāng) firstName 或者 lastName 改變時,這個 fullName 屬性需要被通知到。

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}

你可以重寫 keyPathsForValuesAffectingValueForKey: 方法。其中要先調(diào)父類的這個方法拿到一個set,再做接下來的操作。

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

你也可以通過實(shí)現(xiàn) keyPathsForValuesAffecting<Key> 方法來達(dá)到前面同樣的效果,這里的<Key>就是屬性名,不過第一個字母要大寫,用前面的例子來說就是這樣:

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

To-many Relationships


keyPathsForValuesAffectingValueForKey:方法不能支持 to-many 的關(guān)系。舉個例子,比如你有一個 Department 對象,和很多個 Employee 對象。而 Employee 有一個 salary 屬性。你可能希望 Department 對象有一個 totalSalary 的屬性,依賴于所有的 Employee 的 salary 。

你可以注冊 Department 成為所有 Employee 的觀察者。當(dāng) Employee 被添加或者被移除時,你必須要添加和移除觀察者。然后在 observeValueForKeyPath:ofObject:change:context: 方法中,根據(jù)改變做出反饋。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (context == totalSalaryContext) {
        [self updateTotalSalary];
    }
    else
    // deal with other observations and/or invoke super...
}
 
- (void)updateTotalSalary {
    [self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
}
 
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
 
    if (totalSalary != newTotalSalary) {
        [self willChangeValueForKey:@"totalSalary"];
        _totalSalary = newTotalSalary;
        [self didChangeValueForKey:@"totalSalary"];
    }
}
 
- (NSNumber *)totalSalary {
    return _totalSalary;
}

KVO的實(shí)現(xiàn)細(xì)節(jié)

KVO 的實(shí)現(xiàn)用了一種叫 isa-swizzling 的技術(shù)。isa 指針就是指向類的指針,當(dāng)一個對象的一個屬性注冊了觀察者后,被觀察對象的isa指針的就指向了一個系統(tǒng)為我們生成的中間類,而不是我們自己創(chuàng)建的類。在這個類中,系統(tǒng)為我們重寫了被觀察屬性的setter方法。你可以通過 object_getClass(id obj) 方法獲得對象真實(shí)的類,在 addObserver 前后分別打印,就可以看到isa指針被指向了一個中間類。似乎都是在原來的類名前面加上 NSKVONotifying_

isa指針不總是指向真實(shí)的類,所以你不應(yīng)該依賴于 isa 指針來判斷這個對象的類型,而應(yīng)該通過 class 方法來判斷對象的類型。如果你還不知道什么是isa指針,可以看我之前寫的博客 Objective-C runtime 的簡單理解與使用

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容