iOS - 關于 KVO 的一些總結

KVO 大綱

目錄

  • 1. 什么是 KVO
  • 2. KVO 的基本使用
    ?2.1 注冊方法
    ?2.2 監聽方法
    ?2.3 移除方法
    ?2.4 使用示例
    ?2.5 實際應用
    ?2.6 KVO 觸發監聽方法的方式
    ??2.6.1 自動觸發
    ??2.6.2 手動觸發
  • 3. KVO 的進階使用
    ?3.1 observationInfo 屬性
    ?3.2 context 的使用
    ?3.3 KVO 監聽集合對象
    ?3.4 KVO 的自動觸發控制
    ?3.5 KVO 的手動觸發
    ?3.6 KVO 新舊值相等時不觸發
    ?3.7 KVO 手動觀察集合屬性
    ?3.8 KVO 的依賴觀察
    ??3.8.1 一對一關系
    ??3.8.2 一對多關系
  • 4. KVO 的使用注意
    ?4.1 移除觀察者的注意點
    ?4.2 防止多次注冊和移除相同的 KVO
    ?4.3 其它注意點
  • 5. KVO 的實現原理
    ?5.1 isa-swizzling
    ?5.2 KVO 動態生成的子類都有哪些方法
  • 6. FBKVOController
    ?6.1 系統 KVO 的缺點
    ?6.2 FBKVOController 的介紹
    ?6.3 FBKVOController 的優點
    ?6.4 FBKVOController 的使用
    ?6.5 FBKVOController 的解析
  • 參考

1. 什么是 KVO

  • KVO的全稱是Key-Value Observing,俗稱“鍵值觀察/監聽”,是蘋果提供的一套事件通知機制,允許一個對象觀察/監聽另一個對象指定屬性值的改變。當被觀察對象屬性值發生改變時,會觸發KVO的監聽方法來通知觀察者。KVO是在MVC應用程序中的各層之間進行通信的一種特別有用的技術。
  • KVONSNotification都是iOS中觀察者模式的一種實現。
  • KVO可以監聽單個屬性的變化,也可以監聽集合對象的變化。監聽集合對象變化時,需要通過KVCmutableArrayValueForKey:等可變代理方法獲得集合代理對象,并使用代理對象進行操作,當代理對象的內部對象發生改變時,會觸發KVO的監聽方法。集合對象包含NSArrayNSSet
  • KVOKVC有著密切的關系,如果想要深入了解KVO,建議先學習KVC
    傳送門:iOS - 關于 KVC 的一些總結

2. KVO 的基本使用

KVO使用三部曲:添加/注冊KVO監聽、實現監聽方法以接收屬性改變通知、 移除KVO監聽。

  1. 調用方法addObserver:forKeyPath:options:context:給被觀察對象添加觀察者;
  2. 在觀察者類中實現observeValueForKeyPath:ofObject:change:context:方法以接收屬性改變的通知消息;
  3. 當觀察者不需要再監聽時,調用removeObserver:forKeyPath:方法將觀察者移除。需要注意的是,至少需要在觀察者銷毀之前,調用此方法,否則可能會導致Crash

2.1 注冊方法

/*
 ** target:  被觀察對象
 ** observer:觀察者對象
 ** keyPath: 被觀察對象的屬性的關鍵路徑,不能為nil
 ** options: 觀察的配置選項,包括觀察的內容(枚舉類型):
           NSKeyValueObservingOptionNew:觀察新值
           NSKeyValueObservingOptionOld:觀察舊值
           NSKeyValueObservingOptionInitial:觀察初始值,如果想在注冊觀察者后,立即接收一次回調,可以加入該枚舉值
           NSKeyValueObservingOptionPrior:分別在值改變前后觸發方法(即一次修改有兩次觸發)
 ** context: 可以傳入任意數據(任意類型的對象或者C指針),在監聽方法中可以接收到這個數據,是KVO中的一種傳值方式
             如果傳的是一個對象,必須在移除觀察之前持有它的強引用,否則在監聽方法中訪問context就可能導致Crash
 */
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
 options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

2.2 監聽方法

