KVO(NSKeyValueObserving 键 值编码)与KVC(NSKeyValueCoding 键值观察机制)详解



KVO(NSKeyValueObserving:键 - 值编码的简称)与KVC(NSKeyValueCoding:键值观察机制的简称):

    在很多时候接触到很多地方都有对 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 和原生类型(intfloat 等)都是支持的:Objective-C 有一个自动装箱机制,可以将这些原生类型封装成 NSNumber 或者 NSValue 对象。因此,valueForKey: 返回值都是一个对象。如果需要向 setValue:forKey: 传入 nil,需要使用 setNilValueForKey:


高级特性


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


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

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



KVC--KVO优势:

1. 使用一对高度规范化的访问方法,获取以及设置任何对象的任何属性的值(所谓的属性既可以是个实实在在的成员变量,也可以是通过一对成员方法所抽象出的该对 象的一个性质)。
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 *)context
1.3KVO 的优点
 
   当有属性改变,KVO 会提供自动的消息通知。这样开发人员不需要自己去实现这样的方案:每次属性改变了就发送消息通知。

这是 KVO 机制提供的最大的优点。因为这个方案已经被明确定义,获得框架级支持,可以方便地采用

开发人员不需要添加任何代码,不需要设计自己的观察者模型,直接可以在工程里使用。

其次,KVO 的架构非常的强大,可以很容易的支持多个观察者观察同一个属性,以及相关的值



KVC

2.1  概述

KVC是KeyValue Coding的简称,它是一种可以直接通过字符串的名字(key)来访问类属性的机制。而不是通过调用Setter、Getter方法访问。

当使用KVO、Core Data、CocoaBindings、AppleScript(Mac支持)时,KVC是关键技术。


 

2.2  如何使用KVC

关键方法定义在:NSKeyValueCodingprotocol

KVC支持类对象和内建基本数据类型。

 

2.2.1         获取值

valueForKey:,传入NSString属性的名字。

valueForKeyPath:,传入NSString属性的路径,xx.xx形式。

valueForUndefinedKey它的默认实现是抛出异常,可以重写这个函数做错误处理。

2.2.2         修改值

setValue:forKey:

setValue:forKeyPath:

setValue:forUndefinedKey:

setNilValueForKey: 当对非类对象属性设置nil时,调用,默认抛出异常。

2.2.3         一对多关系成员的情况

mutableArrayValueForKey:有序一对多关系成员  NSArray

mutableSetValueForKey:无序一对多关系成员  NSSet


2.3  KVC的实现细节

搜索Setter、Getter方法

 这一部分比较重要,能让你了解到KVC调用之后,到底是怎样获取和设置类成员值的。

  KVC 的实现分析

KVC 运用了一个 isa-swizzling 技术。isa-swizzling 就是类型混合指针机制。

KVC 主要通过 isa-swizzling,来实现其内部查找定位的。isa 指针,就是 is a kind of 的意思,指向维护分发表的对象的类。该分发表实际上包含了指向实现类中的方法的指针和其它数据。

如下 KVC 的代码:

[person setValue:@"personName" forKey:@"name"];

就会被编译器处理成:

SEL sel = sel_get_uid("setValue:forKey:");
IMP method = objc_msg_lookup (person->isa,sel);
method(person,sel,@"personName",@"name");

其中:

SEL数据类型:它是编译器运行Objective-C里的方法的环境参数。

IMP数据类型:他其实就是一个编译器内部实现时候的函数指针。当Objective-C编译器去处理实现一个方法的时候,就会指向一个IMP对象,这个对象是C语言表述的类型。
 
KVC在调用方法setValue的时候

(1)首先根据方法名找到运行方法的时候所需要的环境参数。

(2)他会从自己isa指针结合环境参数,找到具体的方法实现的接口。

(3)再直接查找得来的具体的方法实现。

这样的话前面介绍的KVO实现就好理解了

当一个对象注册了一个观察者,被观察对象的isa指针被修改的时候,isa指针就会指向一个中间类,而不是真实的类。

所以isa指针其实不需要指向实例对象真实的类。所以我们的程序最好不要依赖于isa指针。在调用类的方法的时候,最好要明确对象实例的类名。

这样只有当我们调用KVC去访问key值的时候KVO才会起作用。所以肯定确定的是,KVO是基于KVC实现的

下面实例演示一下:

@implementation Person
@synthesize name,age;//属性name将被监视
-(void)changeName
{name=@"changeName directly";}
@end
@implementation PersonMonitor
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{ 
    if([keyPath isEqual:@"name"])
    { 
        NSLog(@"change happen,old:%@ new:%@",[change objectForKey:NSKeyValueChangeOldKey],[change objectForKey:NSKeyValueChangeNewKey]); 
    } 
}
@end

//测试代码
Person *p =[[Person alloc] init];//监视对象
PersonMonitor *pm= [[PersonMonitor alloc]init];
[p addObserver:pm forKeyPath:@"name" options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:nil];
//测试前的数据
NSLog(@"p.name is %@",p.name);
//通过setvalue 的方法,PersonMonitor的监视将被调用
[p setValue:@"name kvc" forKey:@"name"];
//查看设置后的值
NSLog(@"p name get by kvc is %@",[p valueForKey:@"name"]);
//效果和通过setValue 是一致的
p.name=@"name change by .name=";
//通过person自己的函数来更改 name
[p changeName]; 
//最后一次修改是直接修改,所以没法产生通知。

输出:

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的效率会稍低一点,所以只有当你非常需要它提供的可扩展性时才使用它



作者:
出处:http://www.cnblogs.com/ChenYilong/(点击RSS订阅)
本文版权归作者和博客园共有,欢迎转载,
但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

原文地址:https://www.cnblogs.com/ChenYilong/p/2808610.html