KVO

KVO 簡介

KVO 鍵值觀察機制,就是觀察指定對象的指定屬性變化情況。

KVO 鍵值觀察 依賴于 KVC 健值編碼

Key-value observing 通常用于 MVC 中,model 與 controller直接的通訊。

繼承于 NSObject 才可以擁有 KVO 機制

兩種 KVO 應用方式

  • 自動 KVO
  • 手動 KVO

舉個例子,一個 Person 類, 一個 Account

  • [圖1]

Account 的屬性 余額 balance 改變時通知 Person
Account 添加觀察者 Person
[account addObserver:person forKeyPath:@"balance" options:options context:context];

  • [圖2]

Person 觀察 Account 的屬性變化
Person 中實現觀察者方法 observeValueForKeyPath:ofObject:change:context:

  • [圖3]

Account 移除觀察者 Person
[account removeObserver:person forKeyPath:@"balance" context:context]

  • [圖4]

這篇文章的重要內容

  1. Registering for Key-Value Observing 注冊鍵值觀察的過程

  2. Registering Dependent Keys 對于存在 key 依賴的鍵值觀察

  3. Key-Value Observing Implementation Details 鍵值觀察的實現細節


1. Registering for Key-Value Observing 注冊鍵值觀察的過程

KVO 生命周期的過程,必須完成下面這三個方法

  1. 給被觀察者注冊觀察者 addObserver:forKeyPath:options:context:.
  2. 在觀察者里實現方法 接受通知 observeValueForKeyPath:ofObject:change:context:
  3. 移除觀察者 removeObserver:forKeyPath: 要在觀察者的內存銷毀之前 移除觀察者機制

- 注冊觀察者 addObserver:forKeyPath:options:context:.

  • Options
    Options 影響方法 observeValueForKeyPath:ofObject:change:context: 中的 change 字典

    • NSKeyValueObservingOptionOld: change 包含 old value
    • NSKeyValueObservingOptionNew: 'change' 包含 new value
    • NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew: 即包含 old 也包含 new
    • NSKeyValueObservingOptionInitial : change 中不包含 key 的值,會在 kvo 注冊的時候立即發送通知。
    • NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew : 注冊kvo時立即發送通知 change 中有 new 值,這里的 new 值是注冊之前 key 的值。
    • NSKeyValueObservingOptionPrior : 會在值發生改變前發出一次通知,當然改變后的通知依舊還會發出,也就是每次change都會有兩個通知。值變化之前發送通知的 change 中包含一個鍵值對 NSKeyValueChangeNotificationIsPriorKey:@(1), 值發生變化之后的的通知 change 不包含上面提到的 鍵值對, 可以跟 willChange 手動通知搭配使用
  • Context
    是一個 void * 指針,可以傳任意數據進去,可以防止父類跟子類同時對同一個 key 注冊觀察者造成的異常。

    可以采用下面的方法創建一個 Context

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

    命名規范:static void * 類名+屬性名+Context

- Receiving Notification of a Change 接收通知響應

        所有的觀察者必須實現方法 `observeValueForKeyPath:ofObject:change:context: message.`
  • keyPath : 注冊 KVO 時的 keyPath
  • object : 被觀察者對象
  • change : 根據注冊 KVO 時 option 不同而展示不同的內容,change中可能會包含 keyPath 變化前后的值,對應的鍵有
    • NSKeyValueChangeOldKey
    • NSKeyValueChangeNewKey

keyPath是個標量或者 c 語言結構體,會把這些包裝成 NSNumberNSValue,

keyPath 是個容器, change中可能會包含這個容器 inserted,removed,replaced 的情況 對應的鍵有

- `NSKeyValueChangeInsertion`
- `NSKeyValueChangeRemoval` 
- `NSKeyValueChangeReplacement`

keypath 是個 NSIndexSet : change 中可能會包含數組

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

- Removing an Object as an Observer 移除觀察者

觀察者被移除之后就不會再接受到通知。

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