如果對象被注冊成為觀察者,則該對象必須能響應以下監聽方法,即該對象所屬類中必須實現監聽方法。當被觀察對象屬性發生改變時就會調用監聽方法。如果沒有實現就會導致Crash

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
/*
 ** keyPath:被觀察對象的屬性的關鍵路徑
 ** object: 被觀察對象
 ** change: 字典 NSDictionary<NSKeyValueChangeKey, id>,屬性值更改的詳細信息,根據注冊方法中options參數傳入的枚舉來返回
             key為 NSKeyValueChangeKey 枚舉類型
             {
                 1.NSKeyValueChangeKindKey:存儲本次改變的信息(change字典中默認包含這個key)
                 {
                     對應枚舉類型 NSKeyValueChange
                     typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
                         NSKeyValueChangeSetting     = 1,
                         NSKeyValueChangeInsertion   = 2,
                         NSKeyValueChangeRemoval     = 3,
                         NSKeyValueChangeReplacement = 4,
                     };
                     如果是對被觀察對象屬性(包括集合)進行賦值操作,kind 字段的值為 NSKeyValueChangeSetting
                     如果被觀察的是集合對象,且進行的是(插入、刪除、替換)操作,則會根據集合對象的操作方式來設置 kind 字段的值
                         插入:NSKeyValueChangeInsertion
                         刪除:NSKeyValueChangeRemoval
                         替換:NSKeyValueChangeReplacement
                 }    
                 2.NSKeyValueChangeNewKey:存儲新值(如果options中傳入NSKeyValueObservingOptionNew,change字典中就會包含這個key)
                 3.NSKeyValueChangeOldKey:存儲舊值(如果options中傳入NSKeyValueObservingOptionOld,change字典中就會包含這個key)
                 4.NSKeyValueChangeIndexesKey:如果被觀察的是集合對象,且進行的是(插入、刪除、替換)操作,則change字典中就會包含這個key
                     這個key的value是一個NSIndexSet對象,包含更改關系中的索引
                 5.NSKeyValueChangeNotificationIsPriorKey:如果options中傳入NSKeyValueObservingOptionPrior,則在改變前通知的change字典中會包含這個key。
                     這個key對應的value是NSNumber包裝的YES,我們可以這樣來判斷是不是在改變前的通知[change[NSKeyValueChangeNotificationIsPriorKey] boolValue] == YES]
             }
 ** context:注冊方法中傳入的context
 */
}

2.3 移除方法

在調用注冊方法后,KVO并不會對觀察者進行強引用,所以需要注意觀察者的生命周期。至少需要在觀察者銷毀之前,調用以下方法移除觀察者,否則如果在觀察者被釋放后,再次觸發KVO監聽方法就會導致Crash

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

2.4 使用示例

以下使用KVOperson對象添加觀察者為當前viewController,監聽person對象的name屬性值的改變。當name值改變時,觸發KVO的監聽方法。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [HTPerson new];
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.person.name= @"張三";
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"keyPath:%@",keyPath);
    NSLog(@"object:%@",object);
    NSLog(@"change:%@",change);
    NSLog(@"context:%@",context);
}

- (void)dealloc
{
    [self.person removeObserver:self forKeyPath:@"name"];
}

keyPath:name
object:<HTPerson: 0x600003ae4340>
change:{ kind = 1; new = "\U70b9\U51fb"; old = "<null>"; }
context:(null)

2.5 實際應用

KVO主要用來做鍵值觀察操作,想要一個值發生改變后通知另一個對象,則用KVO實現最為合適。斯坦福大學的iOS教程中有一個很經典的案例,通過KVOModelController之間進行通信。如圖所示:

斯坦福大學 KVO示例

2.6 KVO觸發監聽方法的方式

KVO觸發分為自動觸發和手動觸發兩種方式。

2.6.1 自動觸發

① 如果是監聽對象特定屬性值的改變,通過以下方式改變屬性值會觸發KVO

  • 使用點語法
  • 使用setter方法
  • 使用KVCsetValue:forKey:方法
  • 使用KVCsetValue:forKeyPath:方法

② 如果是監聽集合對象的改變,需要通過KVCmutableArrayValueForKey:等方法獲得代理對象,并使用代理對象進行操作,當代理對象的內部對象發生改變時,會觸發KVO。集合對象包含NSArrayNSSet

2.6.2 手動觸發

① 普通對象屬性或是成員變量使用:

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

NSArray對象使用:

- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;

NSSet對象使用:

- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;

3. KVO 的進階使用

