KVO詳解

KVO介紹

KVO允許在對象的指定屬性發生變化時獲取通知。這是非常有用的對於模型和控制器層的通訊。控制器對象觀察模型的屬性,視圖對象通過控制器觀察模型的屬性。除外,模型對象可以觀察其他模型對象,甚至是自己。

你可以觀察的屬性包括簡單屬性(attributes),一對一關係,一對多關係。觀察一對多關係的對象會獲取包含屬性變化類型和觸發對象的通知。

註冊KVO

你必須執行下面步驟來激活對象獲取指定屬性的KVO通知:

• 使用被觀察者的addObserver:forKeyPath:options:context:方法來註冊觀察者。

• 觀察者實現observeValueForKeyPath:ofObject:change:context:方法來接收相關通知。

• 當不再接收通知時,調用被觀察者的removeObserver:forKeyPath:方法來移除觀察者。調用之前觀察者沒有被釋放內存。

註冊成爲觀察者

成爲觀察者的第一步是向被觀察者調用addObserver:forKeyPath:options:context:方法來註冊成爲觀察者。這個方法需要提供觀察者對象和需要觀察的屬性鍵路徑,你還可以提供options可選參數和context上下文指針。

Options

這個options參數是一個按位可選常量,它影響通知的變化字典內容和通知行爲。

你可以指定NSKeyValueObservingOptionOld可選常量來獲取觀察屬性改變前的值。你也可以指定NSKeyValueObservingOptionNew來獲取屬性改變後的值。你也可以通過位或這些可選常量來同時獲取這兩個值。

你可以指定NSKeyValueObservingOptionInitial來立即獲取被觀察者對象的屬性變化通知(在addObserver:forKeyPath:options:context:前的狀態)。你可以在觀察者那裏使用這個額外的,一次性的初始化屬性值。

你可以指定NSKeyValueObservingOptionPrior來獲取屬性之前變化的通知(不同於在屬性變化後才發送的普通通知)。通知的改變字典會包含NSKeyValueChangeNotificationIsPriorKey的鍵以及對應的YES的NSNumber值。這個鍵在其他情況是不會出現的。當你需要觸發被觀察者的willChange…方法,這個方法是對應觀察者的某個依賴被觀察者屬性值的屬性,你可以使用這個屬性變化前通知來處理這種情況。通常,變化前通知可能來得太晚而不能觸發willChange…方法。

Context

addObserver:forKeyPath:options:context:方法的context指針是一個包含任意數據的指針,這個指針可以在觀察者對應的變化通知內獲取。你可以傳遞NULL並完全依賴鍵路徑字符串來決定變化通知的來源。但是這種方法可能會造成一些問題,例如對於觀察者的父類同樣觀察同一對象的鍵路徑屬性值。

一個安全、可擴展的方式是使用context上下文來確保通知發送到觀察者而不是它的父類。

你可以使用一個唯一命名的靜態變量地址作爲上下文。父類和子類的上下文選擇將不太可能重疊。你也可以使用類作爲上下文和鍵路徑字符串來獲取通知的變化。另外,你可以爲每個鍵路徑創建不同的上下文,這樣可以完全繞過字符串比較,實現更高效的通知解析。

static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
- (void)registerAsObserverForAccount:(Account*)account {
    [account addObserver:self
              forKeyPath:@"balance"
                 options:(NSKeyValueObservingOptionNew |
                          NSKeyValueObservingOptionOld)
                 context:PersonAccountBalanceContext];
 
    [account addObserver:self
              forKeyPath:@"interestRate"
                 options:(NSKeyValueObservingOptionNew |
                          NSKeyValueObservingOptionOld)
                  context:PersonAccountInterestRateContext];
}

注意:addObserver:forKeyPath:options:context: 方法不會強引用觀察者和被觀察者以及上下文指針。

接收變化通知

當被觀察的屬性值發生變化,觀察者會收到observeValueForKeyPath:ofObject:change:context:通知。所有觀察者必須實現這個方法。

