談?wù)?KVO


本文結(jié)構(gòu)如下:

  • Why? (為什么要用KVO)
  • What? (KVO是什么)
  • How? ( KVO怎么用)
  • More (更多細(xì)節(jié))
  • 原理
  • 自己實(shí)現(xiàn)KVO

在我的上一篇文章淺談 iOS Notification中,我們說(shuō)到了iOS中觀察者模式的一種實(shí)現(xiàn)方式:NSNotification 通知,這次我們?cè)賮?lái)談?wù)刬OS中觀察者模式的另一種實(shí)現(xiàn)方式:KVO 。

Why?

假如,有一個(gè)person類(lèi),和一個(gè)Account類(lèi),account類(lèi)中又有兩個(gè)公開(kāi)的屬性,balance和interestRate,當(dāng)account中的balance和interestRate發(fā)生變化時(shí),需要知道通知到這個(gè)person,這個(gè)要求很正常,我的銀行賬戶里的錢(qián)增加或減少了我當(dāng)然要及時(shí)知道啊。有人可能會(huì)想,每隔一段時(shí)間去輪詢Account中的balance和interestRate,當(dāng)其發(fā)生變化就通知person,但是這樣做不僅低效而且通知也不能及時(shí)發(fā)出。


這個(gè)時(shí)候KVO就派上用場(chǎng)了。

What?

KVO到底是什么呢?不著急,要說(shuō)KVO還得先說(shuō)下KVC,KVC(Key-value coding)是一種基于NSKeyValueCoding非正式協(xié)議的機(jī)制,能讓我們直接使用一個(gè)或一串字符串標(biāo)識(shí)符去訪問(wèn),操作類(lèi)的屬性。
常用的方法比如:

- (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;

- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

通過(guò)這些方法加上正確的標(biāo)識(shí)符(一般和屬性同名),可以直接獲取或者設(shè)置一個(gè)類(lèi)的屬性,甚至可以輕易越過(guò)多個(gè)類(lèi)的層級(jí)結(jié)構(gòu),直接獲取目標(biāo)屬性。


KVC還提供了集合操作的方法,直接獲取到集合屬性的同時(shí)還能對(duì)其進(jìn)行求和,取平均數(shù),求最大最小值等操作,如下為求和操作,具體可以到蘋(píng)果官方文檔詳細(xì)了解。

NSNumber *amountSum = [self.transactions valueForKeyPath:@"@sum.amount"];
KVO

KVO (Key-Value Observing) 是Cocoa提供的一種基于KVC的機(jī)制,允許一個(gè)對(duì)象去監(jiān)聽(tīng)另一個(gè)對(duì)象的某個(gè)屬性,當(dāng)該屬性改變時(shí)系統(tǒng)會(huì)去通知監(jiān)聽(tīng)的對(duì)象(不是被監(jiān)聽(tīng)的對(duì)象)。

上面那個(gè)例子如果用KVO實(shí)現(xiàn)的話,大概就是,用Person類(lèi)的一個(gè)對(duì)象去監(jiān)聽(tīng)Account類(lèi)的一個(gè)對(duì)象的屬性,然后當(dāng)Account類(lèi)對(duì)象的相應(yīng)屬性改變時(shí),Person類(lèi)的對(duì)象就會(huì)收到通知。這也是iOS種觀察者模式的一種實(shí)現(xiàn)方式。

也就是說(shuō),一般情況下,任何一個(gè)對(duì)象可以監(jiān)聽(tīng)任何一個(gè)對(duì)象(當(dāng)然也包括自己本身)的任意屬性,然后在其屬性變化后收到通知。

How?

那么KVO怎么用呢?KVO的使用步驟主要分為3步:添加監(jiān)聽(tīng),接收通知移除監(jiān)聽(tīng)。

1. 添加監(jiān)聽(tīng)

通過(guò)以下方法添加一個(gè)監(jiān)聽(tīng)者:

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

首先說(shuō)下這個(gè)消息(方法)的接收者,就是調(diào)用這個(gè)方法的對(duì)象,即觀察者模式中的被觀察者,這里有一點(diǎn)需要注意的是,被監(jiān)聽(tīng)的對(duì)象最好不要設(shè)置為,在當(dāng)前上下文環(huán)境中容易改變的對(duì)象,因?yàn)镵VO只會(huì)把添加KVO時(shí)的被監(jiān)聽(tīng)對(duì)象保存下來(lái),之后要是被監(jiān)聽(tīng)對(duì)象發(fā)生改變了,KVO監(jiān)聽(tīng)的還是之前保存的那個(gè)對(duì)象,KVO回調(diào)方法就不會(huì)觸發(fā)了。

