iOS - 關於 KVO 的一些總結

網絡配圖.jpg

1. 什麼是 KVO

  • KVO的全稱是Key-Value Observing,俗稱「鍵值觀察/監聽」,是蘋果提供的一套事件通知機制,容許一個對象觀察/監聽另外一個對象指定屬性值的改變。當被觀察對象屬性值發生改變時,會觸發KVO的監聽方法來通知觀察者。KVO是在MVC應用程序中的各層之間進行通訊的一種特別有用的技術。
  • KVONSNotificationCenter都是iOS中觀察者模式的一種實現。
  • KVO能夠監聽單個屬性的變化,也能夠監聽集合對象的變化。監聽集合對象變化時,須要經過KVCmutableArrayValueForKey:等可變代理方法得到集合代理對象,當代理對象的內部對象發生改變時,會觸發KVO的監聽方法。集合對象包含NSArrayNSSet
  • KVOKVC有着密切的關係,若是想要深刻了解KVO,建議先學習KVC
    傳送門:iOS - 關於 KVC 的一些總結

2. KVO 的基本使用

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

  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 監聽方法

若是對象被註冊成爲觀察者,則該對象必須能響應如下監聽方法,即該對象所屬類中必須實現監聽方法。當被觀察對象屬性發生改變時就會調用監聽方法。若是沒有實現就會致使Crashgit

- (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監聽方法就會致使Crashgithub

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
複製代碼

2.4 使用示例

- (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 = ""; }
context:(null)編程

2.5 實際應用

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

斯坦福大學 KVO示例

2.6 KVO觸發監聽方法的方式

KVO觸發分爲自動觸發和手動觸發兩種方式。安全

2.6.1 自動觸發

① 若是是監聽對象特定屬性值的改變,經過如下方式改變屬性值會觸發KVO網絡

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

② 若是是監聽集合對象的改變,須要經過KVCmutableArrayValueForKey:等方法得到代理對象,並使用代理對象進行操做,當代理對象的內部對象發生改變時,會觸發KVO。集合對象包含NSArrayNSSetapp

2.6.2 手動觸發

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

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
複製代碼

NSArray對象使用:ide

- (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:github.com/facebook/KV…

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 實現原理(簡書)

相關文章
相關標籤/搜索