通知提供觸發的鍵路徑、被觀察者、變化字典、context上下文。

變化字典提供NSKeyValueChangeKindKey來獲取變化類型。如果被觀察對象的屬性值改變,這個NSKeyValueChangeKindKey返回NSKeyValueChangeSetting。根據註冊觀察者的options值,NSKeyValueChangeOldKey和NSKeyValueChangeNewKey返回屬性變化前和變化後的值。如果屬性值是對象,那麼直接返回。如果屬性值是數值或者是C結構體,這個值會包裝成NSValue對象。

如果觀察的屬性是一對多關係。NSKeyValueChangeKindKey會返回NSKeyValueChangeInsertion、NSKeyValueChangeRemoval、NSKeyValueChangeReplacement來指示一對多屬性變化是否是插入、刪除、替換。

變化字典提供NSKeyValueChangeIndexesKey來獲取一對多屬性(有下標關係的)的下標變化值(NSIndexSet)。NSKeyValueChangeOldKey和NSKeyValueChangeNewKey返回屬性變化前和變化後的數組值。

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
 
    if (context == PersonAccountBalanceContext) {
        // Do something with the balance…
 
    } else if (context == PersonAccountInterestRateContext) {
        // Do something with the interest rate…
 
    } else {
        // Any unrecognized context must belong to super
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                               context:context];
    }
}
移除觀察者

你通過調用被觀察者的removeObserver:forKeyPath:context:方法來移除觀察者。

- (void)unregisterAsObserverForAccount:(Account*)account {
    [account removeObserver:self
                 forKeyPath:@"balance"
                    context:PersonAccountBalanceContext];
 
    [account removeObserver:self
                 forKeyPath:@"interestRate"
                    context:PersonAccountInterestRateContext];
}

在收到removeObserver:forKeyPath:context:方法後,觀察者將不會收到指定鍵路徑的observeValueForKeyPath:ofObject:change:context:通知。

當你要移除觀察者,你需要留意幾點:

• 如果要移除的觀察者沒有在被觀察者中註冊,系統會拋出NSRangeException異常。你應該調用一次removeObserver:forKeyPath:context:方法,對應addObserver:forKeyPath:options:context:的調用,或者這不太可能實現,你可以在try/catch中調用removeObserver:forKeyPath:context:方法並處理潛在的異常。

• 觀察者不會在被釋放內存前自動移除自己。被觀察者會無視觀察者的狀態並繼續發送通知。但是,這個變化通知像其他消息一樣,發送給已釋放的對象會觸發內存訪問異常。因此你要確保觀察者在釋放內存前移除它。

• KVO沒有提供方法來確定對象是觀察者還是被觀察者。構建你的代碼來避免造成相關錯誤。一個通用的模式是在觀察者初始化前註冊爲觀察者(init或者viewDidLoad),在釋放內存前移除觀察者(dealloc),確保屬性配對並按添加順序來移除觀察者並確保它沒有被釋放內存。


服從KVO

爲了確認指定的屬性符合KVO機制,這個類確保符合下面的內容:

• 屬性必須符合KVO。KVO支持KVC的簡單數據,包括OC對象、KVC支持的數值和結構體。

• 這個類能夠發送這個屬性的KVO變化通知。

• 依賴的鍵被適當地註冊。

這裏有兩種技術能夠確保變化通知能被髮送。NSObject默認自動支持發送並且對所有符合KVC的屬性都支持。通常,如果你符合標準的cocoa代碼編寫和命名規則,你可以使用這種自動變化通知,你不必編寫任何額外的代碼。

手動變化通知提供額外的控制通知什麼時候發送,這需要編寫額外的代碼。你可以通過子類重寫automaticallyNotifiesObserversForKey:類方法來控制哪些屬性是自動變化通知。

自動變化通知

NSObject提供基本的自動鍵值變化通知。自動鍵值變化通知會通知觀察者鍵值訪問造成的變化,以及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];
手動變化通知

