什么是KVO?
什么是KVO?KVO是Key-Value Observing的简称,翻译成中文就是键值观察。这是iOS支持的一种机制,用来做什么呢?我们在开发应用时经常需要进行通信,比如一个model的某个数据变化了,界面上要进行相应的变化,但是如果我们程序并不知道数据什么时候会进行变化,总不能一直循环判断有没有变化吧,那么就需要在数据变化时给controlller发送一个通知,告知我变化了,你可以更新显示内容了,通知的方式有很多种,比如Notification也是其中一种方式,本文要讲解的KVO也是其中一种很轻巧的方式。
他的实现机制为,为可能改变的数据增加一个观察者,在上面的说法中这个观察者就是controller,它去观察这个数据有没有发生变化,一旦发生变化,就会得到一个信号,从而获取到变化的数据,进行自己要做的操作。
实例效果:
如上图所示,界面上设置两个label,一个显示名字,一个显示分数。还有一个按钮,用来修改分数,现在要做到点击按钮分数变化。
可能你会觉得很简单,直接在按钮的响应方法中将分数的label内容修改不就可以了吗,确实如此,但是这里我们不这么做,而是使用KVO来完成。
我们创建一个学生模型,这个模型有两个属性,一个为姓名,一个为分数。label这是读取模型的数据来进行显示。
现在我们给这个实例化了的学生模型添加一个观察者,定义为我要观察学生模型的分数变化情况,这时,如果这个学生模型的分数发生了变化,比如在按钮响应中只对模型的分数属性进行修改,KVO这个机制就会自动给观察者发送通知,说这个属性变化了,你要做什么操作赶紧做。
于是我们在观察者的KVO回调函数中进行相应的操作,如果我们收到了分数变化的通知,那么就将分数label的值给修改为当前的分数。这样就实现了一套KVO键值观察的流程,当然最后还缺一步就是移除观察者,不过要在确实需要移除的时候再移除,因为移除后就不再会收到变化的通知了。
实现方式
上面例子中进行了一套KVO键值观察的流程,我们整理一下进行了哪些工作:
- 设计界面样式
- 建立学生模型
- 对学生的分数属性添加观察
- 修改学生的分数属性
- 在观察到变化的响应方法中进行界面更新操作
- 不再需要观察的时候移除观察
现在通过这个例子来一步步讲解。
设计样式
样式就不说了,两个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还有一个重要的点是键值依赖,也就是说一个属性的值依赖于对象中的其他属性,当那些属性变化后,这个属性的值自动被通知到进行修改。