KVO

KVO 简介

KVO 键值观察机制,就是观察指定对象的指定属性变化情况。

KVO 键值观察 依赖于 KVC 健值编码

Key-value observing 通常用于 MVC 中,model 与 controller直接的通讯。

继承于 NSObject 才可以拥有 KVO 机制

两种 KVO 应用方式

  • 自动 KVO
  • 手动 KVO

举个例子,一个 Person 类, 一个 Account

  • [图1]

Account 的属性 余额 balance 改变时通知 Person
Account 添加观察者 Person
[account addObserver:person forKeyPath:@"balance" options:options context:context];

  • [图2]

Person 观察 Account 的属性变化
Person 中实现观察者方法 observeValueForKeyPath:ofObject:change:context:

  • [图3]

Account 移除观察者 Person
[account removeObserver:person forKeyPath:@"balance" context:context]

  • [图4]

这篇文章的重要内容

  1. Registering for Key-Value Observing 注册键值观察的过程

  2. Registering Dependent Keys 对于存在 key 依赖的键值观察

  3. Key-Value Observing Implementation Details 键值观察的实现细节


1. Registering for Key-Value Observing 注册键值观察的过程

KVO 生命周期的过程,必须完成下面这三个方法

  1. 给被观察者注册观察者 addObserver:forKeyPath:options:context:.
  2. 在观察者里实现方法 接受通知 observeValueForKeyPath:ofObject:change:context:
  3. 移除观察者 removeObserver:forKeyPath: 要在观察者的内存销毁之前 移除观察者机制

- 注册观察者 addObserver:forKeyPath:options:context:.

  • Options
    Options 影响方法 observeValueForKeyPath:ofObject:change:context: 中的 change 字典

    • NSKeyValueObservingOptionOld: change 包含 old value
    • NSKeyValueObservingOptionNew: 'change' 包含 new value
    • NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew: 即包含 old 也包含 new
    • NSKeyValueObservingOptionInitial : change 中不包含 key 的值,会在 kvo 注册的时候立即发送通知。
    • NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew : 注册kvo时立即发送通知 change 中有 new 值,这里的 new 值是注册之前 key 的值。
    • NSKeyValueObservingOptionPrior : 会在值发生改变前发出一次通知,当然改变后的通知依旧还会发出,也就是每次change都会有两个通知。值变化之前发送通知的 change 中包含一个键值对 NSKeyValueChangeNotificationIsPriorKey:@(1), 值发生变化之后的的通知 change 不包含上面提到的 键值对, 可以跟 willChange 手动通知搭配使用
    • 具体解释查看文章: KVO Options 详细介绍
  • Context
    是一个 void * 指针,可以传任意数据进去,可以防止父类跟子类同时对同一个 key 注册观察者造成的异常。

    可以采用下面的方法创建一个 Context

            static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
            static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
    

    命名规范:static void * 类名+属性名+Context

- Receiving Notification of a Change 接收通知响应

        所有的观察者必须实现方法 `observeValueForKeyPath:ofObject:change:context: message.`
  • keyPath : 注册 KVO 时的 keyPath
  • object : 被观察者对象
  • change : 根据注册 KVO 时 option 不同而展示不同的内容,change中可能会包含 keyPath 变化前后的值,对应的键有
    • NSKeyValueChangeOldKey
    • NSKeyValueChangeNewKey

keyPath是个标量或者 c 语言结构体,会把这些包装成 NSNumberNSValue,

keyPath 是个容器, change中可能会包含这个容器 inserted,removed,replaced 的情况 对应的键有

- `NSKeyValueChangeInsertion`
- `NSKeyValueChangeRemoval` 
- `NSKeyValueChangeReplacement`

keypath 是个 NSIndexSet : change 中可能会包含数组

- (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];
       }
}

- Removing an Object as an Observer 移除观察者

