概念
先来看看概念,Key-value coding (KVC) 和 key-value observing (KVO) 是两种能让我们驾驭 Objective-C 动态特性并简化代码的机制,其中KVO是基于KVC来实现的,本文先学习KVC的使用。
KVC
首先,先从KVC说起,KVC是OC对象的一个特性,即允许我们用属性的字符串值来访问属性,而字符串作为key(键值),这个key值必须与属性名称相同,我们可以利用这个特性来获取和设置属性的值,首先看看系统提供的常用的api接口,主要是以下4个:
- (id)valueForKey:(NSString *)key;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
简单用法就是:
NSString *str = [object valueForKey:@"name”]
[objectsetValue:@"Daniel"forKey:@"name”]
当然这是KVC最简单的使用方法,而KVC同样也支持通过关系(一般称为键路径keyPath)来访问属性值,假设一个对象Person,Penson有个属性是address,address有个属性是city,这时候我要通过Person访问到city就可以使用键路径来访问,可以使用点号连接的多层级的属性,比如address.city,address属性对象里的city属性。代码如下:
[Person valueForKeyPath:@“address.city”];
KVC原理窥探
可以发现,KVC和property的存取方法作用是相似的,二者都可以当作属性的存取操作来使用,实际上KVC在存取的时候也是先通过查找property中是否有对应的存取方法从而来访问属性的过程,这个过程中如果没有查询到property对应的存取方法时则会尝试直接访问实例变量,如果实例变量也不存在的话,则程序就会出现crash,而其实系统也给了我们一次挽救的机会,我们可以通过重写对象的这两个方法来使KVC当访问不到对应的属性时作出防崩溃的处理:
setValue:forUndefinedKey:此方法当KVC设置属性值的时候找不到该属性的时候调用
valueForUndefinedKey:此方法当KVC获取属性值的时候找不到属性的时候调用
KVC异常情况处理
KVC中最常见的异常就是不小心使用了错误的key,或者在设值中不小心传递了nil的值,KVC中有专门的方法来处理这些异常,如果你不小心传递了错误的或者nil的值进去,KVC会调用setNilValueForKey:方法。这个方法默认是抛出异常,所以可以重写这个方法:
-(void)setNilValueForKey:(NSString *)key{
NSLog(@“错误:key值为空);
}
KVC访问和修改私有变量
对于类里的私有属性,Objective-C是无法直接访问的,但是KVC是可以的,比如我有一个对象person,person类里面有一个属性是没有暴露给外部的,这时候就可以使用KVC来访问这个属性,而OC本身是不直接支持访问私有变量的,代码如下:
@interface Person :NSObject
@end
@implementation Person {
NSString*_privateStr;
}
- (id)init {
if(self) {
_privateStr = @"我是私有变量";
}
return self;
}
现在在别的类里面就可以用KVC来访问这个私有变量:
Person *person = [Person new];
NSString*privateString = [objvalueForKey:@"privateStr"];
NSLog(@"获取到私有变量的值%@", privateString);
KVC与容器类
对于不可变的有序容器属性(NSArray)和无序容器属性(NSSet)一般可以使用valueForKey:来获取。也就说不可变的容器是可以直接支持KVC的,而对于可变类的容器,比如NSMutableArray,可以使用下面这个方法:
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
这个方法可以得到一个NSMutableArray的代理数组来执行相应的数组添加删除操作,且可以被KVC鉴定搜索到变化过程,具体搜索过程有时间会再作总结,这里暂不说明。
关于mutableArrayValueForKey:的使用,如果对象属性是个NSMutableArray、NSMutableSet、NSMutableDictionary等集合类型时,你给它添加KVO时,你会发现当你添加或者移除元素时并不能接收到变化。因为KVO的本质是系统监测到某个属性的内存地址或常量改变时,而可变数组内的元素更改并不会修改到变量本身的内存地址,所以有个做法就是添加上- (void)willChangeValueForKey:(NSString *)key和- (void)didChangeValueForKey:(NSString *)key方法来手动发送KVO通知,这种方法比较复杂,需要开发者自行掌握通知发送时机。另一种便是利用使用mutableArrayValueForKey:了。
简单举个例子就是,现在有一个NSMutableArray的属性是arr,一般情况下我们调用[_arr addObject:@"value"]时,Observer并不会回调,只有[[self mutableArrayValueForKey:@"arr"] addObject:@"value"];这样写时才能正确地触发KVO。
对于无序容器也有对应的方法:
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
同样的,keyPath版本的也可以使用如下方法:
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;
KVC与字典
当对NSDictionary对象使用KVC时,valueForKey:的表现行为和objectForKey:是一样的,说下另外两个方法的使用:
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;
dictionaryWithValuesForKeys方法的作用是指传入一个数组,数组里包含一组key值,key值是属性的名称,用这个key值得到属性对于的值,最后再组成字典返回。
-(void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;
而setValuesForKeysWithDictionary是用来修改属性对应的值。以下是简单的一个例子:
Person *person = [Person new];
person.address = @“地址”;
person.phone = “110”;
NSArray *arr = [@“address”, @“phone”];
NSDictionary* dict = [persondictionaryWithValuesForKeys:arr]; //把对应key所有的属性全部取出来
NSLog(@"%@",dict);
NSDictionary* modifyDict = @{@“address":@“修改的地址",@"phone":@“修改的电话号码”};
[phone setValuesForKeysWithDictionary:modifyDict]; //用key Value来修改Model的属性
KVC集合操作
KVC 的集合操作符可使用键路径和操作运算作用于集合中的所有元素,简单来说集合操作就是使用一些特殊的键路径来对集合进行相应的操作,再简单点说就是集合操作符就是一个特殊的key Path,它可以作为参数传递给valueForKeyPath:方法,集合操作是以@开始的字符串,简单集合操作符作用于 array 或者 set 中相对于集合操作符右侧的属性。包括@avg,@count,@max,@min,@sum.
除了@count外的所有集合操作,都要求在集合操作右边有一键路径(keypathToProperty),也就是要操作集合中的哪个元素,示例:求一堆书本arrBooks的总价是多少的时候可以这么操作,其中arrBooks中的book对象中含有price的属性:
NSNumber* sum = [arrBooks valueForKeyPath:@"@sum.price”];
集合操作返回的对象值决定于集合操作的类型:
简单类型的集合操作符,返回 strings, numbers, dates, 其值由右侧的键路径决定(见简单集合操作符)
对象操作符,以下会介绍,返回 NSArray 对象实例(见对象操作符)
数组和集合操作符,返回的是一个 array 或者 set 对象
KVC对象操作符
对象操作符包括@distinctUnionOfObjects和@unionOfObjects, 返回一个由操作符右边的keyPath所指定的对象属性组成的数组。其中@distinctUnionOfObjects会对数组去重,而@unionOfObjects不会,用法:
NSArray *unionOfObjects = [obj valueForKeyPath:@"@unionOfObjects.name"];
NSArray *distinctUnionObjects = [obj valueForKeyPath:@"@distinctUnionOfObjects.name"];
比如一个数组中有3个Person对象,而Person中有对应的address属性,用KVC打印出address属性:
Person *person1 = [Person new];
person1.address=@"地址1";
Person *person2 = [Person new];
person2.address=@"地址2";
Person *person3 = [Person new];
person3.address=@"地址1";
NSArray*persons = [NSArray arrayWithObjects:person1, person2, person3,nil];
NSLog(@"distinctUnionOfObjects");
NSArray* distinctUnion = [persons valueForKeyPath:@"@distinctUnionOfObjects.address”];
//打印出不重复的地址
for (NSString *address in distinctUnion) {
NSLog(@"%@", address);
}
NSLog(@"unionOfObjects”);
NSArray* arrUnion = [persons valueForKeyPath:@"@unionOfObjects.price"];
//可以打印出重复的地址
for (NSString *address in arrUnion) {
NSLog(@"%@", address);
}
运行结果
地址1
地址2
地址1
地址2
地址3
KV0
键值观察
我们已经掌握了KVC,现在开始学习KVO。以下是实现KVO的步骤:
使用addObserver: forKeyPath: options: context:方法注册为观察者,用于观察其他类的属性。
观察者必须实现observerValueForKeyPath: ofObject: change: context:方法以接收属性变化通知。
使用removeObserver: forKeyPath: context:方法移除观察者。
更多KVO的总结下篇继续...