首页 > 技术文章 > KVO和KVC

lkjson 2014-11-17 16:41 原文


本章中心是两个能够让代码更简洁的特性。

它们的目的截然不同:键值对编码可以通过选择第一个符合条件的实现而解决间接方法调用;键值对编码实际上是 Cocoa 引入的。

属性则可以让编译器帮我们生成部分代码,属性则是 Objective-C 2.0 语言新增加的。


KVC

键值对编码

原则

键值对编码意思是,能够通过数据成员的名字来访问到它的值。这种语法很类似于关联数组(在 Cocoa 中就是NSDictionary),

数据成员的名字就是这里的键。

NSObject 有一个 valueForKey: setValue:forKey: 方法。如果数据成员就是对象自己,寻值过程就会向下深入下去,此时,这个键应该是一个路径,使用点号 . 分割,对应的方法是 valueForKeyPath: 和 setValue:forKeyPath:。

@interface A {

    NSString* foo;

}

... // 其它代码

@end

@interface B {

    NSString* bar;

    A* myA;

}

... // 其它代码

@end

@implementation B

...

// 假设 A 类型的对象 a,B 类型的对象 b

A* a = ...;

B* b = ...;

NSString* s1 = [a valueForKey:@"foo"]; // 正确

NSString* s2 = [b valueForKey:@"bar"]; // 正确

NSString* s3 = [b valueForKey:@"myA"]; // 正确

NSString* s4 = [b valueForKeyPath:@"myA.foo"]; // 正确

NSString* s5 = [b valueForKey:@"myA.foo"]; // 错误

NSString* s6 = [b valueForKeyPath:@"bar"]; // 正确

...

@end

这种语法能够让我们对不同的类使用相同的代码来处理同名数据。注意,这里的数据成员的名字都是使用的字符串的形式。这种使用方法的最好的用处在于将数据(名字)绑定到一些触发器(尤其是方法调用)上,例如键值对观察(Key-Value Observing, KVO)等。


拦截

通过 valueForKey: 或者 setValue:forKey: 访问数据不是原子操作。这个操作本质上还是一个方法调用。事实上,这种访问当某些方式实现的情况下才是可用的,例如使用属性自动添加的代码等等,或者显式允许直接访问数据。

Apple 的文档对 valueForKey: 和 setValue:forKey: 的使用有清晰的文档:

对于 valueForKey:@”foo” 的调用:

·        如果有方法名为 getFoo,则调用 getFoo;

·        否则,如果有方法名为 foo,则调用 foo(这是对常见的情况);

·        否则,如果有方法名为 isFoo,则调用 isFoo(主要是布尔值的时候);

·        否则,如果类的 accessInstanceVariablesDirectly 方法返回 YES,则尝试访问 _foo 数据成员(如果有的话),否则寻找 _isFoo,然后是 foo,然后是 isFoo;

·        如果前一个步骤成功,则返回对应的值;

·        如果失败,则调用 valueForUndefinedKey:,这个方法的默认实现是抛出一个异常。

对于 forKey:@”foo” 的调用:

·        如果有方法名为 setFoo:,则调用 setFoo:;

·        否则,如果类的 accessInstanceVariablesDirectly 返回 YES,则尝试直接写入数据成员 _foo(如果存在的话),否则寻找 _isFoo,然后是 foo,然后是 isFoo;

·        如果失败,则调用 setValue:forUndefinedKey:,其默认实现是抛出一个异常。

注意 valueForKey: 和 setValue:forKey: 的调用可以用于触发任何相关方法。如果没有这个名字的数据成员,则就是一个虚假的调用。例如,在字符串变量上调用 valueForKey:@”length” 等价于直接调用 length 方法,因为这是 KVC 能够找到的第一个匹配。但是,KVC 的性能不如直接调用方法,所以应当尽量避免


原型

使用 KVC 有一定的方法原型的要求:getters 不能有参数,并且要返回一个对象;setters 需要有一个对象作为参数,不能有返回值。参数的类型不是很重要的,因为你可以使用 id 作为参数类型。注意,struct 和原生类型(int,float 等)都是支持的:Objective-C 有一个自动装箱机制,可以将这些原生类型封装成 NSNumber 或者 NSValue 对象。因此,valueForKey: 返回值都是一个对象。如果需要向 setValue:forKey: 传入 nil,需要使用 setNilValueForKey:。

高级特性

有几点细节需要注意,尽管在这里并不会很详细地讨论这个问题:

1.    keypath 可以包含计算值,例如求和、求平均、最大值、最小值等;使用 @ 标记;

2.    注意方法一致性,例如 valueForKey: 或者 setValue:forKey: 以及关联数组集合中常见的 objectForKey: 和 setObject:forKey:。这里,同样使用 @ 进行区分。


KVO

KVO是Cocoa的一个重要机制,他提供了观察某一属性变化的方法,极大的简化了代码。这种观察-被观察模型适用于这样的情况,比方说根据A(数据类)的某个属性值变化,B(view类)中的某个属性做出相应变化。对于推崇MVC的cocoa而言,KVO应用的地方非常广泛。(这样的机制听起来类似Notification,但是notification是需要一个发送notification的对象,一般是 notificationCenter,来通知观察者。而KVO是直接通知到观察对象。)

适用KVO时,通常遵循如下流程:

1 注册:

-(void)addObserver:(NSObject *)anObserver forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void*)context

keyPath就是要观察的属性值,options给你观察键值变化的选择,而context方便传输你需要的数据(注意这是一个void型)

2 实现变化方法:

-(void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object

change:(NSDictionary *)change context:(void*)context

change里存储了一些变化的数据,比如变化前的数据,变化后的数据;如果注册时context不为空,这里context就能接收到。

是不是很简单?KVO的逻辑非常清晰,实现步骤简单。

说了这么多,大家都要跃跃欲试了吧。可是,在此之前,我们还需要了解KVC机制。其实,知道了KVO的逻辑只是帮助你理解而已,要真正掌握的,不在于KVO的实现步骤是什么,而在于KVC,因为只有符合KVC标准的对象才能使用KVO(强烈推荐要使用KVO的人先理解KVC)。

推荐阅读