OC键值观察KVO


  • 什么是KVO?

什么是KVO?KVO是Key-Value Observing的简称,翻译成中文就是键值观察。这是iOS支持的一种机制,用来做什么呢?我们在开发应用时经常需要进行通信,比如一个model的某个数据变化了,界面上要进行相应的变化,但是如果我们程序并不知道数据什么时候会进行变化,总不能一直循环判断有没有变化吧,那么就需要在数据变化时给controlller发送一个通知,告知我变化了,你可以更新显示内容了,通知的方式有很多种,比如Notification也是其中一种方式,本文要讲解的KVO也是其中一种很轻巧的方式。


他的实现机制为,为可能改变的数据增加一个观察者,在上面的说法中这个观察者就是controller,它去观察这个数据有没有发生变化,一旦发生变化,就会得到一个信号,从而获取到变化的数据,进行自己要做的操作。

实例效果:

OC键值观察KVO

 

如上图所示,界面上设置两个label,一个显示名字,一个显示分数。还有一个按钮,用来修改分数,现在要做到点击按钮分数变化。


可能你会觉得很简单,直接在按钮的响应方法中将分数的label内容修改不就可以了吗,确实如此,但是这里我们不这么做,而是使用KVO来完成。


我们创建一个学生模型,这个模型有两个属性,一个为姓名,一个为分数。label这是读取模型的数据来进行显示。


现在我们给这个实例化了的学生模型添加一个观察者,定义为我要观察学生模型的分数变化情况,这时,如果这个学生模型的分数发生了变化,比如在按钮响应中只对模型的分数属性进行修改,KVO这个机制就会自动给观察者发送通知,说这个属性变化了,你要做什么操作赶紧做。


于是我们在观察者的KVO回调函数中进行相应的操作,如果我们收到了分数变化的通知,那么就将分数label的值给修改为当前的分数。这样就实现了一套KVO键值观察的流程,当然最后还缺一步就是移除观察者,不过要在确实需要移除的时候再移除,因为移除后就不再会收到变化的通知了。


  • 实现方式


上面例子中进行了一套KVO键值观察的流程,我们整理一下进行了哪些工作:


  1. 设计界面样式
  2. 建立学生模型
  3. 对学生的分数属性添加观察
  4. 修改学生的分数属性
  5. 在观察到变化的响应方法中进行界面更新操作
  6. 不再需要观察的时候移除观察

现在通过这个例子来一步步讲解。


设计样式


样式就不说了,两个label,一个按钮,以及按钮的响应方法,都是很常见的。


建立模型


这个部分,就是新建一个NSObject类,用来作为学生模型,有两个属性:姓名和分数,如下所示:


// StudentModel.h
@interface StudentModel : NSObject @property (nonatomic, copy) NSString *name;
@property float score; @end // ViewController.m
// 在controller中实例化学生模型
self.studentModel = [[StudentModel alloc] init];
[self.studentModel setValue:@"Cloudox" forKey:@"name"];
[self.studentModel setValue:@"89.0" forKey:@"score"];

添加观察


这一步,才是真正开始使用KVO了。


要使用KVO,至少必须要实现两个方法:


  • addObserver:forkeyPath:options:context:
  • observeValueForKeyPath:ofObject:change:context:

第二个方法,就是用来获取数据变化的通知并进行相应操作的方法,这个我们后面再讲,先讲第一个方法,顾名思义,这就是用来添加观察者的方法了。


可能你会注意到,我们上面实例化学生模型的时候,使用的是 setVlue:forKey: 的形式来设置属性值的,为什么要这样设置呢?联想到KVO的名字,键值观察,就能大概明白了,学生模型的属性名就相当于key,属性值就相当于值。


紧接着就可以对分数来添加观察了:


[self.studentModel addObserver:self forKeyPath:@"score" options:NSKeyValueObservingOptionNew             context:nil];
 

这里使用的就是第一个方法,有四个参数。


  • 第一个参数是观察者,这里被观察者是学生模型,观察者是controller,也就是self
  • 第二个参数是keyPath,其实也就是要观察的键
  • 第三个是一个options,这里我们写的是一个枚举值,这个地方可以填几种值,下面在进行详细的说明
  • 第四个我们填了nil,也有其作用,下面再细说

总之通过这行代码,我们就对score这个键,也即是分数添加了观察。


修改数据


在按钮的响应方法中修改学生模型的分数数据,同样使用 setVlue:forKey: 的方式进行设置。


接收通知


这里就用到第二个方法:observeValueForKeyPath:ofObject:change:context:


先看看这个例子中的实现:


// KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"score"]) {
        self.scoreLabel.text = [NSString stringWithFormat:@"Score:%@", [self.studentModel valueForKey:@"score"]];
    }
}

可以看到这个回调的变化响应方法也有四个参数,其实就对应上面添加观察时的四个参数,通过keyPath,我们可以判断是不是我们想要接收的数据变化,判断它是不是score,其实也就是对不同的被观察者进行不同的操作。确实是分数变化后,我们就更新界面上的分数label,用新的分数来显示。


移除通知


移除通知的方法很简单,如下:


[self.studentModel removeObserver:self forKeyPath:@"score"];

从观察者那边移除对被观察者特定键的观察。


至此,一个简单的KVO流程就走完了,很简单对吧。


  • 进阶用法


传递对象