然后,我們重點(diǎn)關(guān)注一下這個(gè)方法的4個(gè)參數(shù):

  • observer:就是要添加的監(jiān)聽(tīng)者對(duì)象,,當(dāng)監(jiān)聽(tīng)的屬性發(fā)生改變時(shí)就會(huì)去通知該對(duì)象,該對(duì)象必須實(shí)現(xiàn)- observeValueForKeyPath:ofObject:change:context:方法,要不然當(dāng)監(jiān)聽(tīng)的屬性的改變通知發(fā)出來(lái),卻發(fā)現(xiàn)沒(méi)有相應(yīng)的接收方法時(shí),程序會(huì)拋出異常。

  • keyPath:就是要被監(jiān)聽(tīng)的屬性,這里和KVC的規(guī)則一樣。但是這個(gè)值不能傳nil,要不然會(huì)報(bào)錯(cuò)。通常我們?cè)谟玫臅r(shí)候會(huì)傳一個(gè)與屬性同名的字符串,但是這樣可能會(huì)因?yàn)槠磳?xiě)錯(cuò)誤,導(dǎo)致監(jiān)聽(tīng)不成功,一個(gè)推薦的做法是,用這種方式NSStringFromSelector(@selector(propertyName)),其實(shí)就是是將屬性的getter方法轉(zhuǎn)換成了字符串,這樣做的好處就是,如果你寫(xiě)錯(cuò)了屬性名,xcode會(huì)用警告提醒你。

  • options:是一些配置選項(xiàng),用來(lái)指明通知發(fā)出的時(shí)機(jī)和通知響應(yīng)方法- observeValueForKeyPath:ofObject:change:context:change字典中包含哪些值,它的取值有4個(gè),定義在NSKeyValueObservingOptions中,可以用|符號(hào)連接,如下:
    1> NSKeyValueObservingOptionNew:指明接受通知方法參數(shù)中的change字典中應(yīng)該包含改變后的新值。

2>NSKeyValueObservingOptionOld: 指明接受通知方法參數(shù)中的change字典中應(yīng)該包含改變前的舊值。

3>NSKeyValueObservingOptionInitial: 當(dāng)指定了這個(gè)選項(xiàng)時(shí),在addObserver:forKeyPath:options:context:消息被發(fā)出去后,甚至不用等待這個(gè)消息返回,監(jiān)聽(tīng)者對(duì)象會(huì)馬上收到一個(gè)通知。這種通知只會(huì)發(fā)送一次,你可以利用這種“一次性“的通知來(lái)確定要監(jiān)聽(tīng)屬性的初始值。當(dāng)同時(shí)制定這3個(gè)選項(xiàng)時(shí),這種通知的change字典中只會(huì)包含新值,而不會(huì)包含舊值。雖然這時(shí)候的新值實(shí)際上是改變前的'舊值',但是這個(gè)值對(duì)于監(jiān)聽(tīng)者來(lái)說(shuō)是新的。

