系统回顾一下kvc,在这里简单记录一下。
KVC全称是Key Value Coding,键值编码,定义在NSKeyValueCoding.h文件中,是一个非正式协议。KVC提供了一种间接访问其属性方法或成员变量的机制,可以通过字符串来访问对应的属性方法或成员变量。
在NSKeyValueCoding中提供了KVC通用的访问方法,分别是getter方法valueForKey:和setter方法setValue:forKey:,以及其衍生的keyPath方法,这两个方法各个类通用的。并且由KVC提供默认的实现,我们也可以自己重写对应的方法来改变实现。
基础操作
KVC主要对三种类型进行操作,基础数据类型及常量、对象类型、集合类型。
1 | @interface BankAccount : NSObject |
在使用KVC时,直接将属性名当做key,并设置value,即可对属性进行赋值。
1 | [myAccount setValue:@(100.0) forKey:@"currentBalance"]; |
1 | Dog *aDog = [[Dog alloc] init]; |
keyPath
键路径
除了对当前对象的属性进行赋值外,还可以对其更“深层”的对象进行赋值。例如对当前对象的address属性的street属性进行赋值。KVC进行多级访问时,直接类似于属性调用一样用点语法进行访问即可。
1 | [myAccount setValue:@"中关村大街" forKeyPath:@"address.street"]; |
1 | Person *aPerson = [[Person alloc] init]; |
通过keyPath对数组进行取值时,并且数组中存储的对象类型都相同,可以通过valueForKeyPath:方法指定取出数组中所有对象的某个字段。例如下面例子中,通过valueForKeyPath:将数组中所有对象的name属性值取出,并放入一个数组中返回。
1 | NSArray *names = [array valueForKeyPath:@"name"]; |
集合运算符,快速运算
KVC中的集合运算符有以下三类:
1、简单集合运算符:
- @avg、@sum、@max、@min、@count (只能用在集合对象中,对象属性必须为数字类型)
2、对象操作符:
- @unionOfObjects:返回指定属性的值的数组,不去重;
- @distinctUnionOfObjects:返回指定属性去重后的值的数组
3、数组 / 集体操作符:跟对象操作符很相似,只不过是在NSArray和NSSet所组成的集合中工作的。
- @unionOfArrays:返回一个数组,值由各个子数组的元素组成,不去重
- @distinctUnionOfArrays:返回一个数组,值由各个子数组的元素组成,去重
- @distinctUnionOfSets:和@distinctUnionOfArrays差不多, 只是它期望的是一个包含着NSSet对象的NSSet,并且会返回一个NSSet对象。因为集合不能有重复的值,所以只有distinct操作。
1 | #pragma mark - Dog |
使用KVC进行快速运算的键路径语法类似于 * dogs.@count * , * dogs.@sum.age * 等.
1 | Dog *dog1 = [[Dog alloc] init]; |
打印结果如下:
备注:@max和@min在进行判断时,都是通过调用compare:方法进行判断,所以可以通过重写该方法对判断过程进行控制。
多值操作
需要注意的是,虽然看到dictionary的字样,下面两个方法并不是字典的方法。
KVC还有更强大的功能,可以根据给定的一组key,获取到一组value,并且以字典的形式返回,获取到字典后可以通过key从字典中获取到value。
(NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;
同样,也可以通过KVC进行批量赋值。在对象调用setValuesForKeysWithDictionary:方法时,可以传入一个包含key、value的字典进去,KVC可以将所有数据按照属性名和字典的key进行匹配,并将value给User对象的属性赋值。(void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;
1 | // 根据设置的key, 来进行组合结果. |
实际应用
在项目中经常会遇到字典转模型的情况,如果在自定义的init方法里逐个赋值,这样每次数据发生改变还需要改赋值语句。然而通过KVC为我们提供的赋值API,可以对数据进行批量赋值。
赋值时会遇到一些问题,例如服务器会返回一个id字段,但是对于客户端来说id是系统保留字段,可以重写setValue:forUndefinedKey:方法并在内部处理id参数的赋值。
异常信息
当根据KVC搜索规则,没有搜索到对应的key或者keyPath,则会调用对应的异常方法。异常方法的默认实现,在异常发生时会抛出一个NSUndefinedKeyException的异常,并且应用程序Crash。
解决方法:重写两个方法
1 | -(id)valueForUndefinedKey:(NSString *)key{ |
还有一种是服务端返回的类型和你接受的不一样,也会造成crash,这时候需要重写如下代码
1 | - (void)setValue:(id)value forKey:(NSString *)key{ |
还有一种异常处理问题,当通过KVC给某个非对象的属性赋值为nil时,此时KVC会调用属性所属对象的setNilValueForKey:方法,并抛出NSInvalidArgumentException的异常,并使应用程序Crash。
我们可以通过重写下面方法,在发生这种异常时进行处理。例如给name赋值为nil的时候,就可以重写setNilValueForKey:方法并表示name是空的。
1 | - (void)setNilValueForKey:(NSString *)key { |
总结:使用KVC时,最好重写2个方法 和 一个处理 类型的方法;
当然了,在字典和model进行转换中,推荐使用MJExtension等这种第三方的,像上面这种处理基本在里面进行过了。
私有访问
根据上面的实现原理我们知道,KVC本质上是操作方法列表以及在内存中查找实例变量。我们可以利用这个特性访问类的私有变量,例如下面在.m中定义的私有成员变量和属性,都可以通过KVC的方式访问。
这个操作对readonly的属性,@protected的成员变量,都可以正常访问。如果不想让外界访问类的成员变量,则可以将accessInstanceVariablesDirectly属性赋值为NO。
比如修改placeholder
1 | UITextField *textField1 = [[UITextField alloc] init]; |
再比如可以自定义一个UITabbar对象,然后在内部创建自己想要的视图,并通过layoutSubviews方法在内部进行重新布局。然后通过KVC的方式,将UITabbarController的tabbar属性替换为自定义的类即可。
安全性检查
KVC存在一个问题在于,因为传入的key或keyPath是一个字符串,这样很容易写错或者属性自身修改后字符串忘记修改,这样会导致Crash。
可以利用iOS的反射机制来规避这个问题,通过@selector()获取到方法的SEL,然后通过NSStringFromSelector()将SEL反射为字符串。这样在@selector()中传入方法名的过程中,编译器会有合法性检查,如果方法不存在或未实现会报黄色警告。
1 | [self valueForKey:NSStringFromSelector(@selector(object))]; |
相关问题:
- 1.KVC的底层实现?
当一个对象调用setValue方法时,方法内部会做以下操作:
①检查是否存在相应key的set方法,如果存在,就调用set方法
②如果set方法不存在,就会查找与key相同名称并且带下划线的成员属性,如果有,则直接给成员属性赋值
③如果没有找到_key,就会查找相同名称的属性key,如果有就直接赋值
④如果还没找到,则调用valueForUndefinedKey:和setValue:forUndefinedKey:方法。
这些方法的默认实现都是抛出异常,我们可以根据需要重写它们。
- 2.KVC是什么?说说它的优缺点?
键值编码,KVC提供了一种间接访问其属性方法或成员变量的机制,可以通过字符串来访问对应的属性方法或成员变量。但由于KVC不会对键和键路径进行错误检查,所以编译器无法检测错误。而且使用KVC后的执行效率要低于合成存取器,因为使用KVC必须先解析字符串,然后再设置或服务对象的实例变量。
- 3.kvc的使用场景
①字典转模型
②当我们想替换系统的属性时候,比如系统的tabbar是只读属性不可改变,我们可以自定一个 tabbar去替换tabbar
- 4.KVC的keyPath中的集合运算符如何使用?(集合运算符那三类运算符来入手,即简单集合运算符,对象操作运算符,数组 / 集体操作符)