3.1 observationInfo 屬性

  • observationInfo屬性是NSKeyValueObserving.h文件中系統通過分類給NSObject添加的屬性,所以所有繼承于NSObject的對象都含有該屬性;
  • 可以通過observationInfo屬性查看被觀察對象的全部觀察信息,包括observerkeyPathoptionscontext等。
@property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;

3.2 context 的使用

注冊方法addObserver:forKeyPath:options:context:中的context可以傳入任意數據,并且可以在監聽方法中接收到這個數據。

  • context作用:標簽-區分,可以更精確的確定被觀察對象屬性,用于繼承、 多監聽;也可以用來傳值。
    ??KVO只有一個監聽回調方法observeValueForKeyPath:ofObject:change:context:,我們通常情況下可以在注冊方法中指定contextNULL,并在監聽方法中通過objectkeyPath來判斷觸發KVO的來源。
    ??但是如果存在繼承的情況,比如現在有 Person 類和它的兩個子類 Teacher 類和 Student 類,person、teacher 和 student 實例對象都對 account 對象的 balance 屬性進行觀察。問題:
    ??① 當 balance 發生改變時,應該由誰來處理呢?
    ??② 如果都由 person 來處理,那么在 Person 類的監聽方法中又該怎么判斷是自己的事務還是子類對象的事務呢?
    ??這時候通過使用context就可以很好地解決這個問題,在注冊方法中為context設置一個獨一無二的值,然后在監聽方法中對context值進行檢驗即可。

  • 蘋果的推薦用法:用context來精確的確定被觀察對象屬性,使用唯一命名的靜態變量的地址作為context的值。可以為整個類設置一個context,然后在監聽方法中通過objectkeyPath來確定被觀察屬性,這樣存在繼承的情況就可以通過context來判斷;也可以為每個被觀察對象屬性設置不同的context,這樣使用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];
}
- (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];
    }
}
  • context優點:嵌套少、性能高、更安全、擴展性強。
  • context注意點:
    ① 如果傳的是一個對象,必須在移除觀察之前持有它的強引用,否則在監聽方法中訪問context就可能導致Crash
    ② 空傳NULL而不應該傳nil

3.3 KVO 監聽集合對象

KVO可以監聽單個屬性的變化,也可以監聽集合對象的變化。監聽集合對象變化時,需要通過KVCmutableArrayValueForKey:等方法獲得代理對象,并使用代理對象進行操作,當代理對象的內部對象發生改變時,會觸發KVO的監聽方法。集合對象包含NSArrayNSSet
(注意:如果直接對集合對象進行操作改變,不會觸發KVO。)

示例代碼及輸出如下:

觀察者 viewController 對被觀察對象 person 的 mArray 屬性進行監聽。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [HTPerson new];
    self.person.mArray = [NSMutableArray arrayWithCapacity:5];
    [self.person addObserver:self forKeyPath:@"mArray" options:(NSKeyValueObservingOptionNew| NSKeyValueObservingOptionOld) context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//    [self.person.mArray addObject:@"2"]; //如果直接對數組進行操作,不會觸發KVO
    NSMutableArray *array = [self.person mutableArrayValueForKey:@"mArray"];
    [array addObject:@"1"];
    [array replaceObjectAtIndex:0 withObject:@"2"];
    [array removeObjectAtIndex:0];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    /*  change 字典的值為:
        {
            indexes:對應的值為數組操作的詳細信息,包括索引等
            kind:   對應的值為數組操作的方式:
                     2:代表插入操作
                     3:代表刪除操作
                     4:代表替換操作
                     typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
                         NSKeyValueChangeSetting = 1,
                         NSKeyValueChangeInsertion = 2,
                         NSKeyValueChangeRemoval = 3,
                         NSKeyValueChangeReplacement = 4,
                     };
            new/old:如果是插入操作,則字典中只會有new字段,對應的值為插入的元素,前提條件是options中傳入了(NSKeyValueObservingOptionNew)
                     如果是刪除操作,則字典中只會有old字段,對應的值為刪除的元素,前提條件是options中傳入了(NSKeyValueObservingOptionOld)
                     如果是替換操作,則字典中new和old字段都可以存在,對應的值為替換后的元素和替換前的元素,前提條件是options中傳入了(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld)

        如: indexes = "<_NSCachedIndexSet: 0x600001d092e0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
            kind = 2; 
            new =     (
                1
            );
        }
     */  
    NSLog(@"%@",change);  
}