4>NSKeyValueObservingOptionPrior:當(dāng)指定了這個(gè)選項(xiàng)時(shí),在被監(jiān)聽(tīng)的屬性被改變前,監(jiān)聽(tīng)者對(duì)象就會(huì)收到一個(gè)通知(一般的通知發(fā)出時(shí)機(jī)都是在屬性改變后,雖然change字典中包含了新值和舊值,但是通知還是在屬性改變后才發(fā)出),這個(gè)通知會(huì)包含一個(gè)NSKeyValueChangeNotificationIsPriorKeykey,其對(duì)應(yīng)的值為一個(gè)NSNumber類(lèi)型的YES。當(dāng)同時(shí)指定該值、new和old的話,change字典會(huì)包含舊值而不會(huì)包含新值。你可以在這個(gè)通知中調(diào)用- (void)willChangeValueForKey:(NSString *)key;

  • context:添加監(jiān)聽(tīng)方法的最后一個(gè)參數(shù),是一個(gè)可選的參數(shù),可以傳任何數(shù)據(jù),這個(gè)參數(shù)最后會(huì)被傳到監(jiān)聽(tīng)者的響應(yīng)方法中,可以用來(lái)區(qū)分不同通知,也可以用來(lái)傳值。如果你要用context來(lái)區(qū)分不同的通知,一個(gè)推薦的做法是聲明一個(gè)靜態(tài)變量,其保持它自己的地址,這個(gè)變量沒(méi)有什么意義,但是卻能起到區(qū)分的作用,如下:
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;

然后,結(jié)合上面Person,account的例子,我們可以給Account對(duì)象添加監(jiān)聽(tīng):

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

需要注意的是,添加監(jiān)聽(tīng)的方法addObserver:forKeyPath:options:context:并不會(huì)對(duì)監(jiān)聽(tīng)和被監(jiān)聽(tīng)的對(duì)象以及context做強(qiáng)引用,你必須自己保證他們?cè)诒O(jiān)聽(tīng)過(guò)程中不被釋放。

2. 接受通知

前面說(shuō)過(guò)了,每一個(gè)監(jiān)聽(tīng)者對(duì)象都必須實(shí)現(xiàn)下面這個(gè)方法來(lái)接收通知:

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;

keyPath,object,context和監(jiān)聽(tīng)方法中指定的一樣,關(guān)于change參數(shù),它是一個(gè)字典,有五個(gè)常量作為它的鍵:

NSString *const NSKeyValueChangeKindKey;  
NSString *const NSKeyValueChangeNewKey;  
NSString *const NSKeyValueChangeOldKey;  
NSString *const NSKeyValueChangeIndexesKey;  
NSString *const NSKeyValueChangeNotificationIsPriorKey;

一個(gè)一個(gè)分析下:

  • NSKeyValueChangeKindKey:指明了變更的類(lèi)型,值為“NSKeyValueChange”枚舉中的某一個(gè),類(lèi)型為NSNumber。
enum {
   NSKeyValueChangeSetting = 1,
   NSKeyValueChangeInsertion = 2,
   NSKeyValueChangeRemoval = 3,
   NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;

一般情況下返回的都是1也就是第一個(gè)NSKeyValueChangeSetting,但是如果你監(jiān)聽(tīng)的屬性是一個(gè)集合對(duì)象的話,當(dāng)這個(gè)集合中的元素被插入,刪除,替換時(shí),就會(huì)分別返回NSKeyValueChangeInsertion,NSKeyValueChangeRemovalNSKeyValueChangeReplacement

  • NSKeyValueChangeNewKey:被監(jiān)聽(tīng)屬性改變后新值的key,當(dāng)監(jiān)聽(tīng)屬性為一個(gè)集合對(duì)象,且NSKeyValueChangeKindKey不為NSKeyValueChangeSetting時(shí),該值返回的是一個(gè)數(shù)組,包含插入,替換后的新值(刪除操作不會(huì)返回新值)。

  • NSKeyValueChangeOldKey:被監(jiān)聽(tīng)屬性改變前舊值的key,當(dāng)監(jiān)聽(tīng)屬性為一個(gè)集合對(duì)象,且NSKeyValueChangeKindKey不為NSKeyValueChangeSetting時(shí),該值返回的是一個(gè)數(shù)組,包含刪除,替換前的舊值(插入操作不會(huì)返回舊值)

