Key-Value Observing

官方鏈接.

Introduction

Key-value observing(KVO) 是一種機(jī)制,它允許對(duì)象在 更改其他對(duì)象的指定屬性時(shí) 得到通知。

At a Glance

在應(yīng)用中在 modelcontroller 層間通訊非常有用(在 OS X中, controllerbinding 技術(shù)嚴(yán)重依賴 KVO), controller 同時(shí)觀察 model 的屬性, view 通過(guò) controller 觀察 model 的屬性. 另外, model 可能觀察其他 model 甚至是 model 本身(通常由依賴值決定).

可以觀察包括簡(jiǎn)單的屬性, to-one and to-many 關(guān)系. to-many 的觀察者被通知 change 的類型就像改變涉及的值一樣.
簡(jiǎn)單例子, 假設(shè) Person 有一個(gè) Account 屬性, 表現(xiàn)為 person 保存 account 到銀行. Person 實(shí)例可能需要知道 Account 實(shí)例的某些方面何時(shí)發(fā)生更改弥咪,比如余額或利率涕癣。
如果 Account 的屬性時(shí)公有屬性, 則 Person 可以通過(guò)輪詢查看 changes, 這種方式顯然低效, 更好的方法是使用 KVO, 在 change 時(shí), 類似 Person 接收一個(gè)分岔.
使用 KVO, 必須確定被觀察對(duì)象, Account 是否符合. 通常, 如果對(duì)象繼承 NSObject, 使用一般方式創(chuàng)建的屬性, 都自動(dòng)符合 KVO, 也可以手動(dòng)實(shí)現(xiàn)遵從性. KVO Compliance 描述了自動(dòng)和手動(dòng) KVO 以及怎樣實(shí)現(xiàn).
下一步, 必須注冊(cè)觀察者實(shí)例, 例如 Person, 被觀察者 Account, Person 發(fā)送一個(gè) addObserver:forKeyPath:options:context: 消息給 Account, 為每個(gè)被觀察的 key-path仗岸,并將自己命名為觀察者耿焊。

image.png

為了接收 Account 改變的消息, Person 實(shí)現(xiàn) observeValueForKeyPath:ofObject:change:context: 方法, 每個(gè)觀察者都必須實(shí)現(xiàn). Account 在注冊(cè)的 key-path 發(fā)生改變時(shí)發(fā)送消息給 Person. Person 作相應(yīng)處理

image.png

最終, 當(dāng)它不想收到消息, 至少要在它析構(gòu)之前 Person 實(shí)例必須通過(guò)發(fā)送 removeObserver:forKeyPath: 消息給 Account 注銷.

image.png

Registering for Key-Value Observing 描述了 KVO 從注冊(cè)到注銷的生命周期.
KVO 主要好處就是不需要實(shí)現(xiàn)自己的方案來(lái)在每次屬性更改時(shí)發(fā)送通知. 具有良好的基礎(chǔ)設(shè)置具有框架級(jí)的支持, 使其便于采用--通常你不需要想工程中添加任何代碼. 此外,基礎(chǔ)設(shè)施已經(jīng)功能齊全怀浆,這使得支持單個(gè)屬性的多個(gè)觀察者以及相關(guān)值變得很容易谊囚。
Registering Dependent Keys 解釋了如何定義一個(gè) key 依賴另一個(gè) key.
不像 NSNotificationCenter, 它沒有中心對(duì)象給所有觀察者提供 change 消息. 當(dāng)發(fā)生 change 時(shí),消息直接被發(fā)送給觀察的對(duì)象, 這是 NSObject 提供 KVO 的基本實(shí)現(xiàn).

Key-Value Observing Implementation Details KVO 是如何實(shí)現(xiàn)的.

Registering for Key-Value Observing

必須執(zhí)行以下步驟以讓一個(gè)對(duì)象可以接收遵循 KVO 屬性的 KVO 消息:

  • addObserver:forKeyPath:options:context: 方法給被觀察者注冊(cè)觀察者.
  • 在觀察者內(nèi)部實(shí)現(xiàn) observeValueForKeyPath:ofObject:change:context: 方法接收通知消息.
  • 使用 removeObserver:forKeyPath: 注銷觀察者當(dāng)它不想收到消息使, 最低限度, 在觀察者從內(nèi)存被釋放之前調(diào)用.

