KVC和KVO(三)

继续我们之前的KVC和KVO系列,这是这个系列的最后一篇,这两周都没怎么写东西,估计这一周过后,文章更新会正常了,手头上的事情多了,就不想写了,写出来的东西也是粗制滥造

KVO和线程

KVO 行为是同步的 并且发生与所观察的值发生变化的同样的线程上。这听起来有点拗口,简单点说,就是监听行为发生的线程和所观察的值发生变化的线程,肯定是同一个线程,这样我们使用的时候就需要注意了:

当我们试图从其他线程改变属性值的时候我们应当十分小心,除非能确定所有的观察者都用线程安全的方法处理 KVO 通知

通常来说,我们不推荐把 KVO 和多线程混起来。如果我们要用多个队列和线程,我们不应该在它们互相之间用 KVO。

KVC

最简单的 KVC 能让我们通过以下的形式访问属性:

1
@property (nonatomic, copy) NSString *name;

取值:

1
NSString *n = [object valueForKey:@"name"]

设定:

1
[object setValue:@"Daniel" forKey:@"name"]

这个不仅可以访问作为对象属性,而且也能访问一些标量(例如 int 和 CGFloat)和 struct(例如 CGRect)。Foundation 框架会为我们自动封装它们。举例来说,如果有以下属性:

1
@property (nonatomic) CGFloat height;

我们可以这样设置:

1
[object setValue:@(20) forKey:@"height"]

键路径 (key path)

KVC 同样允许我们通过关系来访问对象。假设 person 对象有属性 address,address 有属性 city,我们可以这样通过 person 来访问 city:

1
[person valueForKeyPath:@"address.city"]

值得注意的是这里我们调用 -valueForKeyPath: 而不是 -valueForKey:

Key-Value Coding Without @property

不需要 @property 的 KVC

我们可以实现一个支持 KVC 而不用 @property 和 @synthesize 或是自动 synthesize 的属性。最直接的方式是添加 - 和 -set: 方法。例如我们想要 name ,我们这样做:

1
2
- (NSString *)name;
- (void)setName:(NSString *)name;

这完全等于 @property 的实现方式。

但是当标量和 struct 的值被传入 nil 的时候尤其需要注意。假设我们要 height 属性支持 KVC 我们写了以下的方法:

1
2
- (CGFloat)height;
- (void)setHeight:(CGFloat)height;

然后我们这样调用:

1
[object setValue:nil forKey:@"height"]

这会抛出一个 exception。要正确的处理 nil,我们要像这样 override -setNilValueForKey:

1
2
3
4
5
6
7
- (void)setNilValueForKey:(NSString *)key
{
if ([key isEqualToString:@"height"]) {
[self setValue:@0 forKey:key];
} else
[super setNilValueForKey:key];
}

集合的操作

一个常常被忽视的 KVC 特性是它对集合操作的支持。举个例子,我们可以这样来获得一个数组中最大的值:

1
2
NSArray *a = @[@4, @84, @2];
NSLog(@"max = %@", [a valueForKeyPath:@"@max.self"]);

或者说,我们有一个 Transaction 对象的数组,对象有属性 amount 的话,我们可以这样获得最大的 amount:

1
2
NSArray *a = @[transaction1, transaction2, transaction3];
NSLog(@"max = %@", [a valueForKeyPath:@"@max.amount"]);

当我们调用 [a valueForKeyPath:@”@max.amount”] 的时候,它会在数组 a 的每个元素中调用 -valueForKey:@”amount” 然后返回最大的那个。

常见的 KVO 错误

首先,KVO 兼容是 API 的一部分。如果类的所有者不保证某个属性兼容 KVO,我们就不能保证 KVO 正常工作。苹果文档里有 KVO 兼容属性的文档。例如,NSProgress 类的大多数属性都是兼容 KVO 的。

当做出改变以后,有些人试着放空的 -willChange 和 -didChange 方法来强制 KVO 的触发。KVO 通知虽然会生效,但是这样做破坏了有依赖于 NSKeyValueObservingOld 选项的观察者。详细来说,这影响了 KVO 对观察键路径 (key path) 的原生支持。KVO 在观察键路径 (key path) 时依赖于 NSKeyValueObservingOld 属性。

我们也要指出有些集合是不能被观察的。KVO 旨在观察关系 (relationship) 而不是集合。我们不能观察 NSArray,我们只能观察一个对象的属性——而这个属性有可能是 NSArray。举例说,如果我们有一个 ContactList 对象,我们可以观察它的 contacts 属性。但是我们不能向要观察对象的 -addObserver:forKeyPath:… 传入一个 NSArray。

相似地,观察 self 不是永远都生效的。而且这不是一个好的设计。

调试 KVO

你可以在 lldb 里查看一个被观察对象的所有观察信息。

1
(lldb) po [observedObject observationInfo]

这会打印出有关谁观察谁之类的很多信息。

这个信息的格式不是公开的,我们不能让任何东西依赖它,因为苹果随时都可以改变它。不过这是一个很强大的排错工具。

参考:

https://www.objccn.io/issue-7-3/