KVO知識點

kim_jin發表於2017-12-13
  1. 被觀察的物件必須相容KVO。一般情況下,如果物件是繼承自NSObject的話,那麼該物件及其屬性都會自動相容KVO。
  2. 觀察者呼叫addObserver:forKeyPath:options:context:方法來將自身新增為觀察者,觀察物件的key path
  3. 為了獲取被觀察物件的key path的變化,觀察者還需要實現observeValueForKeyPath:ofObject:change:context:
  4. 在結束觀察(比方說物件被釋放等情況)的話,觀察者需要呼叫removeObserver:forKeyPath:來移除觀察。
  5. KVO跟NSNotificationCenter不同的一點是,KVO沒有一個集中的物件來提供通知給所有觀察者。在被觀察的物件發生變化時,KVO會直接給觀察者傳送訊息,沒有其他多餘的操作。

addObserver:forKeyPath:options:context:

  • options:

    • NSKeyValueObservingOptionOld: 獲取改變之前的值
    • NSKeyValueObservingOptionNew: 獲取改變之後的值
    • NSKeyValueObservingOptionInitial: 當某個屬性進行了初始化時傳送通知。只會接收到一次
    • NSKeyValueObservingOptionPrior: 在發生變回之前傳送通知
  • context:

    可以賦值為NULL,但是如果觀察者的父類也同樣觀察者同一個key path時,會出現問題。

    為了避免問題的發生,最好的方式是在類中建立一個靜態變數來作為context.

    static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
    static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
    複製程式碼

observeValueForKeyPath:ofObject:change:context:

所有觀察者都必須實現此方法來接收通知!


removeObserver:forKeyPath:context:

  1. 如果你的物件沒有被註冊為觀察者,而你又呼叫了移除觀察者的方法的話,會出現NSRangeException錯誤。避免出現此錯誤的話,最好是註冊和移除的方法配套使用,或者是將移除的方法放在try/catch中執行。
  2. 觀察者物件在被摧毀時並不會自動移除自身的觀察者身份。
  3. 通常在init或者viewDidLoad中註冊觀察者身份,在dealloc中移除。

自動傳送訊息

e.g. 能夠引起KVO訊息傳送的例子

// 呼叫訪問器方法
[account setName:@"Savings"];

// 使用 setValue:forKey:
[account setValue:@"Savings" forKey:@"name"];

// 使用 setValue:forKeyPath:
[document setValue:@"Savings" forKeyPath:@"account.name"];
複製程式碼

手動傳送訊息

某些情況下,你可能會想自己來管理訊息的程式。比方說因為某些原因減少觸發的訊息數,又或者將幾個通知合併為一個。要完成上面的需求的話,你就需要手動觸發KVO的訊息傳送。

手動和自動傳送訊息並不會衝突。一般情況下,我們只會對某一個特殊的物件進行手動的訊息處理。這樣的話,我們在繼承NSObject的時候,需要複寫automaticallyNotifiesObserversForKey:方法,並且返回NO.

e.g.

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

BOOL automatic = NO;

if ([theKey isEqualToString:@"balance"]) {

automatic = NO;

}

else {

automatic = [super automaticallyNotifiesObserversForKey:theKey];

}

return automatic;

}
複製程式碼

然後,在該屬性的訪問器方法中,呼叫willChangeValueForKeydidChangeValueForKey

- (void)setBalance:(double)theBalance {

  [self willChangeValueForKey:@"balance"];

  _balance = theBalance;

  [self didChangeValueForKey:@"balance"];

}


// or

- (void)setBalance:(double)theBalance {

  if (theBalance != _balance) {

    [self willChangeValueForKey:@"balance"];

    _balance = theBalance;

    [self didChangeValueForKey:@"balance"];

  }
}

// 如果是一對多的關係的話,還需要修改物件改變的型別(NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, 或者 NSKeyValueChangeReplacement)以及所涉及到index
- (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"];

}
複製程式碼

  • To-One RelationShips

要自動觸發一對一關係的通知的話,需要複寫keyPathsForValuesAffectingValueForKey:

比方說有一個fullName的屬性

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

fullName的屬性由firstName和lastName決定。在複寫的時候,需要指定這兩個相關的屬性

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

// 可以更改為
+ (NSSet *)keyPathsForValuesAffectingFullName {
  return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
複製程式碼
  • To-Many RelationShips
  1. 在被觀察者的相關屬性中註冊觀察者。
- (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. 如果使用Core Data的話,可以將managed context object註冊為觀察者。