并非所有類的屬性都符合 KVO. KVO Compliance中描述如何確定類是否符合 KVO, 通常蘋果提供的 frameworks 中的屬性只有在被文檔化時(shí)才符合 KVO.

Registering as an Observer

addObserver:forKeyPath:options:context::

  • Options
    選項(xiàng)參數(shù)指定為位或選項(xiàng)常量,它影響通知中提供的更改字典的內(nèi)容执赡,以及生成通知的方式镰踏。
    NSKeyValueObservingOptionOld(change 前舊值) |
    NSKeyValueObservingOptionNew(change 后新值) |
    NSKeyValueObservingOptionInitial( addObserver:forKeyPath:options:context: 方法返回前) |
    NSKeyValueObservingOptionPrior(在屬性 change 之前的通知, 普通通知在其后, 通過(guò) NSKeyValueChangeNotificationIsPriorKey 獲取值為 NSNumber 轉(zhuǎn)換為 YES, 當(dāng)觀察者自身的 KVO遵從性要求它為其依賴于所觀察屬性的屬性調(diào)用 -willChange… 之一時(shí), 一般的通知來(lái)得太晚, 無(wú)法及時(shí)調(diào)用 willChange...)

  • Context
    context 指針會(huì)在觀察者對(duì)應(yīng)的消息中傳遞. 可以指定為 NULL 也可以完全依賴于 keypath 字符串決定通知的初始化. 但是這會(huì)導(dǎo)致對(duì)象父類(也實(shí)現(xiàn)了一個(gè)相同 keypath 的觀察)一些不明原因的問題.
    一個(gè)更安全可擴(kuò)展的方法就是通過(guò) context 保證通知是你指定的觀察者而不是父類.
    類中唯一命名的靜態(tài)變量的地址是一個(gè)很好的上下文∩澈希可以為整個(gè)類選擇上下文, 并依賴 keypath:

static void * PersonAccountBalanceContext =&PersonAccountBalanceContext;
static void * PersonAccountInterestRateContext =&PersonAccountInterestRateContext;

將自身注冊(cè)為 Account 實(shí)例 balanceinterestRate 屬性的觀察者:

- (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: 方法不保持對(duì)觀察者奠伪,被觀察對(duì)象或上下文的強(qiáng)引用。您應(yīng)該確保在必要時(shí)保持對(duì)觀察者首懈,被觀察對(duì)象和上下文的強(qiáng)引用绊率。

Receiving Notification of a Change

當(dāng)對(duì)象的屬性發(fā)生變化時(shí), 觀察者會(huì)接收到 observeValueForKeyPath:ofObject:change:context: 消息.
change 字典 key NSKeyValueChangeKindKey 包含發(fā)生改變的信息. 如果被觀察的值發(fā)生變化, NSKeyValueChangeKindKey 返回 NSKeyValueChangeSetting. NSKeyValueChangeOldKey and NSKeyValueChangeNewKey 包含屬性的變化的前后值. 如果屬性是一個(gè)對(duì)象, 值直接被提供, 如果是一個(gè)標(biāo)量或一個(gè) C 結(jié)構(gòu)體, 會(huì)轉(zhuǎn)換為 NSValue 對(duì)象(as KVC).
如果被觀察對(duì)象是一個(gè) to-many 的關(guān)系, NSKeyValueChangeKindKey 會(huì)標(biāo)識(shí)是NSKeyValueChangeInsertion(插入), NSKeyValueChangeRemoval(刪除), or NSKeyValueChangeReplacement(替換).
NSKeyValueChangeIndexesKey 是一個(gè) NSIndexSet 對(duì)象指定變化的關(guān)系索引.
如果注冊(cè)觀察者時(shí)指定 NSKeyValueObservingOptionNew or NSKeyValueObservingOptionOld options, 則 change 字典中 NSKeyValueChangeOldKey and NSKeyValueChangeNewKey 返回關(guān)聯(lián)對(duì)象改變前/后 的值數(shù)組.

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

注意: 如果通知傳播到類層次結(jié)構(gòu)的頂部,則 NSObject 拋出一個(gè) NSInternalInconsistencyException 因?yàn)檫@是一個(gè)編程錯(cuò)誤:子類無(wú)法使用它注冊(cè)的通知究履。

