KVC(key-value coding)
概要
KVC允许开发者通过名字访问属性,无需调用明确的存取方法,这样开发者就能在运行时确定属性的绑定,而不是在编译时。
- 访问器或实例变量在程序内书写时只能访问该程序的属性。例如
setName:
,那么在这个地方就只能访问一个属性。与此相对,由于键可以保存在字符串变量中,因此根据变量值是“name”还是“age”,就可以访问不同的属性。
基本处理
KVC允许开发者通过名字(键)访问属性
常用的方法:
- (id)valueForKey:(NSString *)key;
- (void)setValue:(id)value forKey:(NSString *)key;
还有一种比较常用但很好用的方法,如下所示
//KVC常用方法的使用示例如下
id aPerson = [aGroup valueForKey:@"leader"];
id name = [aPerson valueForKey:@"name"];
//上面的实例可以简化为下面这行代码
id name = [aGroup valueForKeyPath:@"leader.name"];
keyPath
称为键路径,用“.”号连接键。同样有两种方法:
- (id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
备注:keyPath版本方法更灵活,而key版本稍微快一些。如果键是通过参数传给我,通常用vauleForKeyPath:
,如果键是硬编码的,那我通常会用valueForKey:
KVO(key-value observing)
概要
KVO某个对象的属性改变时通知其它对象的机制。例如MVC中,model层数据变化便可以通过KVO,告知Controller。Cocoa有若干观察机制,包括委托和NSNotification,但是KVO的开销更小。
注意:只有在使用键值编码准则来访问访问器或实例变量的情况下,才可以监视属性的变化。在方法内直接改变实例变量变量值时,就不能监视了。具体KVC准则有三点:
- 随访问器方法而改变
- 使用
setValue:forKey:
和键进行改变。此时可能不经由访问器。 - 使用
setValue:forKeyPath:
和键路径进行改变。此时也可能不经由访问器。
基本处理
假设现在要在viewController观察一个model中name的变化,分为三个步骤,代码如下
-
首先注册KVO
[self addObserver:self //观察者 forKeyPath:@"model.name" //被观察的属性 options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld //指定属性变化时发送的通知消息中包含变化内容的字典数据中包含什么样的值 //NSKeyValueObservingOptionNew提供属性改变后的值 //NSKeyValueObservingOptionOld提供属性改变前的值 context:nil];//context见后面KVO与context解释,可以为nil
-
指定监视的属性被改变时,要实现的方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
//keyPath:被检测属性
//object:keyPath所属对象
//change:显示变化内容的字典数据,可按如下方式获得具体值
//id old = [change objectForKey:NSKeyValueChangeOldKey];
//id new = [change objectForKey:NSKeyValueChangeNewKey];
//context:返回注册观察者时指定的值。
}
```
-
要停止已经注册的监视,可使用如下方法
[self removeObserver:self forKeyPath:@"model.name"];
进阶KVO
-
KVO与context
如果我们观察很多个键的话,在实现一个类的时候,把它自己注册成为观察者的话,一个非常重要的点是要传入这个类唯一的context
,推荐把以下代码static int const PrivateKVOContext;
写在这个类.m文件的顶端,然后我们像这样调用 API 并传入 PrivateKVOContext的指针:
```
[otherObject addObserver:self forKeyPath:@"someKey" options:someOptions context:&PrivateKVOContext];
```
然后我们这样写 -observeValueForKeyPath:...的方法
```
- (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 的奇怪行为。
NSKeyValueObservingOptionInitial
我们需要在一个值改变时更新UI,也需要在第一次运行代码的时候更新UI。我们可以在-addObserver:forKeyPath:...
时,在option中增加NSKeyValueObservingOptionInitial
来设置这件事,使得KVO在调用-addObserver:forKeyPath:...
时也被触发一次。-
NSKeyValueObservingOptionPrior
注册KVO时,可以添加NSKeyValueObservingOptionPrior
选项,当KVO被触发后,我们会收到两个通知:值改变前和值改变后。变更前的通知将会在 change字典中有不同的键。可以像下面这样区分通知是在变更之前还是之后:if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) { // 改变之前 } else { // 改变之后 }
设置属性间的依赖
假如有个Person
类,类里有三个属性,fullName、firstName、lastName
。按照之前的知识,如果需要观察名字的变化,就要分别添加fullName、firstName、lastName
三次观察,非常麻烦。如果能够只观察 fullName
,并建立fullName
和firstName、lastName
的某种依赖关系,当发生变化时,也受到通知,那该多好啊!
可以参考如下文章:
KVO的权衡
- KVO的一个不足是不能对属性名进行编译检查。
- 只有一个回调方法,这意味着开发者经常会在那个地方高出一堆不相关的代码。
- 当你不是某个路径的观察者而调用
removeObserver
时会发生崩溃。
KVO&KVC相关问题
参考文章: