KVC和KVO(二)

上一周实在是手头上的事情太多,项目忙着出版本,都没怎么写点东西,估计这一周也不会轻松,感觉已经好久没动了,还是需要积累点东西的。上一篇文章,我们讲到了KVC和KVO,我们接着完成这个系列,这个系列是我在喵神的Objc上看到的,结合自己的一点理解,记录下来。

在上一篇文章中,我们讲到,通过键值观察,属性依赖,来讲解KVC,如果没有看过上一篇文章的,请移步
http://ppsheep.com/2016/11/19/KVC和KVO(一)/

手动通知和自动通知

在OC中,消息通知扮演着很重要的角色,在我们在调用 LabColor 类的 -setLComponent:方法时,会触发以下方法的调用

1
- (void)willChangeValueForKey:(NSString *)key
1
- (void)didChangeValueForKey:(NSString *)key

会在运行 -setLComponent: 中的代码之前以及之后被自动调用。

有些情况下当我们需要 override -setLComponent: 并且我们要控制是否发送键值改变的通知的时候,我们要做以下的事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//是否自动发送通知
+ (BOOL)automaticallyNotifiesObserversForLComponent;
{
return NO;
}

- (void)setLComponent:(double)lComponent;
{
if (_lComponent == lComponent) {
return;
}
//手动调用
[self willChangeValueForKey:@"lComponent"];
_lComponent = lComponent;
[self didChangeValueForKey:@"lComponent"];
}

我们关闭了 -willChangeValueForKey: 和 -didChangeValueForKey: 的自动调用,然后我们手动调用他们。我们只应该在关闭了自动调用的时候我们才需要在 setter 方法里手动调用 -willChangeValueForKey: 和 -didChangeValueForKey:。大多数情况下,我们都不会这样做,这样做,会让代码变得复杂,不容易理解

KVO和context

有时我们会有理由不想用 KeyValueObserver 辅助类。创建另一个对象会有额外的性能开销。如果我们观察很多个键的话,这个开销可能会变得明显。

如果我们在实现一个类的时候把它自己注册为观察者的话:

1
2
3
4
- (void)addObserver:(NSObject *)anObserver
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context

一个非常重要的点是我们要传入一个这个类唯一的 context。推荐把以下代码

1
static int const PrivateKVOContext;

写在这个类 .m 文件的顶端,然后我们像这样调用 API 并传入 PrivateKVOContext 的指针:

我们在LabColor类中定义一个方法

1
2
//将自己设置为观察者
- (void)setObserverWithSelf;

在本类中实现方法

1
2
3
-(void)setObserverWithSelf{
[self addObserver:self forKeyPath:@"lComponent" options:NSKeyValueObservingOptionInitial context:(void *)&PrivateKVOContext];
}

然后我们这样写 -observeValueForKeyPath:… 的方法:

1
2
3
4
5
6
7
8
9
10
11
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if (context == &PrivateKVOContext) {
// 这里写相关的观察代码
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}

这将确保我们写的子类都是正确的。如此一来,子类和父类都能安全的观察同样的键值而不会冲突。否则我们将会碰到难以 debug 的奇怪行为。

进阶KVO

我们常常需要当一个值改变的时候更新 UI,但是我们也要在第一次运行代码的时候更新一次 UI。我们可以用 KVO 并添加 NSKeyValueObservingOptionInitial 的选项 来一箭双雕地做好这样的事情。这将会让 KVO 通知在调用 -addObserver:forKeyPath:… 到时候也被触发。

之前和之后

如果我们注册通知的时候附加了 NSKeyValueObservingOptionPrior 选项,我们将会收到两个通知:一个在值变更前,另一个在变更之后。变更前的通知将会在 change 字典中有不同的键。我们可以像以下这样区分通知是在改变之前还是之后被触发的:

1
2
3
4
5
if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) {
// 改变之前
} else {
// 改变之后
}

如果我们需要改变前后的值,我们可以在 KVO 选项中加入 NSKeyValueObservingOptionNew 和/或 NSKeyValueObservingOptionOld。

更简单的办法是用 NSKeyValueObservingOptionPrior 选项,随后我们就可以用以下方式提取出改变前后的值:

1
2
id oldValue = change[NSKeyValueChangeOldKey];
id newValue = change[NSKeyValueChangeNewKey];

通常来说 KVO 会在 -willChangeValueForKey: 和 -didChangeValueForKey: 被调用的时候存储相应键的值。

索引

KVO 对一些集合类也有很强的支持,以下方法会返回集合对象:

1
2
3
-mutableArrayValueForKey:
-mutableSetValueForKey:
-mutableOrderedSetValueForKey:

我将会详细解释这是怎么工作的。如果你使用这些方法,change 字典里会包含键值变化的类型(添加、删除和替换)。对于有序的集合,change 字典会包含受影响的 index。

集合代理对象和变化的通知在用于更新UI的时候非常有效,尤其是处理大集合的时候。但是它们需要花费你一些心思。

后面将会结合UI层面,来对KVC和KVO进行进一步讲解

参考:

https://www.objccn.io