Removing an Object as an Observer

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

移除觀察者記住以下幾點(diǎn):

  • 移除一個(gè)未注冊(cè)的觀察者會(huì)導(dǎo)致 NSRangeException. 只需要為對(duì)應(yīng)于 addObserver:forKeyPath:options:context: 調(diào)用一次 removeObserver:forKeyPath:context:, 如果不可行, 則可以將 removeObserver:forKeyPath:context: 放到 try/catch block 中處理潛在的異常滤否。
  • 當(dāng)觀察者析構(gòu)時(shí)不會(huì)自動(dòng)移除. 被觀察對(duì)象繼續(xù)發(fā)送消息, 像發(fā)送一個(gè)消息給已經(jīng)釋放的對(duì)象, 觸發(fā) exception.
  • 無(wú)法判斷一個(gè)對(duì)象是否是觀察者或被觀察對(duì)象. 避免這種情況. 典型的方式是在觀察者初始化的時(shí)候注冊(cè)(init or viewDidLoad), 在對(duì)象析構(gòu)時(shí)注銷(dealloc), 確保正確配對(duì)和排序添加和刪除消息, 觀察者在釋放之前注銷.

KVO Compliance

遵循 KVO, 類必須確保:

  • 指定的屬性遵循 KVC, KVO 同 KVC 一樣支持相同的數(shù)據(jù)類型.
  • 由類為屬性變化通知.
  • 相關(guān) key 已正確注冊(cè).

NSObject 默認(rèn)自動(dòng)支持類遵循 KVC 的屬性.
子類通過(guò)實(shí)現(xiàn) automaticallyNotifiesObserversForKey: 方法控制是否自動(dòng)發(fā)消息.

Automatic Change Notification

NSObject 提供自動(dòng)鍵值更改通知的基本實(shí)現(xiàn):

//調(diào)用訪問器方法。
[account setName:@"Savings"];
 
//使用 setValue:forKey:最仑。
[account setValue:@"Savings" forKey:@"name"];
 
//使用 key-path藐俺,其中'account'是'document' 遵循 KVC 的屬性。
[document setValue:@"Savings" forKeyPath:@"account.name"];
 
//使用 mutableArrayValueForKey: 檢索 關(guān)系代理對(duì)象泥彤。
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];

Manual Change Notification

手動(dòng)通知, 控制指定的 key 發(fā)送 change 通知.

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

手動(dòng)通知需要在屬性值發(fā)生改變前后分別調(diào)用 willChangeValueForKey:didChangeValueForKey::
手動(dòng)調(diào)用 balance 屬性的 KVO.

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

判斷屬性是否有變化, 是否需要發(fā)送通知:

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

一個(gè)操作改變多個(gè)值:

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

有序集的情況下, 必須指定不僅僅是 key 改變, 還有對(duì)象調(diào)用的改變類型以及索引.變化類型是一個(gè) NSKeyValueChange 值, 索引是 NSIndexSet 對(duì)象.
有序集的刪除操作:

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

Registering Dependent Keys

在許多情況下欲芹,一個(gè)屬性的值取決于另一個(gè)對(duì)象中的一個(gè)或多個(gè)其他屬性的值。如果一個(gè)屬性的值發(fā)生了變化吟吝,那么派生屬性的值也應(yīng)該標(biāo)記為變化菱父。如何確保為這些依賴屬性發(fā)布 KVO 通知取決于關(guān)系的基數(shù)。

To-One Relationships

觸發(fā) to-one 關(guān)系對(duì)象自動(dòng)發(fā)出消息, 需要重寫 keyPathsForValuesAffectingValueForKey: 方法或適合的方法,該方法遵循它為注冊(cè)依賴鍵定義的模式。
例如, fullName 依賴 first and last name, 獲取 fullName 的方法可能為:

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