- (void)dealloc
{
    [self.person removeObserver:self forKeyPath:@"mArray"];
}

{ indexes = "<_NSCachedIndexSet: 0x6000030e5380>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 2; new = (1); }
{ indexes = "<_NSCachedIndexSet: 0x6000030e5380>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 4; new = (2); old = (1); }
{ indexes = "<_NSCachedIndexSet: 0x6000030e5380>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 3; old = (2); }

3.4 KVO 的自動觸發控制

??可以在被觀察對象的類中重寫+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key方法來控制KVO的自動觸發。
??如果我們只允許外界觀察 person 的 name 屬性,可以在 Person 類如下操作。這樣外界就只能觀察 name 屬性,即使外界注冊了對 person 對象其它屬性的監聽,那么在屬性發生改變時也不會觸發KVO

// 返回值代表允不允許觸發 KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    BOOL automatic = NO;
    if ([key isEqualToString:@"name"]) {
        automatic = YES;
    } else {
        automatic = [super automaticallyNotifiesObserversForKey:key];
    }
    return automatic;
}

??也可以實現遵循命名規則為+ (BOOL)automaticallyNotifiesObserversOf<Key>的方法來單一控制屬性的KVO自動觸發,<Key>為屬性名(首字母大寫)。

+ (BOOL)automaticallyNotifiesObserversOfName
{
    return NO;
}

注意:

  • 第一個方法的優先級高于第二個方法。如果實現了automaticallyNotifiesObserversForKey:方法,并對<Key>做了處理,則系統就不會再調用該<Key>automaticallyNotifiesObserversOf<Key>方法。
  • options指定的NSKeyValueObservingOptionInitial觸發的KVO通知,是無法被automaticallyNotifiesObserversForKey:阻止的。

3.5 KVO 的手動觸發

使用場景:

  • 使用KVO監聽成員變量值的改變;
  • 在某些需要控制監聽過程的場景下。比如:為了盡量減少不必要的觸發通知操作,或者當多個更改同時具備的時候才調用屬性改變的監聽方法。

??由于KVO的本質,重寫setter方法來達到可以通知所有觀察者對象的目的,所以只有通過setter方法或KVC方法去修改屬性變量值的時候,才會觸發KVO,直接修改成員變量不會觸發KVO
??當我們要使用KVO監聽成員變量值改變的時候,可以通過在為成員變量賦值的前后手動調用willChangeValueForKey:didChangeValueForKey:兩個方法來手動觸發KVO,如:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self.person willChangeValueForKey:@"age"];
    self.person->_age = 18;
    [self.person didChangeValueForKey:@"age"];
}

??NSKeyValueObservingOptionPrior(分別在值改變前后觸發方法,即一次修改有兩次觸發)的兩次觸發分別在willChangeValueForKey:didChangeValueForKey:的時候進行的。
??如果注冊方法中options傳入NSKeyValueObservingOptionPrior,那么可以通過只調用willChangeValueForKey:來觸發改變前的那次KVO,可以用于在屬性值即將更改前做一些操作。

3.6 KVO 新舊值相等時不觸發

??有時候我們可能會有這樣的需求,KVO監聽的屬性值修改前后相等的時候,不觸發KVO的監聽方法,可以結合KVO的自動觸發控制和手動觸發來實現。
??例如:對 person 對象的 name 屬性注冊了KVO監聽,我們希望在對 name 屬性賦值時做一個判斷,如果新值和舊值相等,則不觸發KVO,可以在 Person 類中如下這樣實現,將 name 屬性值改變的KVO觸發方式由自動觸發改為手動觸發。

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

- (void)setName:(NSString *)name
{
    if (![_name isEqualToString:name]) {
        [self willChangeValueForKey:@"name"];
        _name = name;
        [self didChangeValueForKey:@"name"];
    } 
}

3.7 KVO 手動觀察集合屬性

有些情況下我們想手動觀察集合屬性,下面以觀察數組為例。
關鍵方法:

- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;

