KVC: Key-value coding is a mechanism for indirectly accessing anobject’s attributes and relationships using string identifiers.
所谓键值编码,并不是访问器方法的启动和实例变量的访问这种直接的方式,而是使用表示属性的字符串来间接访问对象属性值的一种结构。
只要存在访问器方法、声明属性或实例变量,就可以将其名字指定为字符串来访问。
之所以说键值编码的访问是接的:
1. 可以在运行中确定作为键的字符串
2. 使用者无法知道实际访问属性的方法
键值编码必需的方法在非正式协议NSKeyValueCoding中声明(头文件Foundation/NSKeyValueCoding.h)。默认在NSObject中实现。
下面就以下两个方法的调用进行说明:
- (id) valueForKey: (NSString *) key
返回表示属性的键字符串所对应的值。如果不能取得值,则将引起接收器调用方法valueForUndefinedKey:。
- (void)setValue: (id) value forKey: (NSString*) key
将键字符串key所对应的属性的值设置为value。不能设定属性时,将引起接收器调用方法setValue:ForUndefinedKey:。
执行时,有访问器的属性会使用访问器,没有访问器的属性也可以设定值和访问。因为上面两个方法均为实例方法,可以在方法体内访问实例变量。
访问过程如下:
1. 接收器中如果有key访问器(或getKey、isKey、_key、_getKey、setKey)则使用它。
2. 没有访问器时,使用接收器的类方法accessInstanceVariablesDirectly来查询。返回YES时,如果存在实例变量key(或_key、isKey、_isKey等)则返回或设置其值。使用引用计数管理方式时,实例变量如果为对象,则旧值会被自动释放,新值被保存并代入。
+ (BOOL)accessInstanceVariablesDirectly
通常定义为返回YES,可以在子类中改变。该类方法返回YES时,使用键值编码可以访问该类的实例变量。返回NO时不可以访问。只要该方法返回YES,实例变量的可视属性即使有@private修饰,也可以访问。
3. 既没有访问器也没有实例变量时,将引起接收器调用方法valueForUndefinedKey:或setValue:forUndefinedKey:。
- (id) valueForUndefinedKey: (NSStirng *) key
不能取得键字符串对应的值时,从方法valueForKey:中调用该方法。默认情况下,该方法的执行会触发NSUndefinedKeyException。不过,通过在子类中修改定义,就可以返回其他对象。
- (void) setValue: (id) value forUndefinedKey: (NSString *) key
不能设置键字符串key对应的属性值时,从方法setValue:forKey中调用该方法。默认情况下,该方法的执行会触发异常NSUndefinedKeyException。不过,通过在子类中修改定义,可以返回其他对象。
4. 如果该返回值不是对象,则返回被适当的对象包装的值;设置值时也应先包装成相应的对象。
属性为对象时,该对象还可能持有属性。这时候可以用“.”连接表示键的字符串,这种表示方式称为键路径。只要能找到对象,点和键多长都没有关系。
- (id) valueForKeyPath:(NSString *) keyPath
以点切分键路径,并使用第一个键向接收器发送valueForKey:方法。然后,再使用键路径的下一个键,向得到的对象发送valueForKey:方法,如此反复操作,返回最后获得的对象。
- (void)setValue: (id) value forKeyPath:(NSString *) keyPath
与valueForKeyPath:方法一样取出对象,这里只对路径中的最后一个键调用setValue:forKey:方法,并设定属性值为value。
KVO:key-value observing,是在KVC基础上实现的,当某个对象的属性发生改变时,通知其它对象的机制。仅仅在以KVC准则来访问访问器或实例变量的情况下,才可以监视属性的变化。在方法内直接改变实例变量的值时,就不能监视了。
具体KVC准则有一下三点:
1. 随访问器方法而改变。
2. 使用setValue:forKey:和键进行改变。此时也可能不经由访问器。
3. 使用setValue:forKeyPath:和键路径进行改变。此时也可能不经由访问器。不仅仅是最终的监视对象的属性,当路径中的属性发生变化时,也会被通知。
KVO中的常用方法如下:
注册键值观察的方法:
- (void) addObserver: (NSObject *)anObserver forKeyPath (NSString *)keyPath
options: (NSKeyValueObservingOptions)options
context: (void *) context
从接收器的角度来看,监视键路径keyPath中的某个属性,要在接收器中注册。观察者为对象anObserver 。属性变化时发送的通知消息中,包含着显示变化内容的字典数据、参数context中指定的任意指针(或对象)。options中指定字典数据中包含什么样的值。值可取下面的常数或它们的异或运算。
NSKeyValueObservingOptionNew ---- >提供属性改变后的值
NSKeyValueObservingOptionOld ---->提供属性改变前的值
移除已注册的观察:
- (void) removeObserver: (NSObject *) anObserver
forKeyPath: (NSString *) keyPath
移除观察者anObserver对于某个路径keyPath的观察。
观察者需要实现接受通知的方法:
- (void) observerValueForKeyPath: (NSString *)keyPath ofObject: (id) object
Change: (NSDictionary *) change context: (void *) context
从object的角度来看,当键路径keyPath的属性发生变化时会发送通知。字典change中保存着改变的相关信息。参数context中返回注册观察者时指定的值。
下面通过实验的方式来探索KVO的实现机制:
其实KVO是通过isa-swizzling技术实现的,主要的操作如下:
1.当为某个对象添加观察者的时候,该对象的类将被继承生成一个中间类,并使该对象的isa指针指向中间类(所以,有时候发送消息需要明确指定类型)。注意:同一个类的其它实例对象并不受影响。
2.中间类在被观察的属性的setter方法中,在改变属性值的前后分别添加了willChangeValueForKey:和didChangeValueForKey:。使其在通过KVC标准改变属性值时可以被观察到,并向观察者发送消息。
3.当移除对某个对象的所有观察后,该对象的isa指针会重新指向原有的类。
做如下验证:
可以得到结果:
说明person对象的isa指向的类对象的确在改变。
官方文档中的说明是willChangeValueForKey:和didChangeValueForKey:这两个方法必需成对调用,其实完全可以分别调用(当然你要明白分开调用意味着什么),甚至调用时两个方法的key都可以不同。例如:
[selfwillChangeValueForKey:@"age"];
[selfdidChangeValueForKey:@"name"];
经过我的“黑盒测试”,我的结论是:willChangeValueForKey:方法用于设置将要发送通知内容中与值改变之前相关的内容。如果不调用该方法,则前后两次接收到的消息内容中old值将会相同。didChangeValueForKey:方法中主要的工作是查看key值是否被观察,如果被观察则设置新值、发送通知消息;否则将不发送消息。
例如,实现的响应函数如下
有一个手动实现KVO的函数如下
则有结果:
若change做如下改变:
则有结果:
注释掉willChangeValueForKey:方法后old值将不再更新。
有一点需要注意的是,以下的调用方式会引起死循环:
扩展:依赖登记
有时候需要某属性值随着同一对象的其他属性的改变而改变。可以通过事先将这样的依赖关系在类中注册,那么即便属性值间接地发生了改变,也会发送通知消息。
需实现方法如下:
- (void)setKeys: (NSArray*) keys
triggerChangeNotificationsForDependentKey:(NSString *) dependentKey
数组keys中可以保存多个键。注册依赖关系,使当这些键中任意一个键对应的属性发生改变时,都会自动引起与键dependentKey的属性变化时一样的行为(被监视时发送通知)。
这种依赖登记的方式感觉有时候也是非常有用的呢(*^-^*)