需要注意的地方

  • 不要重復移除觀察者,會造成異常,如果不知道這個觀察者是否已經被移除,可以在 try/catch 安全移除觀察者
  • 在觀察者內存銷毀之前從觀察者中釋放出來,不然會造成內存異常
  • 無法檢測一個對象是否處于觀察者模式中,
    • init 或者 viewDidLoad 中添加觀察者
    • dealloc 中移除觀察者

Automatic Change Notification 自動 KVO 通知

NSObject 提供了自動健值更改通知的實現,自動 KVO 通知依賴于 KVC 編碼機制獲取, KVC method,和 集合代理(collection proxy)mutableArrayValueForKey:

手動 KVO

當你想要控制整個 KVO 的進程可以采用手動 KVO, 比如減少不必要的通知,比如將大量的通知控制到一個通知中。
手動 KVO 跟 自動 KVO 可以共存,比如同一個對象的同一個屬性,可以在父類里自動 KVO, 在子類里手動 KVO, 重寫方法 automaticallyNotifiesObserversForKey即可

  • 打開手動 KVO 的開關 automaticallyNotifiesObserversForKey

    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
    
       BOOL automatic = NO;
       if ([theKey isEqualToString:@"balance"]) {
           automatic = NO;
       }
       else {
           automatic = [super automaticallyNotifiesObserversForKey:theKey];
       }
       return automatic;
    }
    
    • 手動觸發 KVO(一般是在 set 方法里)
      • willChangeValueForKey
      • didChangeValueForKey
      - (void)setBalance:(double)theBalance {
          if (theBalance != _balance) {
              [self willChangeValueForKey:@"balance"];
              _balance = theBalance;
              [self didChangeValueForKey:@"balance"];
          }
      }
      
    • 也可以把多個觸發 KVO 的 key 放到一起
      - (void)setBalance:(double)theBalance {
          [self willChangeValueForKey:@"balance"];
          [self willChangeValueForKey:@"itemChanged"];
          _balance = theBalance;
          _itemChanged = _itemChanged+1;
          [self didChangeValueForKey:@"itemChanged"];
          [self didChangeValueForKey:@"balance"];
      }
      
    • 容器內部更改時,采用采用手動 KVO(當容器內容修改時,會觸發 KVO)
      注意:根據容器變化的類型去設置對應的 NSKeyValueChange
      • NSKeyValueChangeInsertion
      • NSKeyValueChangeRemoval
      • NSKeyValueChangeReplacement

      針對容器對象的 KVO ,需要借用 KVC 機制創建的 容器代理。

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

2.KVO - 注冊依賴 key

單個關系

重寫下面兩個方法中的一個即可

  • keyPathsForValuesAffectingValueForKey

  • keyPathsForValuesAffecting<Key>
    用例如下

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

多個關系

比如,有個部門類 Department,有個員工類 Employees, 每個員工的薪水都會影響這個部門的總薪水。
可以在 Department 添加、刪除 Employees 的時候,給 Employees 注冊、移除觀察者 Employees

用例如下

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

3. KVO 內部實現細節

使用 isa-swizzling 原理
當 對象的屬性注冊到觀察者中時,會創建一個中間類,重寫了被觀察屬性的 setter 方法。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容

  • KVO編程指南 Key-Value Observing Programming Guide 1 Introduct...
    codeTao閱讀 614評論 0 0
  • 引言 鍵值觀察(KVO)提供了一種機制以允許對象被告知其他對象的特定屬性的更改,它對應用程序中的模型和控制器層之間...
    漸z閱讀 584評論 0 0
  • 本文結構如下: Why? (為什么要用KVO) What? (KVO是什么) How? ( KVO怎么用) Mo...
    等開會閱讀 1,659評論 1 21
  • 本文由我們團隊的 糾結倫 童鞋撰寫。 文章結構如下: Why? (為什么要用KVO) What? (KVO是什么...
    知識小集閱讀 7,420評論 7 105
  • 三十二 宸妃 自趙嬪晉位后,皇帝的心漸漸回到了蘇妃身上,對蘇妃多加寵愛和憐惜。 而蘇妃仍舊淡淡的,不驕不躁。 君后...
    君清兮閱讀 190評論 0 0