KVO知识点

225 阅读3分钟
  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注册为观察者。