在很多时候接触到很多地方都有对 KVC,KVO 的描述,但是都是一笔带过。只知道这是Object-C提供的一个不错的机制,是能够让代码更简洁的特性。它们的目的截然不同:键值对编码可以通过选择第一个符合条件的实现而解决间接方法调用;属性则可以让编译器帮我们生成部分代码。键值对编码实际上是 Cocoa 引入的,而属性则是 Objective-C 2.0 语言新增加的。
键值对编码(KVC)
原则
键值对编码意思是,能够通过数据成员的名字来访问到它的值。这种语法很类似于关联数组(在 Cocoa 中就是 NSDictionary),数据成员的名字就是这里的键。NSObject 有一个 valueForKey: 和 setValue:forKey: 方法。如果数据成员就是对象自己,寻值过程就会向下深入下去,此时,这个键应该是一个路径,使用点号 . 分割,对应的方法是 valueForKeyPath: 和 setValue:forKeyPath:。
//Language: Objective-C @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:。
高级特性
有几点细节需要注意,尽管在这里并不会很详细地讨论这个问题:
keypath 可以包含计算值,例如求和、求平均、最大值、最小值等;使用 @ 标记;
注意方法一致性,例如 valueForKey: 或者 setValue:forKey: 以及关联数组集合中常见的 objectForKey: 和 setObject:forKey:。这里,同样使用 @ 进行区分。
KVC--KVO优势:
2. 通过继承一个特定的方法,并且指定希望监视的对象及希望监视的属性名称,就能在该对象的指定属性的值发生改变时,得到一个“通知”(尽管这不是一个真正意 义上的通知),并且得到相关属性的值的变化(原先的值和改变后的新值)。
3. 通过一个简单的函数调用,使一个视图对象的一个指定属性随时随地都和一个控制器对象或模型对象的一个指定属性保持同步。
KVO:
1.1 概述
KVO(NSKeyValueObserving:键 - 值编码的简称)
当指定的对象的属性被修改了,允许对象接收到通知的机制。
1.2 kvo实现原理每当在类中定义一个监听如:
[self addObserver:self forKeyPath:@"items" options:0 context:contexStr];
当然你还可以监听其他对象的属性变化,如:
[person addObserver:money forKeyPath:@"account" options:0 context:contexStr];
只要当前类中 items 这个属性发生的变化都会触发到以下的方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context1.3KVO 的优点:
当有属性改变,KVO 会提供自动的消息通知。这样开发人员不需要自己去实现这样的方案:每次属性改变了就发送消息通知。
这是 KVO 机制提供的最大的优点。因为这个方案已经被明确定义,获得框架级支持,可以方便地采用。
开发人员不需要添加任何代码,不需要设计自己的观察者模型,直接可以在工程里使用。
其次,KVO 的架构非常的强大,可以很容易的支持多个观察者观察同一个属性,以及相关的值。
输出:
p.name is name
change happen, old:name new:name kvc
p name get by kvc is name kvc
change happen, old:name kvc new:name change by .name=
最后一次修改是直接修改 所以没法产生通知
此实例的完整工程下载地址:"kvc_Demo.zip" 点击打开链接2.3.1 搜索简单的成员
如:基本类型成员,单个对象类型成员:NSInteger,NSString*成员。
a. setValue:forKey的搜索方式:
1. 首先搜索set<Key>:方法
如果成员用@property,@synthsize处理,因为@synthsize告诉编译器自动生成set<Key>:格式的setter方法,所以这种情况下会直接搜索到。
注意:这里的<Key>是指成员名,而且首字母大写。下同。
2. 上面的setter方法没有找到,如果类方法accessInstanceVariablesDirectly返回YES(注:这是NSKeyValueCodingCatogery中实现的类方法,默认实现为返回YES)。
那么按_<key>,_is<Key>,<key>,is<key>的顺序搜索成员名。
3. 如果找到设置成员的值,如果没有调用setValue:forUndefinedKey:。
b. valueForKey:的搜索方式:
1. 首先按get<Key>、<key>、is<Key>的顺序查找getter方法,找到直接调用。如果是bool、int等内建值类型,会做NSNumber的转换。
2. 上面的getter没有找到,查找countOf<Key>、objectIn<Key>AtIndex:、<Key>AtIndexes格式的方法。
如果countOf<Key>和另外两个方法中的一个找到,那么就会返回一个可以响应NSArray所有方法的代理集合(collection proxy object)。发送给这个代理集合(collection proxy object)的NSArray消息方法,就会以countOf<Key>、objectIn<Key>AtIndex:、<Key>AtIndexes这几个方法组合的形式调用。还有一个可选的get<Key>:range:方法。
3. 还没查到,那么查找countOf<Key>、enumeratorOf<Key>、memberOf<Key>:格式的方法。
如果这三个方法都找到,那么就返回一个可以响应NSSet所有方法的代理集合(collection proxy object)。发送给这个代理集合(collection proxy object)的NSSet消息方法,就会以countOf<Key>、enumeratorOf<Key>、memberOf<Key>:组合的形式调用。
4. 还是没查到,那么如果类方法accessInstanceVariablesDirectly返回YES,那么按_<key>,_is<Key>,<key>,is<key>的顺序直接搜索成员名。
5. 再没查到,调用valueForUndefinedKey:。
2.3.2 查找有序集合成员,比如NSMutableArray
mutableArrayValueForKey:搜索方式如下:
1. 搜索insertObject:in<Key>AtIndex:、removeObjectFrom<Key>AtIndex:或者insert<Key>:atIndexes、remove<Key>AtIndexes:格式的方法。
如果至少一个insert方法和至少一个remove方法找到,那么同样返回一个可以响应NSMutableArray所有方法的代理集合。那么发送给这个代理集合的NSMutableArray消息方法,以insertObject:in<Key>AtIndex:、removeObjectFrom<Key>AtIndex:、insert<Key>:atIndexes、remove<Key>AtIndexes:组合的形式调用。还有两个可选实现的接口:replaceObjectIn<Key>AtIndex:withObject:、replace<Key>AtIndexes:with<Key>:。
2. 否则,搜索set<Key>:格式的方法,如果找到,那么发送给代理集合的NSMutableArray最终都会调用set<Key>:方法。
也就是说,mutableArrayValueForKey取出的代理集合修改后,用set<Key>:重新赋值回去。这样做效率会差很多,所以推荐实现上面的方法。
3. 否则,那么如果类方法accessInstanceVariablesDirectly返回YES,那么按_<key>,<key>的顺序直接搜索成员名。如果找到,那么发送的NSMutableArray消息方法直接转交给这个成员处理。
4. 再找不到,调用setValue:forUndefinedKey:。
2.3.3 搜索无序集合成员,如:NSSet。
mutableSetValueForKey:搜索方式如下:
1. 搜索add<Key>Object:、remove<Key>Object:或者add<Key>:、remove<Key>:格式的方法,如果至少一个insert方法和至少一个remove方法找到,那么返回一个可以响应NSMutableSet所有方法的代理集合。那么发送给这个代理集合的NSMutableSet消息方法,以add<Key>Object:、remove<Key>Object:、add<Key>:、remove<Key>:组合的形式调用。还有两个可选实现的接口:intersect<Key>、set<Key>:。
2. 如果reciever是ManagedObejct,那么就不会继续搜索了。
3. 否则,搜索set<Key>:格式的方法,如果找到,那么发送给代理集合的NSMutableSet最终都会调用set<Key>:方法。也就是说,mutableSetValueForKey取出的代理集合修改后,用set<Key>:重新赋值回去。这样做效率会差很多,所以推荐实现上面的方法。
4. 否则,那么如果类方法accessInstanceVariablesDirectly返回YES,那么按_<key>,<key>的顺序直接搜索成员名。如果找到,那么发送的NSMutableSet消息方法直接转交给这个成员处理。
5. 再找不到,调用setValue:forUndefinedKey:。
KVC还提供了下面的功能
2.4 值的正确性核查
KVC提供属性值确认的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
实现核查方法
为如下格式:validate<Key>:error:
如:
-(BOOL)validateName:(id *)ioValue error:(NSError **)outError { // The name must not be nil, and must be at least two characters long. if ((*ioValue == nil) || ([(NSString *)*ioValue length] < 2)) { if (outError != NULL) { NSString *errorString = NSLocalizedStringFromTable( @"A Person's name must be at least two characters long", @"Person", @"validation: too short name error"); NSDictionary *userInfoDict = [NSDictionary dictionaryWithObject:errorString forKey:NSLocalizedDescriptionKey]; *outError = [[[NSError alloc] initWithDomain:PERSON_ERROR_DOMAIN code:PERSON_INVALID_NAME_CODE userInfo:userInfoDict] autorelease]; } return NO; } return YES; }
调用核查方法:
validateValue:forKey:error:,默认实现会搜索 validate<Key>:error:格式的核查方法,找到则调用,未找到默认返回YES。
注意其中的内存管理问题。
2.5 集合操作
集合操作通过对valueForKeyPath:传递参数来使用,一定要用在集合(如:array)上,否则产生运行时刻错误。其格式如下:
Left keypath部分:需要操作对象路径。
Collectionoperator部分:通过@符号确定使用的集合操作。
Rightkey path部分:需要进行集合操作的属性。
2.5.1 数据操作
@avg:平均值
@count:总数
@max:最大
@min:最小
@sum:总数
确保操作的属性为数字类型,否则运行时刻错误。
2.5.2 对象操作
针对数组的情况
@distinctUnionOfObjects:返回指定属性去重后的值的数组
@unionOfObjects:返回指定属性的值的数组,不去重
属性的值不能为空,否则产生异常。
2.5.3 数组操作
针对数组的数组情况
@distinctUnionOfArrays:返回指定属性去重后的值的数组
@unionOfArrays:返回指定属性的值的数组,不去重
@distinctUnionOfSets:同上,只是返回值为NSSet
2.6 效率问题
相比直接访问KVC的效率会稍低一点,所以只有当你非常需要它提供的可扩展性时才使用它。