需要注意的是,根據KVCNSMutableArray 搜索模式
傳送門:iOS - 關于 KVC 的一些總結

  • 至少要實現一個插入和一個刪除方法,否則不會觸發KVO。如
    插入方法:insertObject:in<Key>AtIndex:insert<Key>:atIndexes:
    刪除方法:removeObjectFrom<Key>AtIndex:remove<Key>AtIndexes:
  • 可以不實現替換方法,但是如果不實現替換方法,執行替換操作時,KVO會把它當成先刪除后添加,即會觸發兩次KVO。第一次觸發的KVOchange字典的old鍵的值為替換前的元素,第二次觸發的KVOchange字典的new鍵的值為替換后的元素,前提條件是注冊方法中的options傳入對應的枚舉值。
  • 如果實現替換方法,則執行替換操作只會觸發一次KVO,并且change字典會同時包含newold,前提條件是注冊方法中的options傳入對應的枚舉值。
    替換方法:replaceObjectIn<Key>AtIndex:withObject:replace<Key>AtIndexes:with<Key>:
  • 建議實現替換方法以提高性能。

示例代碼如下:

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

- (void)insertMArray:(NSArray *)array atIndexes:(NSIndexSet *)indexes
{
    [self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"mArray"];

    [self.mArray insertObjects:array atIndexes:indexes];

    [self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"mArray"];
}

- (void)removeMArrayAtIndexes:(NSIndexSet *)indexes
{
    [self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"mArray"];

    [self.mArray removeObjectsAtIndexes:indexes];

    [self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"mArray"];
}

- (void)replaceMArrayAtIndexes:(NSIndexSet *)indexes withMArray:(NSArray *)array
{
    [self willChange:NSKeyValueChangeReplacement valuesAtIndexes:indexes forKey:@"mArray"];

    [self.mArray replaceObjectsAtIndexes:indexes withObjects:array];

    [self didChange:NSKeyValueChangeReplacement valuesAtIndexes:indexes forKey:@"mArray"];
}

3.8 KVO 的依賴觀察

3.8.1 一對一關系