  • NSKeyValueChangeIndexesKey:如果NSKeyValueChangeKindKey的值為NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, 或者 NSKeyValueChangeReplacement,這個(gè)鍵的值是一個(gè)NSIndexSet對(duì)象,包含了增加,移除或者替換對(duì)象的index。

  • NSKeyValueChangeNotificationIsPriorKey:如果注冊(cè)監(jiān)聽(tīng)者是options中指明了NSKeyValueObservingOptionPrior,change字典中就會(huì)帶有這個(gè)key,值為NSNumber類(lèi)型的YES.

最后,完整的change字典大概就類(lèi)似這樣:

    NSDictionary *change = @{
                             NSKeyValueChangeKindKey : NSKeyValueChange(枚舉值),
                             NSKeyValueChangeNewKey : newValue,
                             NSKeyValueChangeOldKey : oldValue,
                             NSKeyValueChangeIndexesKey : @[NSIndexSet, NSIndexSet],
                             NSKeyValueChangeNotificationIsPriorKey : @1,
                             };

繼續(xù)用上面的例子實(shí)現(xiàn)接受通知如下:

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

你可以通過(guò)context或者keypath來(lái)區(qū)分不同的通知,但是要注意的是,正如上面實(shí)例代碼中那樣,當(dāng)接收到一個(gè)不能識(shí)別的context或者keypath的話,需要調(diào)用一下父類(lèi)的- observeValueForKeyPath:ofObject:change:context:方法

3. 移除監(jiān)聽(tīng)

當(dāng)一個(gè)監(jiān)聽(tīng)者完成了它的監(jiān)聽(tīng)任務(wù)之后,就需要注銷(xiāo)(移除)監(jiān)聽(tīng)者,調(diào)用以下2個(gè)方法來(lái)移除監(jiān)聽(tīng)。通常會(huì)在-dealloc方法或者-observeValueForKeyPath:ofObject:change:context:方法中移除。

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context
或者
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

有幾點(diǎn)需要注意的:

  • 當(dāng)你向一個(gè)不是監(jiān)聽(tīng)者的對(duì)象發(fā)送remove消息的時(shí)候(也可能是,你發(fā)送remove消息時(shí),接受消息的對(duì)象已經(jīng)被remove了一次,或者在注冊(cè)為監(jiān)聽(tīng)者前就調(diào)用了remove),xcode會(huì)拋出一個(gè)NSRangeException異常,所以,保險(xiǎn)的做法是,把remove操作放在try/catch中。

  • 一個(gè)監(jiān)聽(tīng)者在其被銷(xiāo)毀時(shí),并不會(huì)自己注銷(xiāo)監(jiān)聽(tīng),而給一個(gè)已經(jīng)銷(xiāo)毀的監(jiān)聽(tīng)者發(fā)送通知,會(huì)造成野指針錯(cuò)誤。所以至少保證,在監(jiān)聽(tīng)者被釋放前,將其監(jiān)聽(tīng)注銷(xiāo)。保證有一個(gè)add方法,就有一個(gè)remove方法。

More

再說(shuō)更多的一些東西,想讓類(lèi)的某個(gè)屬性支持KVO機(jī)制的話,這個(gè)類(lèi)必須滿足一下3點(diǎn):

  1. 這個(gè)類(lèi)必須使得該屬性支持KVC。
  2. 這個(gè)類(lèi)必須保證能夠?qū)⒏淖兺ㄖl(fā)出。
  3. 當(dāng)有依賴關(guān)系的時(shí)候,注冊(cè)合適的依賴鍵。
  • 第一個(gè)條件:這個(gè)類(lèi)必須使得該屬性支持KVC
    就是需要實(shí)現(xiàn)與該屬性對(duì)應(yīng)的getter和setter方法和其他一些可選方法。幸運(yùn)的是,NSObject類(lèi)已經(jīng)幫我們實(shí)現(xiàn)了這些,只要你的類(lèi)最終是繼承自NSObject,并且使用正常的方式創(chuàng)建屬性,這些屬性都是支持KVO的。