观察者被移除之后就不会再接受到通知。

        - (void)unregisterAsObserverForAccount:(Account*)account {
            [account removeObserver:self
                         forKeyPath:@"balance"
                            context:PersonAccountBalanceContext];
         
            [account removeObserver:self
                         forKeyPath:@"interestRate"
                            context:PersonAccountInterestRateContext];
        }

需要注意的地方

  • 不要重复移除观察者,会造成异常,如果不知道这个观察者是否已经被移除,可以在 try/catch 安全移除观察者
  • 在观察者内存销毁之前从观察者中释放出来,不然会造成内存异常
  • 无法检测一个对象是否处于观察者模式中,
    • init 或者 viewDidLoad 中添加观察者
    • dealloc 中移除观察者

Automatic Change Notification 自动 KVO 通知

NSObject 提供了自动健值更改通知的实现,自动 KVO 通知依赖于 KVC 编码机制获取, KVC method,和 集合代理(collection proxy)mutableArrayValueForKey:

手动 KVO

当你想要控制整个 KVO 的进程可以采用手动 KVO, 比如减少不必要的通知,比如将大量的通知控制到一个通知中。
手动 KVO 跟 自动 KVO 可以共存,比如同一个对象的同一个属性,可以在父类里自动 KVO, 在子类里手动 KVO, 重写方法 automaticallyNotifiesObserversForKey即可

  • 打开手动 KVO 的开关 automaticallyNotifiesObserversForKey

    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
    
       BOOL automatic = NO;
       if ([theKey isEqualToString:@"balance"]) {
           automatic = NO;
       }
       else {
           automatic = [super automaticallyNotifiesObserversForKey:theKey];
       }
       return automatic;
    }
    
    • 手动触发 KVO(一般是在 set 方法里)
      • willChangeValueForKey
      • didChangeValueForKey
      - (void)setBalance:(double)theBalance {
          if (theBalance != _balance) {
              [self willChangeValueForKey:@"balance"];
              _balance = theBalance;
              [self didChangeValueForKey:@"balance"];
          }
      }
      
    • 也可以把多个触发 KVO 的 key 放到一起
      - (void)setBalance:(double)theBalance {
          [self willChangeValueForKey:@"balance"];
          [self willChangeValueForKey:@"itemChanged"];
          _balance = theBalance;
          _itemChanged = _itemChanged+1;
          [self didChangeValueForKey:@"itemChanged"];
          [self didChangeValueForKey:@"balance"];
      }
      
    • 容器内部更改时,采用采用手动 KVO(当容器内容修改时,会触发 KVO)
      注意:根据容器变化的类型去设置对应的 NSKeyValueChange
      • NSKeyValueChangeInsertion
      • NSKeyValueChangeRemoval
      • NSKeyValueChangeReplacement

      针对容器对象的 KVO ,需要借用 KVC 机制创建的 容器代理。

    - (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"];
    }
    

2.KVO - 注册依赖 key

单个关系

重写下面两个方法中的一个即可

  • keyPathsForValuesAffectingValueForKey

  • keyPathsForValuesAffecting
    用例如下

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

多个关系

比如,有个部门类 Department,有个员工类 Employees, 每个员工的薪水都会影响这个部门的总薪水。
可以在 Department 添加、删除 Employees 的时候,给 Employees 注册、移除观察者 Employees

用例如下

```
- (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:@"[email protected]"]];
}
 
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
 
    if (totalSalary != newTotalSalary) {
        [self willChangeValueForKey:@"totalSalary"];
        _totalSalary = newTotalSalary;
        [self didChangeValueForKey:@"totalSalary"];
    }
}
 
- (NSNumber *)totalSalary {
    return _totalSalary;
}
```

3. KVO 内部实现细节

使用 isa-swizzling 原理
当 对象的属性注册到观察者中时,会创建一个中间类,重写了被观察属性的 setter 方法。

  • KVO-demo
  • KVO官方文档 Introduction to Key-Value Observing Programming Guide - KVO官方文档

你可能感兴趣的:(KVO)