KVO初探

241 阅读2分钟

KVO使用三部曲

  1. 添加观察:addObserver:self forKeyPath:options:context:
  2. 观察回调:observeValueForKeyPath:ofObject :change:context:
  3. 移除观察:removeObserver: forKeyPath:

context 细节

在观察回调中有一个参数context,建议传入指定的值,它的主要作用是用来区分,特别是在有继承关系的情况,分别添加子类和父类的观察。 它的好处:查找更快速,性能高,不会进行多次判断嵌套。

移除观察者的重要性

针对单例添加的观察,如果不移除可能出现以下两种异常情况:

  1. 导致回调二次,影响业务逻辑处理
  2. 展示崩溃

所以,一定要养成移动观察者的好习惯。

自动或手动观察

先解读KVO中的一个方法:automaticallyNotifiesObserversForKey:

  • 返回值BOOL类型
  • 返回 YES 的情况下,类的实例对象接收到 KVC 类型的实例方法时,如 setValueForKey: 等,或变相的遵守 KVC 的该 key 的访问器方法被调用时,如 setKey: 等,KVO 内部机制会自动调用 -willChangeValueForKey:, -didChangeValueForKey:等,自动发送 Change 通知。
  • -willChangeValueForKey:, -didChangeValueForKey: 调用是触发通知的源头,这也就解释了手动发送 Change 通知时,为何需要写这两个方法。
  • 若不开启自动改变通知,则应返回 NO。
  • 从 Mac OS 10.5 开始,这个方法的默认实现逻辑是:会先查找消息接受方的类是否有 key 匹配的 +automaticallyNotifiesObserversOf[Key] 方法,如:+automaticallyNotifiesObserversOfName,若有,则返回匹配方法的结果,若没有则返回 YES。

当我们对 keyPath 注册观察者时,就会调用该方法。

手动观察

如果返回NO,KVO无法自动运作,需手动触发。因为前两个方法默认是在setter中实现的(用KVO做键值观察后,系统会在运行时重写被观察对象属性的setter)

- (void)setName:(NSString *)name {
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];
}

联动观察

通过方法:+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key,通过一个Key观察多个属性值的改变。

//添加观察
[self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];

#pragma mark - KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{

    NSLog(@"%@",change);
    NSLog(@"downloadProgress == %@",self.person.downloadProgress);
}

//触发观察
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.writtenData += 10;
    self.person.totalData  += 1;
}

//观察多个属性值的改变
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"downloadProgress"]) {
    //下载进度 -- writtenData/totalData
        NSArray *affectingKeys = @[@"totalData", @"writtenData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
- (NSString *)downloadProgress{
    if (self.writtenData == 0) {
        self.writtenData = 10;
    }
    if (self.totalData == 0) {
        self.totalData = 100;
    }
    return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}

集合类观察

比如,我们通过kvo监听观察数组里,特别注意要通过kvc的方式来获取数组,才能触发回调。

 [self mutableArrayValueForKeyPath:NSStringFromSelector(@selector(dataArray))];