KVO支持的類(lèi)型和KVC一樣,包括對(duì)象類(lèi)型,標(biāo)量(例如 intCGFloat)和 struct(例如 CGRect)。

  • 第二個(gè)條件:這個(gè)類(lèi)必須保證能夠?qū)⒏淖兺ㄖl(fā)出。
    通知發(fā)出的方式又分為自動(dòng)通知手動(dòng)通知
    1> 自動(dòng)通知
    自動(dòng)通知由NSObject默認(rèn)實(shí)現(xiàn)了,也就是說(shuō)一般情況下,你不用寫(xiě)額外的一些代碼,屬性改變的通知就會(huì)自動(dòng)發(fā)出,這也是我們平常開(kāi)發(fā)中接觸最多的。

觸發(fā)自動(dòng)通知發(fā)出的方式包括下面這些:

 // 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];

其中包括調(diào)用setter方法,調(diào)用KVC的setValue:forKey:setValue:forKeyPath:,最后一個(gè)方法需要說(shuō)一下,mutableArrayValueForKey:也是KVC的方法,大家應(yīng)該都知道,如果你用KVO監(jiān)聽(tīng)了一個(gè)集合對(duì)象(比如一個(gè)數(shù)組),當(dāng)你給數(shù)組發(fā)送addObject:消息時(shí),是不會(huì)觸發(fā)KVO通知的,但是通過(guò)mutableArrayValueForKey:這個(gè)方法對(duì)集合對(duì)象進(jìn)行的相關(guān)操作(增加,刪除,替換元素)就會(huì)觸發(fā)KVO通知,這個(gè)方法會(huì)返回一個(gè)中間代理對(duì)象,這個(gè)中間代理對(duì)象的類(lèi)會(huì)指向一個(gè)中間類(lèi),你在這個(gè)代理對(duì)象上進(jìn)行的操作最終應(yīng)在原始對(duì)象上造成同樣的效果。
2> 手動(dòng)通知
有時(shí)候,你可能會(huì)想控制通知的發(fā)送,比如,阻止一些不必要的通知發(fā)出,或者把一組類(lèi)似的通知合并成一個(gè),這時(shí)候就需要手動(dòng)發(fā)送通知了。

首先,你需要重寫(xiě)NSObject的一個(gè)類(lèi)方法,來(lái)指明你不想讓哪個(gè)屬性的改變通知自動(dòng)發(fā)出。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {

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

如上,return NO就可以阻止,該key對(duì)應(yīng)的屬性改變時(shí),通知不會(huì)自動(dòng)發(fā)送給監(jiān)聽(tīng)者對(duì)象,當(dāng)然對(duì)于其他的屬性別忘了調(diào)用super方法保持它原來(lái)的狀態(tài)。(改方法默認(rèn)返回YES)

然后,你需要重寫(xiě)你想手動(dòng)發(fā)送通知屬性的setter方法,然后在屬性值改變之前和之后分別調(diào)用willChangeValueForKey:didChangeValueForKey:方法。

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

這樣就基本實(shí)現(xiàn)了一個(gè)KVO的手動(dòng)通知,當(dāng)該屬性值改變時(shí),監(jiān)聽(tīng)者對(duì)象就能收到改變通知了。

你還可以過(guò)濾一些通知,像下面的例子就是只有當(dāng)屬性真正改變時(shí)才會(huì)發(fā)出通知

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

如果一個(gè)操作導(dǎo)致了多個(gè)鍵的變化,你必須嵌套變更通知:

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

在to-many關(guān)系操作的情形中,你不僅必須表明key是什么,還要表明變更類(lèi)型和影響到的索引。變更類(lèi)型是一個(gè) NSKeyValueChange值,被影響對(duì)象的索引是一個(gè) NSIndexSet對(duì)象。

下面的代碼示范了在to-many關(guān)系transactions對(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"];
 }
  • 第三個(gè)條件:這個(gè)類(lèi)必須使得該屬性支持KVC

有時(shí)候會(huì)存在這樣一種情況,一個(gè)屬性的改變依賴于別的一個(gè)或多個(gè)屬性的改變,也就是說(shuō)當(dāng)別的屬性改了,這個(gè)屬性也會(huì)跟著改變,比如說(shuō)一個(gè)人的全名fullName包括firstName和lastName,當(dāng)firstName或者lastName中任何一個(gè)值改變了,fullName也就改變了。一個(gè)監(jiān)聽(tīng)者監(jiān)聽(tīng)了fullName,當(dāng)firstName或者lastName改變時(shí),這個(gè)監(jiān)聽(tīng)者也應(yīng)該被通知。

一種方法就是重寫(xiě)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;
 }

