KVO(Key-Value Observing)

一、KVO简介

KVOObjective-C 对观察者模式(Observer Pattern)的实现,也是 Cocoa Binding 的基础。当被观察对象的某个属性发生更改时,观察者对象会获得通知。

二、KVO的基本使用

  1. 通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件
 /*
@observer:观察者
@keyPath:想要观察的对象属性
@options:options一般选择NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld,这样当属性值发生改变时我们可以同时获得旧值和新值,如果我们只填NSKeyValueObservingOptionNew则属性发生改变时只会获得新值
@context:想要携带的其他信息
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

  1. 在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者
/*
@keyPath:观察的属性
@object:观察的是哪个对象的属性
@change:这是一个字典类型的值,通过键值对显示新的属性值和旧的属性值
@context:添加观察者时携带的信息
*/
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void *)context;
  1. 当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。注意调用removeObserver需要在观察者消失之前,否则会导致Crash
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

三、KVO实现机制

KVO是通过isa混写(isa-swizzling)技术实现。

  1. 当观察一个对象时,一个新的类会动态被创建(动态添加的类名就是在原来类的类名前加上NSKVONotifying_类名);
  2. 这个类继承自该对象原本的类,并重写被观察属性的 setter 方法
  3. 重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象值的更改
  4. 最后把这个对象的 isa指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就变成了新创建的子类的实例。

四、KVO的自动触发与手动触发

KVO观察的开启和关闭有两种方式,自动手动
自动开关,返回NO,就监听不到,返回YES,表示监听;对于想要手动通知的属性,可以根据它的keyPath返回NO,而其对于其他位置的keyPath,要返回父类的这个方法。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
       BOOL automatic = NO;
       if ([theKey isEqualToString:@"openingBalance"]) {
           automatic = NO;
       } else {
           automatic = [super automaticallyNotifiesObserversForKey:theKey];
       }
       return automatic;
}

要实现手动通知,你需要在值改变前调用 willChangeValueForKey:方法,在值改变后调用 didChangeValueForKey: 方法。你可以在发送通知前检查值是否改变,如果没有改变就不发送通知。

- (void)setOpeningBalance:(double)theBalance {
       if (theBalance != _openingBalance) {
        [self willChangeValueForKey:@"openingBalance"];
        _openingBalance = theBalance;
        [self didChangeValueForKey:@"openingBalance"];
       }
}

使用手动开关的好处就是你监听就监听,不想监听关闭即可,比自动触发更方便灵活

如果一个操作会导致多个属性改变,你需要嵌套通知,像下面这样:

- (void)setOpeningBalance:(double)theBalance {
       [self willChangeValueForKey:@"openingBalance"];
       [self willChangeValueForKey:@"itemChanged"];
       _openingBalance = theBalance;
       _itemChanged = _itemChanged+1;
       [self didChangeValueForKey:@"itemChanged"];
       [self didChangeValueForKey:@"openingBalance"];
}

在一个一对多的关系中,你必须注意不仅仅是这个key改变了,还有它改变的类型以及索引。

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

五、KVO一对多,键值依赖

通过注册一个KVO观察者,可以监听多个属性的变化

比如目前有一个需求,需要根据总的下载量totalData 和当前下载量currentData 来计算当前的下载进度currentProcess,实现有两种方式

  • 分别观察totalDatacurrentData 两个属性,当其中一个发生变化计算currentProcess
  • 实现keyPathsForValuesAffectingValueForKey方法,将两个观察合为一个观察,即观察当前下载进度currentProcess
//1、合二为一的观察方法
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"currentProcess"]) {
        NSArray *affectingKeys = @[@"totalData", @"currentData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

//2、注册KVO观察
[self.person addObserver:self forKeyPath:@"currentProcess" options:(NSKeyValueObservingOptionNew) context:NULL];

//3、触发属性值变化
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    self.person.currentData += 10;
    self.person.totalData  += 1;
}

//4、移除观察者
- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"currentProcess"];
}

你也可以通过实现 keyPathsForValuesAffecting 方法来达到前面同样的效果,这里的就是属性名,不过第一个字母要大写,用前面的例子来说就是这样:

+ (NSSet *)keyPathsForValuesAffectingCurrentProcess {
    return [NSSet setWithObjects:@"totalData", @"currentData", nil];
}

六、KVO观察 可变数组

KVO是基于KVC基础之上的,所以可变数组如果直接添加数据,是不会调用setter方法的,即直接通过[self.person.dateArray addObject:@"1"];向数组添加元素,是不会触发KVO通知回调的

在KVC官方文档中,针对可变数组的集合类型,有如下说明,即访问集合对象需要需要通过mutableArrayValueForKey方法,这样才能将元素添加到可变数组中;

// KVC 用此方法添加则可以触发KVO
    [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];

七、其他知识点

  • 通过KVC修改属性会触发KVOKVC 内部做了监听操作
  • 直接修改成员变量不会触发 KVO,没走set方法
  • 哪些情况下使用kvo会崩溃,怎么防护崩溃?

1.removeObserver一个未注册的keyPath,导致错误:Cannot remove an observer A for the key path "str",because it is not registered as an observer.
解决办法:根据实际情况,增加一个添加keyPath的标记,在dealloc中根据这个标记,删除观察者。
2.添加的观察者已经销毁,但是并未移除这个观察者,当下次这个观察的keyPath发生变化时,kvo中的观察者的引用变成了野指针,导致crash
解决办法:在观察者即将销毁的时候,先移除这个观察者。
其实还可以将观察者observer委托给另一个类去完成,这个类弱引用被观察者,当这个类销毁的时候,移除观察者对象

  • kvo的优缺点

优点:
1.能够提供一种简单的方法实现两个对象间的同步
2.能够对非我们创建的对象,即内部对象的状态改变做出响应,而且不需要改变内部对象的实现
3.能够提供观察的属性的最新值以及先前值
4.用key paths来观察属性,因此也可以观察嵌套对象
5.完成了对观察对象的抽象,因为不需要额外的代码来允许观察值能够被观察
缺点:
1.我们观察的属性必须使用string来定义,因此在编译期不会出现警告以及检查

2.对属性重构将导致我们的观察代码不再可用
3.只能通过重写 -observeValueForKeyPath:ofObject:change:context:方法来获得通知。
4.不能通过指定selector的方式获取通知。
5.不能通过block的方式获取通知。

  • 添加观察者和移除观察者要相对应;
  • 不要将已经释放的观察者对象,再进行移除;
  • 可以多次对同一个属性添加相同的观察者,当属性更改的时候,会多次调用接收方法,不过移除观察者也要执行多次;
  • 在iOS10及其以下,不移除观察者会出现闪退的情况,在iOS11及其以上,不会出现闪退的情况;

你可能感兴趣的:(KVO(Key-Value Observing))