正确使用KVO的姿势

使用KVO的前提条件

  1. 该类必须满足KVC命名约定查看此处
  2. 该类可以触发属性变更的KVO通知继承自NSObject的类默认由NSObject实现该功能
  3. 依赖的属性被正确的注册到KVO。如:fullName依赖lastNamefirstName

使用方式

  1. 添加观察者:addObserver:forKeyPath:options:context:,如:需要观察Account对象的balance属性
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;

[account addObserver:self
              forKeyPath:@"balance"
                 options:(NSKeyValueObservingOptionNew |
                          NSKeyValueObservingOptionOld)
                 context:PersonAccountBalanceContext];
  1. 观察回调方法处理:observeValueForKeyPath:ofObject:change:context:
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    
    if (context == PersonAccountBalanceContext && [keyPath isEqualToString:@"balance"]) {
        
        NSLog(@"Do something with the balance…");
        
    } else {
        // Any unrecognized context must belong to super
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}
  1. 移除观察者:removeObserver:forKeyPath:context:
[account removeObserver:self
             forKeyPath:@"balance"
                context:PersonAccountBalanceContext];

以上是我们常见的使用KVO的方式,在具体实践中还有许多坑要踩,接下来我们逐个探讨。

进阶使用

截止目前我们使用KVO的方式都是依赖系统实现的自动触发机制,在有些情况下我们需要更精确的控制KVO的触发时机,此时需要手动触发KVO。

  1. 手动触发KVO

手动触发KVO需要覆写NSObjectautomaticallyNotifiesObserversForKey:方法。该方法默认返回YES,调用自动触发KVO的逻辑。对需要手动触发KVO的属性需要变更该方法的返回值为NO。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    BOOL automatic = NO;
    
    // 手动触发balance属性KVO
    if ([key isEqualToString:@"balance"]) {
        automatic = NO;
    } else {
    
        // 其它属性KVO自动触发
        automatic = [super automaticallyNotifiesObserversForKey:key];
    }
    return automatic;
}

实现手动触发KVO通知还需要在属性值变更前调用willChangeValueForKey:变更后调用didChangeValueForKey:

- (void)setBalance:(double)theBalance {
    if (theBalance != _balance) {
        [self willChangeValueForKey:@"balance"];
        _balance = theBalance;
        [self didChangeValueForKey:@"balance"];
    }
}

从OS X 10.5开始automaticallyNotifiesObserversForKey:会在被观察的类中查找+automaticallyNotifiesObserversOf方法,其中表示被观察类的属性。以balance属性为例,实现automaticallyNotifiesObserversOfBalance即可,此时不需要再覆写automaticallyNotifiesObserversForKey:

+ (BOOL)automaticallyNotifiesObserversOfBalance {
    return NO;
}
  1. 依赖属性KVO

在许多情况下一个属性需要依赖其它属性值,例如:fullName是由firstNamelastName组成。

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@", _firstName, _lastName];
}

假设我们需要对fullName做观察,当firstNamelastName有变化时自动更新fullName的值并触发KVO通知。

实现依赖属性KVO有以下两种方式。

  • 实现keyPathsForValuesAffectingValueForKey:方法
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
  • 实现keyPathsForValuesAffecting方法,其中表示被观察对象的属性
+ (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"firstName",@"lastName", nil];
}
  • 注意事项:在category中依赖属性观察不能覆写keyPathsForValuesAffectingValueForKey:方法,因为category中不能覆写方法。在category中可以通过实现keyPathsForValuesAffecting方法实现依赖属性观察。

注意事项

  1. keypath使用方式优化

截止目前我们都是以字符串的方式使用keypath,这种方式使得编译器在编译期间不能及时的发现错误,一种比较好的方式是通过NSStringFromSelector@selector结合的方式来使用keypath,如下所示。

 [self addObserver:self
               forKeyPath:NSStringFromSelector(@selector(fullName))
                  options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                  context:FullNameContext];
  1. Context使用

在使用KVO时有一个关键点就是在添加观察者时传入一个唯一的context,如下所示。

// 观察fullName的context
static void *FullNameContext = &FullNameContext;
...
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    
     if (context == FullNameContext && [keyPath isEqualToString:NSStringFromSelector(@selector(fullName))]) {
        // do somthing with fullName
    } else {
        // Any unrecognized context must belong to super
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}

这样会保证我们观察的子类都是正确的,子类和父类都能安全的观察同样的键值而不会冲突。否则我们将会碰到难以 debug 的奇怪行为。

安全的移除观察者

当一个观察者完成了监听对象的改变使命,需要调用–removeObserver:forKeyPath:context:来移除观察,否则会出现程序崩溃的情况。
当重复移除观察者时同样会导致程序崩溃。

目前还没有公开的API来检测一个对象是否被注册为观察者。
要安全的移除观察者可以通过以下两种方式来实现。

  1. try/catch
- (void)dealloc {
   
    @try {
        [self removeObserver:self
                  forKeyPath:NSStringFromSelector(@selector(fullName))
                     context:FullNameContext];
    } @catch (NSException * exception) {
        
    }
}
  1. 遍历observationInfo

通过查阅API我们发现NSObject有一个observationInfo属性,官方文档对该属性的描述如下。

Returns a pointer that identifies information about all of the observers that are registered with the observed object.

基于此可以通过KVC的方式获取到对象是否注册相关keypath的观察者

observationInfo结构
// 按key检索
- (BOOL)observerKeyPath:(NSString *)key observer:(id)observer
{
    id info = self.observationInfo;
    NSArray *array = [info valueForKey:@"_observances"];
    for (id objc in array) {
        id Properties = [objc valueForKeyPath:@"_property"];
        id newObserver = [objc valueForKeyPath:@"_observer"];
        
        NSString *keyPath = [Properties valueForKeyPath:@"_keyPath"];
        if ([key isEqualToString:keyPath] && (newObserver == observer)) {
            return YES;
        }
    }
    return NO;
}

实现原理

KVO的实现官方文档中提到使用了isa-swizzling技术,实现思路如下

当你观察一个对象时,一个新的类会被动态创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象:值的更改。最后通过 isa 混写(isa-swizzling) 把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。

并未透漏更多细节内容。
关于KVO实现细节的探究可以参考KVO实现原理。

优雅的使用KVO

关于KVO被吐槽最多的就是其晦涩的API和使用方式,如何解决这个问题呢,可以使用Facebook开源的KVOController,使用方式如下

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

// observe clock date property
[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];
}];

使用KVOContoller解决了以下问题。

  • 不需要再手动移除观察者
  • 使用Block方式降低接口使用复杂度
  • 不再需要if判断keypath

参考

Key-Value Observing Programming Guide

Key-Value Observing

KVO和KVC

如何优雅地使用 KVO

KVO实现原理

你可能感兴趣的:(正确使用KVO的姿势)