另一種實(shí)現(xiàn)同樣結(jié)果的方法是實(shí)現(xiàn)一個(gè)遵循命名方式為keyPathsForValuesAffecting<Key>的類(lèi)方法,<Key>是依賴于其他值的屬性名(首字母大寫(xiě)),用上面代碼的例子來(lái)重新實(shí)現(xiàn)一下:

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

但是在To-many Relationships中(比如數(shù)組屬性),上面的方法就不管用了,比如,假如你有一個(gè)Department類(lèi),它有一個(gè)針對(duì)Employee類(lèi)的to-many關(guān)系(即擁有一個(gè)裝有Employee類(lèi)對(duì)象的數(shù)組),Employee類(lèi)有salary屬性。你希望Department類(lèi)有一個(gè)totalSalary屬性來(lái)計(jì)算所有員工的薪水,也就是在這個(gè)關(guān)系中DepartmenttotalSalary依賴于所有Employeesalary屬性。這種情況你不能通過(guò)實(shí)現(xiàn)keyPathsForValuesAffectingTotalSalary方法并返回employees.salary。

有兩種解決方法:

  1. 你可以用KVO將parent(比如Department)作為所有children(比如Employee)相關(guān)屬性的觀察者。你必須在把child添加或刪除到parent時(shí)也把parent作為child的觀察者添加或刪除。在observeValueForKeyPath:ofObject:change:context:方法中我們可以針對(duì)被依賴項(xiàng)的變更來(lái)更新依賴項(xiàng)的值:
- (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;
}
  1. 使用iOS中觀察者模式的另一種實(shí)現(xiàn)方式:通知 (NSNotification) ,有關(guān)通知相關(guān)的概念和用法,可以參考我上一篇文章 淺談 iOS Notification 。??

原理

說(shuō)了這么多,KVO的原理到底是什么呢?

先上官方文檔:

  Automatic key-value observing is implemented using a technique called 
isa-swizzling...When an observer is registered for an attribute of an
object the isa pointer of the observed object is modified, pointing to
 an intermediate class rather than at the true class. As a result the 
value of the isa pointer does not necessarily reflect the actual class
of the instance.

對(duì)于KVO實(shí)現(xiàn)的原理,蘋(píng)果官方文檔描述的比較少,從中只能知道蘋(píng)果使用了一張叫做isa-swizzling的黑魔法...

其實(shí),當(dāng)某個(gè)類(lèi)的對(duì)象第一次被觀察時(shí),系統(tǒng)就會(huì)在運(yùn)行期動(dòng)態(tài)地創(chuàng)建該類(lèi)的一個(gè)派生類(lèi)(類(lèi)名就是在該類(lèi)的前面加上NSKVONotifying_ 前綴),在這個(gè)派生類(lèi)中重寫(xiě)基類(lèi)中任何被觀察屬性的 setter 方法。

派生類(lèi)在被重寫(xiě)的 setter 方法實(shí)現(xiàn)真正的通知機(jī)制,就如前面手動(dòng)實(shí)現(xiàn)鍵值觀察那樣,調(diào)用willChangeValueForKey:didChangeValueForKey:方法。這么做是基于設(shè)置屬性會(huì)調(diào)用 setter 方法,而通過(guò)重寫(xiě)就獲得了 KVO 需要的通知機(jī)制。當(dāng)然前提是要通過(guò)遵循 KVO 的屬性設(shè)置方式來(lái)變更屬性值,如果僅是直接修改屬性對(duì)應(yīng)的成員變量,是無(wú)法實(shí)現(xiàn) KVO 的。