??有些情況下,一個屬性的改變依賴于別的一個或多個屬性的改變,也就是說當別的屬性改了,這個屬性也會跟著改變。
??比如我們想要對 Download 類中的 downloadProgress 屬性進行KVO監聽,該屬性的改變依賴于 writtenData 和 totalData 屬性的改變。觀察者監聽了 downloadProgress ,當 writtenData 和 totalData 屬性值改變時,觀察者也應該被通知。以下有兩種方法可以解決這個問題。

  1. 重寫以下方法來指明 downloadProgress 屬性依賴于 writtenData 和 totalData:
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray *affectingKeys = @[@"writtenData",@"totalData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
  1. 實現一個遵循命名規則為keyPathsForValuesAffecting<Key>的類方法,<Key>是依賴于其他值的屬性名(首字母大寫):
+ (NSSet<NSString *> *)keyPathsForValuesAffectingDownloadProgress
{
    return [NSSet setWithObjects:@"writtenData",@"totalData", nil];
}

注意:以上兩個方法可以同時存在,且都會調用,但是最終結果會以keyPathsForValuesAffectingValueForKey:為準。

3.8.2 一對多關系

??以上方法在觀察集合屬性時就不管用了。例如,假如你有一個 Department 類,它有一個裝有 Employee 類的實例對象的數組,Employee 類有 salary 屬性。你希望 Department 類有一個 totalSalary 屬性來計算所有員工的薪水,也就是在這個關系中 Department 的 totalSalary 依賴于所有 Employee 實例對象的 salary 屬性。以下有兩種方法可以解決這個問題。

  1. 你可以用KVO將 parent(比如 Department )作為所有 children(比如 Employee )相關屬性的觀察者。你必須在把 child 添加或刪除到 parent 時把 parent 作為 child 的觀察者添加或刪除。在observeValueForKeyPath:ofObject:change:context:方法中我們可以針對被依賴項的變更來更新依賴項的值:
#import "Department.h"

static void *totalSalaryContext = &totalSalaryContext;

@interface Department ()
@property (nonatomic,strong)NSArray<Employee *> *employees;
@property (nonatomic,strong)NSNumber *totalSalary;

@end


@implementation Department

- (instancetype)initWithEmployees:(NSArray *)employees
{
    self = [super init];
    if (self) {
        self.employees = [employees copy];
        for (Employee *em in self.employees) {
            [em addObserver:self forKeyPath:@"salary" options:NSKeyValueObservingOptionNew context:totalSalaryContext];
        }
    }
    return self;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
 
    if (context == totalSalaryContext) {
        [self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
    
}
 
- (void)setTotalSalary:(NSNumber *)totalSalary
{
    if (_totalSalary != totalSalary) {
        [self willChangeValueForKey:@"totalSalary"];
        _totalSalary = totalSalary;
        [self didChangeValueForKey:@"totalSalary"];
    }
}

- (void)dealloc
{
    for (Employee *em in self.employees) {
        [em removeObserver:self forKeyPath:@"salary" context:totalSalaryContext];
    }
}

@end
  1. 使用iOS中觀察者模式的另一種實現方式:通知 (NSNotification) 。

4. KVO的使用注意

4.1 移除觀察者的注意點

  • 在調用KVO注冊方法后,KVO并不會對觀察者進行強引用,所以需要注意觀察者的生命周期。至少需要在觀察者銷毀之前,調用KVO移除方法移除觀察者,否則如果在觀察者被釋放后,再次觸發KVO監聽方法就會導致Crash
  • KVO的注冊方法和移除方法應該是成對的,如果重復調用移除方法,就會拋出異常NSRangeException并導致程序Crash
  • 蘋果官方推薦的方式是,在觀察者初始化期間(init或者viewDidLoad的時候)注冊為觀察者,在釋放過程中(dealloc時)調用移除方法,這樣可以保證它們是成對出現的,是一種比較理想的使用方式。

4.2 防止多次注冊和移除相同的KVO

??有時候我們難以避免多次注冊和移除相同的KVO,或者移除了一個未注冊的觀察者,從而產生可能會導致Crash的風險。
??三種解決方案:黑科技防止多次添加刪除KVO出現的問題

  • 利用 @try @catch(只能針對刪除多次KVO的情況下)
    NSObject增加一個分類,然后利用Runtime API交換系統的removeObserver方法,在里面添加@try @catch
  • 利用 模型數組 進行存儲記錄;
  • 利用 observationInfo 里私有屬性。

4.3 其它注意點

  • 如果對象被注冊成為觀察者,則該對象必須能響應監聽方法,即該對象所屬類中必須實現監聽方法。當被觀察對象屬性發生改變時就會調用監聽方法。如果沒有實現就會導致Crash。所以KVO三部曲缺一不可。
  • keyPath傳入的是一個字符串,為避免寫錯,可以使用NSStringFromSelector(@selector(propertyName)),將屬性的getter方法SEL轉換成字符串,在編譯階段對keyPath進行檢驗。
  • 如果注冊方法中context傳的是一個對象,必須在移除觀察之前持有它的強引用,否則在監聽方法中訪問context就可能導致Crash
  • 如果是監聽集合對象的改變,需要通過KVCmutableArrayValueForKey:等方法獲得代理對象,并使用代理對象進行操作,當代理對象的內部對象發生改變時,會觸發KVO。如果直接對集合對象進行操作改變,不會觸發KVO
  • 在觀察者類的監聽方法中,應該為無法識別的context或者objectkeyPath調用父類的實現[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];

5. KVO的實現原理

Key-Value Observing Implementation Details
  • Automatic key-value observing is implemented using a technique called isa-swizzling.
  • The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
  • 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.
  • You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

??以上是蘋果官方對KVO實現的解釋,只說明了KVO是使用isa-swizzling技術來實現的,并沒有做過多介紹。

5.1 isa-swizzling

??蘋果使用了isa混寫技術(isa-swizzling)來實現KVO。當我們調用了addObserver:forKeyPath:options:context:方法,為instance被觀察對象添加KVO監聽后,系統會在運行時利用Runtime API動態創建instance對象所屬類A的子類NSKVONotifying_A,并且讓instance對象的isa指向這個全新的子類,并重寫原類A的被觀察屬性的setter方法來達到可以通知所有觀察者對象的目的。
??這個子類的isa指針指向它自己的meta-class對象,而不是原類的meta-class對象。
??重寫的setter方法的SEL對應的IMPFoundation中的_NSSetXXXValueAndNotify函數(XXXKey的數據類型),當被觀察對象的屬性發送改變時,會調用_NSSetXXXValueAndNotify函數,這個函數中會調用:

  • willChangeValueForKey:方法
  • 父類原來的setter方法
  • didChangeValueForKey:方法(內部會觸發監聽器即觀察對象observer的監聽方法:observeValueForKeyPath:ofObject:change:context:

??在移除KVO監聽后,被觀察對象的isa會指回原類A,但是NSKVONotifying_A類并沒有銷毀,還保存在內存中。

5.2 KVO 動態生成的子類都有哪些方法

??NSKVONotifying_A除了重寫了setter方法,還重寫了classdealloc_isKVOA這三個方法(可以使用runtimeclass_copyMethodList函數打印方法列表獲得),其中:

  • classclass方法中返回的是父類的class對象,目的是為了不讓外界知道KVO動態生成類的存在;
  • dealloc:釋放KVO使用過程中產生的東西;
  • _isKVOA:用來標志它是一個KVO的類。

6. FBKVOController

6.1 系統 KVO 的缺點

  • 使用比較麻煩,需要三個步驟:添加/注冊KVO監聽、實現監聽方法以接收屬性改變通知、 移除KVO監聽,缺一不可;
  • 需要手動移除觀察者,移除觀察者的時機必須合適,還不能重復移除;
  • 注冊觀察者的代碼和事件發生處的代碼上下文不同,傳遞上下文context是通過void *指針;
  • 需要實現-observeValueForKeyPath:ofObject:change:context:方法,比較麻煩;
  • 在復雜的業務邏輯中,準確判斷被觀察者相對比較麻煩,有多個被觀測的對象和屬性時,需要在方法中寫大量的if進行判斷。

6.2 FBKVOController 的介紹

FBKVOController是 Facebook 開源的一個基于系統KVO實現的框架。支持Objective-CSwift語言。
GitHub:https://github.com/facebook/KVOController

6.3 FBKVOController 的優點

  • 會自動移除觀察者;
  • 函數式編程,可以一行代碼實現系統KVO的三個步驟;
  • 實現KVO與事件發生處的代碼上下文相同,不需要跨方法傳參數;
  • 增加了blockSEL自定義操作對NSKeyValueObserving回調的處理支持;
  • 每一個keyPath會對應一個block或者SEL,不需要使用if判斷keyPath
  • 可以同時對一個對象的多個屬性進行監聽,寫法簡潔;
  • 線程安全。

6.4 FBKVOController 的使用

FBKVOController實現了觀察者和被觀察者的角色反轉,系統的KVO是被觀察者添加觀察者,而FBKVO實現了觀察者主動去添加被觀察者,實現了角色上的反轉,使用比較方便。

// create KVO controller with observer
FBKVOController *KVOController = [FBKVOController controllerWithObserver:self];
self.KVOController = KVOController;

// observe clock date property
// 使用 block
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(ClockView *clockView, Clock *clock, NSDictionary *change) {

  // update clock view with new value
  clockView.date = change[NSKeyValueChangeNewKey];
}];

// 使用 SEL
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew action:@selector(updateClockWithDateChange:)];

6.5 FBKVOController 的解析

如何優雅地使用KVO(簡書)
iOS - FBKVOController 實現原理(簡書)

參考

Key-Value Observing Programming Guide(蘋果官方文檔)
iOS - 關于 KVC 的一些總結(簡書)
KVO原理分析及使用進階(簡書)
iOS開發 - 黑科技防止多次添加刪除KVO出現的問題(簡書)
談談 KVO(簡書)
GitHub/facebook/KVOController(GitHub)
如何優雅地使用KVO(簡書)
iOS - FBKVOController 實現原理(簡書)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 本文參考鏈接: iOS KVO詳解 Foundation: NSKeyValueObserving(KVO) KV...
    擰發條鳥xds閱讀 3,009評論 0 6
  • 上半年有段時間做了一個項目,項目中聊天界面用到了音頻播放,涉及到進度條,當時做android時候處理的不太好,由于...
    DaZenD閱讀 3,038評論 0 26
  • 本文結構如下: Why? (為什么要用KVO) What? (KVO是什么) How? ( KVO怎么用) Mo...
    等開會閱讀 1,661評論 1 21
  • 本文由我們團隊的 糾結倫 童鞋撰寫。 文章結構如下: Why? (為什么要用KVO) What? (KVO是什么...
    知識小集閱讀 7,427評論 7 105
  • iOS底層原理總結 - 探尋KVO本質 對小碼哥底層班視頻學習的總結與記錄。 面試題:iOS用什么方式實現對一個對...
    愛吃兔兔的胡蘿卜吖閱讀 298評論 0 1