上面添加观察者和响应变化的方法中都有一个 context参数,通过这个参数可以传递一些东西,在添加观察者时设置要传递的内容,在响应变化时获得传递的内容。


比如我要传递一个字符串,在添加观察者时设置:


[self.studentModel addObserver:self forKeyPath:@"score" options:NSKeyValueObservingOptionNew context:@"heyMe"];// 2.通过context传递内容给观察者
 

这里在context的参数中就直接设置了要传递的对象字符串。然后在变化响应时:


// KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"score"]) {
        self.scoreLabel.text = [NSString stringWithFormat:@"Score:%@", [self.studentModel valueForKey:@"score"]];
    }
    NSLog(@"%@", context);// 通过context获取被观察者传递的内容
}
 

这里就可以输出context看一下,会得到传递过来的字符串内容。


options参数


在添加观察者时有一个options参数,在回调获取变化时有一个change参数,这两个参数其实是对应的,都是用来增加传递变化的丰富度。


options参数可以设为:


  • NSKeyValueObservingOptionOld:这表示在回调获取变化时可以通过change参数获取变化之前的值;
  • NSKeyValueObservingOptionNew:这表示在回调获取变化时可以通过change参数获取变化后的值;
  • NSKeyValueObservingOptionInitial:在添加观察者方法return的时候就发出一次通知;
  • NSKeyValueObservingOptionPrior:会在观察的值发生变化前发出一次通知,变化后还是会发出一次通知,所以变化一次一共会得到两次通知。

以上就是options参数,可以看到都是对应change参数的,用来决定change参数可以得到什么样的数据,在回调获取变化时可以输出change看一下,就可以知道不同的效果了。


change参数


在使用change的时候可以通过下面的key来操作:


  • NSKeyValueChangeKindKey:对应NSKeyValueChange的枚举值
    • NSKeyValueChangeSetting = 1:说明被观察的数据的setter方法被调用了;
    • NSKeyValueChangeInsertion = 2:当观察的数据是集合时,且对它进行insert操作时会返回该值;
    • NSKeyValueChangeRemoval = 3:当观察的数据是集合时,且对它进行remove操作时会返回该值;
    • NSKeyValueChangeReplacement = 4:当观察的数据是集合时,且对它进行replace操作时会返回该值。
  • NSKeyValueChangeNewKey:对应options参数中的NSKeyValueObservingOptionNew,会在其中包含观察的数据变化后的新值
  • NSKeyValueChangeOldKey:对应options参数中的NSKeyValueObservingOptionOld,会在其中包含观察的数据变化之前得旧值
  • NSKeyValueChangeIndexesKey:当NSKeyValueChangeKindKey是2、3、4的时候,也就是说是观察集合数据时,这个key的值是一个NSIndexSet,包含操作对象的索引集合
  • NSKeyValueChangeNotificationIsPriorKey:包含一个布尔值,如果options的参数是NSKeyValueObservingOptionPrior,也就是会通知两次,在第一次通知,也就是改变前的通知时,会包含这个key

关于这些change的值,都可以输出到控制台试一下看看是什么效果,会有更加直观的感受。


手动通知


之前说的都是自动通知,当添加了观察者后,只要发生改变就会自动通知观察者,但有时候我们并不是什么改变都希望得到通知,或者有时候是希望变化到什么情况后再通知,这就需要改变通知的机制。默认的实现模式为自动通知的模式,要自定义何时进行通知,就要改成手动通知的模式。


要改成手动通知,首先要在被观察者的模型中重写一个方法 automaticallyNotifiesObserversForKey :


// StudentModel.m
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    BOOL automatic = NO;
    if ([key isEqualToString:@"score"]) {
        automatic = NO;
    } else {
        automatic = [super automaticallyNotifiesObserversForKey:key];
    }
    return automatic;
}
 

这里我们在学生模型的实现文件中重写了这个方法,判断当观察的key是score分数时,就将自动通知关闭,其余的情况还是根据父类来进行判断,这样写比较保险。


这样在我们改变学生模型的分数时,就不会自动触发通知了,要触发通知,需要自己进行设置:


// 按钮响应
- (void)changeScore {
    [self willChangeValueForKey:@"score"];// 改为手动通知
    [self.studentModel setValue:@"99.0" forKey:@"score"];
    [self didChangeValueForKey:@"score"];// 改为手动通知
}
 

这时就可以触发通知了,如果一个操作会触发多个属性改变,都要发通知,那么需要嵌套通知:


// 按钮响应
- (void)changeScore {
    [self willChangeValueForKey:@"name"];// 改为手动通知
    [self willChangeValueForKey:@"score"];// 改为手动通知
    [self.studentModel setValue:@"Cloud" forKey:@"name"];
    [self.studentModel setValue:@"99.0" forKey:@"score"];
    [self didChangeValueForKey:@"score"];// 改为手动通知
    [self didChangeValueForKey:@"name"];// 改为手动通知
}
 

而在一个一对多的关系中,比如集合,你必须注意不仅仅是这个key改变了,还有它改变的类型以及索引,也就是我们change中对应的几种涉及到集合的东西,如下所示:


 - (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还有一个重要的点是键值依赖,也就是说一个属性的值依赖于对象中的其他属性,当那些属性变化后,这个属性的值自动被通知到进行修改。

 
上一篇:K-V-O 键值观察机制


下一篇:更换Mac记录