同時(shí)派生類(lèi)還重寫(xiě)了 class 方法以“欺騙”外部調(diào)用者它就是起初的那個(gè)類(lèi)。然后系統(tǒng)將這個(gè)對(duì)象的 isa 指針指向這個(gè)新誕生的派生類(lèi),因此這個(gè)對(duì)象就成為該派生類(lèi)的對(duì)象了,因而在該對(duì)象上對(duì) setter 的調(diào)用就會(huì)調(diào)用重寫(xiě)的 setter,從而激活鍵值通知機(jī)制。此外,派生類(lèi)還重寫(xiě)了 dealloc 方法來(lái)釋放資源。

自己實(shí)現(xiàn)KVO

港真,原生的KVO API是不太友好的,需要監(jiān)聽(tīng)者對(duì)象,和被監(jiān)聽(tīng)的對(duì)象分別去實(shí)現(xiàn)一些東西,代碼實(shí)現(xiàn)比較分散,并且響應(yīng)通知的方法也不能自定義,只能在蘋(píng)果提供的方法中處理,不能用我們熟悉的block或者Target-Action,最后還不能忘了調(diào)用removeObserve方法,一忘可能程序運(yùn)行的時(shí)候就奔潰了...

在知道了KVO的使用方法和內(nèi)部原理之后,我們其實(shí)可以自己去實(shí)現(xiàn)一個(gè)使用起來(lái)更加便捷,API更加友好的KVO的,這類(lèi)的實(shí)現(xiàn)網(wǎng)上有很多,我就不獻(xiàn)丑了... github上也有一些開(kāi)源的實(shí)現(xiàn)代碼,感興趣的童鞋可以自行查閱。

其實(shí)基本思路和蘋(píng)果官方的原理差不多,都是創(chuàng)建一個(gè)原類(lèi)的派生類(lèi)當(dāng)做中間類(lèi),再把原來(lái)的對(duì)象指向這個(gè)中間類(lèi),再重寫(xiě)監(jiān)聽(tīng)屬性的Setter方法,在屬性改變后調(diào)用回調(diào)通知監(jiān)聽(tīng)者。


參考:

KVO官方文檔
KVC官方文檔
Objective-C中的KVC和KVO
詳解鍵值觀察(KVO)及其實(shí)現(xiàn)機(jī)理

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

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

  • 本文由我們團(tuán)隊(duì)的 糾結(jié)倫 童鞋撰寫(xiě)。 文章結(jié)構(gòu)如下: Why? (為什么要用KVO) What? (KVO是什么...
    知識(shí)小集閱讀 7,483評(píng)論 7 105
  • 在iOS開(kāi)發(fā)中,我們常常用到鍵值編碼KVC和鍵值監(jiān)聽(tīng)KVO兩個(gè)東東,今天小編和大家分享的就是這兩個(gè)東東在應(yīng)用開(kāi)發(fā)中...
    突然自我閱讀 1,081評(píng)論 2 3
  • 由于ObjC主要基于Smalltalk進(jìn)行設(shè)計(jì),因此它有很多類(lèi)似于Ruby、Python的動(dòng)態(tài)特性,例如動(dòng)態(tài)類(lèi)型、...
    JonesCxy閱讀 405評(píng)論 0 0
  • 你要知道的KVC、KVO、Delegate、Notification都在這里 轉(zhuǎn)載請(qǐng)注明出處 http://www...
    WWWWDotPNG閱讀 1,904評(píng)論 1 3
  • 由于ObjC主要基于Smalltalk進(jìn)行設(shè)計(jì),因此它有很多類(lèi)似于Ruby、Python的動(dòng)態(tài)特性,例如動(dòng)態(tài)類(lèi)型、...
    JonesCxy閱讀 516評(píng)論 1 0

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