在一些情況下,你可能控制通知的進程,例如,減少觸發通知的次數可能對於某些應用來說不必要,或者組合一組變化爲一個單獨通知。手動變化通知提供一些方式來完成這些工作。

手動和自動通知並不是互斥的。你可以在自動通知的屬性發送手動通知。你可能想要完全地控制特定屬性的進程。在這種情況下,你需要重寫NSObject的automaticallyNotifiesObserversForKey:方法。對於一些不使用自動變化通知的屬性,你應該在automaticallyNotifiesObserversForKey:方法返回NO。子類應該調用父類的方法來處理不識別的鍵。

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

爲了實現手動變化通知,你在改變屬性值前調用willChangeValueForKey:方法,在改變屬性值後調用didChangeValueForKey:方法。

- (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"];
    }
}

如果一個單獨的操作會造成多個鍵的屬性值變化,你必須嵌套變化通知。

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

在有序一對多關係中,你必須不僅指定這個鍵發生變化,還要指定變化類型和改變下標。NSKeyValueChange變化類型可以指定NSKeyValueChangeInsertion、NSKeyValueChangeRemoval、NSKeyValueChangeReplacement。影響的下標傳遞NSIndexSet對象。

- (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"];
}


註冊依賴鍵

有很多情形涉及一個屬性的值依賴於一個或者多個其他對象的屬性值。如果其中一個屬性值改變,依賴的屬性應該被標記爲改變。你怎樣確保那些依賴屬性的KVO通知的多個依賴關係。

一對一關係

爲了自動觸發一對一關係通知,你應該重寫keyPathsForValuesAffectingValueForKey:方法或者適當地實現註冊依賴鍵模式的方法。

下面的例子提供一個依賴屬性。

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

當firstName或者lastName發生變化時,監聽fullName屬性的觀察者應該被通知。

一種方法是重寫keyPathsForValuesAffectingValueForKey:方法來指定fullName屬性依賴firstName和lastName屬性。

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

通常你應該調用父類方法來獲取包含任何成員的集合(set)並處理它(而不是完全重寫父類的方法)。

你可以實現一些類方法來實現相同的結果,這些方法的命名符合keyPathsForValuesAffecting<Key>,這裏的<Key>爲依賴屬性的名稱(首字符爲大寫開頭)。使用這種方式來重寫keyPathsForValuesAffectingFullName:類方法。

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

當你在已有類的種類中添加計算性屬性,你不能重寫keyPathsForValuesAffectingValueForKey:方法,因爲種類不支持重寫方法。在這種情況下,實現keyPathsForValuesAffecting<Key> 類方法來實現這種機制。

一對多關係

keyPathsForValuesAffectingValueForKey:方法不支持一對多關係的鍵路徑。

這裏有2種方式來處理這種情況:

1. 你可以使用KVO來註冊父對象(一對多的父對象)作爲子對象(一對多的子對象)的相關屬性的觀察者,你必須爲每一個子對象添加和刪除父對象觀察者。

- (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:@"[email protected]"]];
}
 
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
 
    if (totalSalary != newTotalSalary) {
        [self willChangeValueForKey:@"totalSalary"];
        _totalSalary = newTotalSalary;
        [self didChangeValueForKey:@"totalSalary"];
    }
}
 
- (NSNumber *)totalSalary {
    return _totalSalary;
}

2. 如果你使用Core Data,你可以註冊父對象爲應用的通知中心觀察者,這個父對象管理對象上下文。父對象應該響應子對象發送的變化通知。


KVO實現細節

自動鍵值觀察的實現使用isa-swizzling技術。

isa指針就像暗示的那樣,指向對象的類,這個類管理一個派發表。這個派發表本質上是包含類定義的方法和其他數據的指針,。

當觀察者註冊成爲某個屬性的觀察者,被觀察對象的isa指針會被修改,指向這個類的派生類而不是實際的類。結果是isa指針的值不在影響實際實例的類。

你不應該依賴isa指針來確定類的成員關係,而是使用class類方法來確定實例的類。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章