觀察 fullName 的對(duì)象, 不管 firstName 還是 lastName 屬性發(fā)生改變時(shí)都應(yīng)該被通知到, 作為他們影響的值.
一個(gè)解決辦法是通過(guò)重寫 keyPathsForValuesAffectingValueForKey: 方法指定 fullName 屬性依賴 firstNamelastName 屬性:

+ (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)一個(gè)形如 keyPathForValuesAffecting<Key>的類方法:

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

不能通過(guò)分類形式給一個(gè)已經(jīng)存在的類重寫 keyPathsForValuesAffectingValueForKey: 方法, 但可以實(shí)現(xiàn) keyPathsForValuesAffecting<Key> 類方法.

To-Many Relationships

keyPathsForValuesAffectingValueForKey: 方法不支持 to-many 關(guān)系對(duì)象, 比如有一個(gè) Department 對(duì)象有一個(gè) to-many 關(guān)系對(duì)象(employees)包含 Employee, Employee 有一個(gè) salary 屬性. 你可能想要 Department 對(duì)象有一個(gè) totalSalary 屬性統(tǒng)計(jì)所有 Employeessalaries. 使用 keyPathsForValuesAffectingTotalSalary 返回 employees.salary 是不行的.
有兩種解決方法:

  1. 可以通過(guò)注冊(cè)父類對(duì)象(Department)作為觀察者, 觀察所有子屬性(Employees). 在關(guān)系對(duì)象中(employees)中添加或移除子對(duì)象時(shí), 必須響應(yīng)的添加和移除父對(duì)象觀察者. 在 observeValueForKeyPath:ofObject:change:context: 方法中根據(jù) changes 更新依賴值:

    - (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;
    }
    
  2. 如果是 Core Data, 則可以通過(guò) NSNotificationCenter 注冊(cè)通知.

Key-Value Observing Implementation Details (KVO 的實(shí)現(xiàn)細(xì)節(jié))

KVO 的實(shí)現(xiàn)使用的是 isa-swizzling 技術(shù).
isa 指向維護(hù)在 dispatch table 中的類的對(duì)象. dispatch table 中包含類實(shí)現(xiàn)的方法以及其他數(shù)據(jù).
當(dāng)一個(gè)對(duì)象的屬性注冊(cè)了一個(gè)觀察者, 被觀察對(duì)象的 isa 會(huì)被修改, 指向一個(gè)中間類而不是真正的類. 因此浙宜,isa 指針的值不一定反映實(shí)例的實(shí)際類官辽。
永遠(yuǎn)不要使用 isa 來(lái)判斷類的從屬關(guān)系. 應(yīng)該使用 class 方法.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市粟瞬,隨后出現(xiàn)的幾起案子野崇,更是在濱河造成了極大的恐慌,老刑警劉巖亩钟,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異鳖轰,居然都是意外死亡清酥,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門蕴侣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)焰轻,“玉大人,你說(shuō)我怎么就攤上這事昆雀∪柚荆” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵狞膘,是天一觀的道長(zhǎng)揩懒。 經(jīng)常有香客問我,道長(zhǎng)挽封,這世上最難降的妖魔是什么已球? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮辅愿,結(jié)果婚禮上智亮,老公的妹妹穿的比我還像新娘。我一直安慰自己点待,他們只是感情好阔蛉,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著癞埠,像睡著了一般状原。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上燕差,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天遭笋,我揣著相機(jī)與錄音,去河邊找鬼徒探。 笑死瓦呼,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播央串,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼磨澡,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了质和?” 一聲冷哼從身側(cè)響起稳摄,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎饲宿,沒想到半個(gè)月后厦酬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡瘫想,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年仗阅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片国夜。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡减噪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出车吹,到底是詐尸還是另有隱情筹裕,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布窄驹,位于F島的核電站朝卒,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏馒吴。R本人自食惡果不足惜扎运,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望饮戳。 院中可真熱鬧豪治,春花似錦、人聲如沸扯罐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)歹河。三九已至掩浙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間秸歧,已是汗流浹背厨姚。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留键菱,地道東北人谬墙。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親拭抬。 傳聞我的和親對(duì)象是個(gè)殘疾皇子部默,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容