本文由快学吧个人写作,以任何形式转载请表明原文出处。
KVC非常重要,只要还研究对象这个东西,就都避不开KVC的理念。
一、准备资料
二、KVC简介
全称 : key value coding,中文叫键值编码。
特性和主要功能 :
(1). 无论成员变量是公有还是私有的,KVC都可以通过变量名称(字符串类型)直接访问成员变量。
(2). 践行了OC的动态性,KVC无需调用明确的存取方法,可以动态的访问和修改对象的属性。
KVC是要依赖runtime的。
KVC本身是一种机制,它是根据NSKeyValueCoding的非正式协议使用的,NSKeyValueCoding是NSObject的分类。
KVC提供了更简单,更统一的消息接口。
适用范围
(1). 所有继承了NSObject的类都可以使用KVC,因为KVC的实现在NSObject的分类NSKeyValueCoding里面。
(2). NSArray、NSDictionary、NSMutableDictionary、NSOrderedSet、NSSet等也遵循KVC协议,所以也可以使用KVC。
三、KVC常见API
KVC设值
// 通过key设置value
- (void)setValue:(nullable id)value forKey:(NSString *)key;
// 通过keypath设置value
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
KVC取值
// 通过key取value
- (nullable id)valueForKey:(NSString *)key;
//通过keypath取value
- (nullable id)valueForKeyPath:(NSString *)keyPath;
其他常用
// 默认返回YES,意为如果没有找到Set<Key>方法的话(Key指的是变量名称),会以_key,_iskey,key,iskey的顺序搜索实例变量,设置成NO就不访问实例变量了
+ (BOOL)accessInstanceVariablesDirectly;
// 检查set的值是否正确,不正确可以替换一下,或者拿到错误的原因
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
// 如果类的属性中有NSMutableArray类型的,可以通过key来获取这个类的属性
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
// key不存在,且KVC无法找到任何和key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
- (nullable id)valueForUndefinedKey:(NSString *)key;
// key不存在,想要给这个不存在的key设置value。默认抛出异常。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
// 给一个key设置的value是nil时,在这里可以拦截。
- (void)setNilValueForKey:(NSString *)key;
// 将一数组的key都找到对应value,然后以字典的结构进行存储。可以用来模型转字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
四、KVC的部分原理
KVC属于Foundation框架,没有开源,所以没源码看,所以只能通过上面准备的KVC官方文档的介绍来分析。或者可以反汇编,查看伪代码来梳理逻辑。
只看最基础的setValue:Forkey:和valueForKey:。
另外,探索的基础是以实例变量为主要探索对象,因为属性的setter和getter方法都是系统给我们生成的。而KVC官方文档中说明了逻辑,set方法和成员变量都是对kvc的影响因素
1. setValue:ForKey:
注 :<key>代表的是实例变量的名称。下述皆为官方文档翻译。
第1步
寻找是否有set<Key>方法或者_set<Key>方法,如果能找到,直接利用这个方法进行赋值。其实还有一个方法也会被查找 : setIs<Key>。准确的说是先查找变量是否有这三个方法对其进行赋值,如果有,则直接调用方法,调用的顺序是:set<Key>--->_set<Key>--->setIs<Key>。前面的调用了,后面的就不会调用了,比如找到了set<Key>,就直接调用了,不会再找后面的_set<Key>和setIs<Key>了。
对第1步举例 :
随意创建个项目,再创建一个JDMan的类继承于NSObject,代码如下 :
JDMan :
VC中 :
逐步运行 :
- JDMan的三个set方法都不注释
- 注释掉setJdName。
- 继续注释掉_setJdName。
三个运行结果如下:
(1). 不注释 :
kvc调用的是setJdName。
(2). 注释掉setJdName。
kvc调用的是_setJdName。
(3). 注释掉setJdName和_setJdName
kvc调用的是setIsJdName。
第2步
如果上述的三个方法都没找到,就是都没实现,并且类方法
accessInstanceVariablesDirectly返回的是YES(默认就是YES)。直接找实例变量进行赋值,顺序为 : _<Key>--->_is<Key> ---><Key>--->is<Key>。如果
accessInstanceVariablesDirectly返回的是NO,则进入第3步。如果这里的实例变量也都没有,则也进入第3步。
对第2点举例 :
JDMan代码 :
注释掉上述的3个set方法,让逻辑走到第2点中来。并且保证accessInstanceVariablesDirectly的返回值是YES。另外,需要在外部访问JDMan的实例变量,所以实例变量前要加@public。
VC代码 :
key是jdName。
运行结果 :
逐步运行 :
(1). 注释掉_jdName。把vc里面的NSLog里的_jdName也要删除了。
(2). 继续注释掉_isJdName。
(3). 继续注释掉jdName。
逐步运行结果 :
第3步
上述的两步中的set方法和实例变量都找不到。就会执行
setValue:forUndefinedKey:抛出异常,异常默认是NSUndefinedKeyException。但是NSObject的子类可以重写这个方法,避免崩溃。
对第3点举例 :
注释掉JDMan中所有自己写的东西。然后重写一下setValue:forUndefinedKey:如下 :
VC中 :
运行结果 :
正是我重写的setValue:forUndefinedKey:。
2. valueForKey
注 :<key>代表的是实例变量的名称。下述皆为官方文档翻译。
第1步
查找变量的getter方法。方法查找顺序 : get<Key>---><Key>--->is<Key>--->_<Key>。 找得到就跳到步骤5,找不到继续进行步骤2.
第2步
第2步是针对NSArray类型的实例变量进行的操作。
如果,用kvc取的是数组的值。在第1步中没有找到4种get方法,KVC会去找对象中是否有
countOf<Key>、objectIn<Key>AtIndex:、<key>AtIndexes:这样的方法实现。如果,对象中有
countOf<Key>,并且,objectIn<Key>AtIndex:、<key>AtIndexes:两个中至少有一个实现了。则会创建一个响应所有NSArray方法的代理集合对象,并将这个集合对象返回。如果不是NSArray,或者上述的条件不满足,则进入第3步。
第3步
第3步是针对NSSet类型的实例变量进行的操作。
如果,用kvc取的是集合的值。在第1步中没有找到4种get方法,KVC会去找对象中是否有
countOf<Key>,enumeratorOf<Key>,memberOf<Key>:这样的三个方法的实现。如果3个方法都实现了,创建一个响应所有NSSet方法的代理集合对象,并将这个集合对象返回。如果有任何1个没有实现。进入到第4步。
第4步
accessInstanceVariablesDirectly的返回值是否还是YES。如果是YES,则按照_<Key>--->_<isKey>---><Key>---><isKey>的顺序,去查找对应格式成员变量。如果找到了成员变量,则进入第5步。如果
accessInstanceVariablesDirectly返回值是NO。则进入第6步。
第5步
根据取到的值的类型,返回不一样的结果 :
(1). 如果取到的值是对象指针,也就是对象,则直接返回结果。
(2). 如果取到的值可以转换成NSNumber,则转换成NSNumber类型,并在NSNumber对象中存储,然后返回转换后的结果。
(3). 如果取到的值既不是对象指针,也不能转换成NSNumber类型,则将结果转换成NSValue类型,然后返回。
第6步
上述5步都没能返回符合规定的值,则调用
valueForUndefinedKey抛出NSUndefinedKeyException类型的默认异常。如果不想直接崩溃,可以在对象对应的子类中重写
valueForUndefinedKey。
valueForKey举例 :
- 针对第1步 :
JDMan代码 :
VC代码 :
运行结果 :
并没有直接取我给jdName设置的值,而是取了getJdName方法的返回值。验证了valueForKey的第1步。后续的逐步注释和上面的setValue:ForKey:一样,篇幅有限,不再多举例。
- 针对第2步 :
JDMan代码 :
VC代码 :
运行结果 :
所以数组也是要先走第一步的。如果没有第一步的getter方法,就会进入第二步。
去掉get<Key>,也就是注释掉getJdArr,就是进入第二步了 :
运行结果 :
后面步骤就不举例了,和setValue:forKey:是一样的。
五、总结
- KVC本身是一种机制,遵循NSKeyValueCoding非正式协议(NSKeyValueCoding就是NSObject的分类),提供了更简单,更统一的接口来访问对象的变量。
- KVC是依赖于runtime的,并且因为它可以动态的访问和修改变量的属性值,所以它也践行了OC是一门动态语言。
setValue:forKey:的原理 :(1). 按照顺序找调用者的类中是否实现了set<Key>、_set<Key>、setIs<Key>方法,其中Key表示对象的实例变量名,只要其中有一个实现了,则
setValue:forKey:的结果就是对应的方法实现。(2). 如果没有实现(1)中的3个方法,并且
accessInstanceVariablesDirectly的返回值是YES(默认是YES),则直接找到实例变量,对实例变量进行赋值。如果实例变量存在名字相同格式不同的情况下,例如 :
[aMan setValue:@"名字" forKey:@"jdName"];,但是aMan对象有4个和jdName相关的成员变量 :
_jdName,_isJdName,jdName,isJdName,则赋值的顺序为 : _<Key>--->_<isKey>---><Key>---><isKey>。(3). 如果找不到key对应的变量,就会执行
setValue:forUndefinedKey:抛出异常,异常默认是NSUndefinedKeyException。但是NSObject的子类可以重写这个方法,避免崩溃。
valueForKey:的原理 :(1). 按照顺序找调用者的类中是否实现了get<Key>---><Key>--->is<Key>--->_<Key>方法,其中Key表示对象的实例变量名,只要其中有一个实现了,则
valueForKey会返回其对应的实现。(2). 针对数组和集合的处理。如果数组和集合不满足上面的(1),则还有对应的方法实现会进行查找返回。
(3). 如果上述的(1)(2),都找不到对应的实现,并且
accessInstanceVariablesDirectly的返回值为YES的情况下,就去找实例变量本身,同名但格式不同的变量(和上面说过的一样)的查找顺序为 : _<Key>--->_<isKey>---><Key>---><isKey>。(4). 无论是在哪一步找到的值,都会根据取到的值的类型,返回不同的结果 :
<1>. 如果取到的值是对象指针,则直接返回。
<2>. 如果取到的值不是对象指针,但是可以转换成NSNumber类型,则转换成NSNumber类型再返回。
<3>. 如果取到的值既不是对象指针,又不能转换成NSNumber类型,则转换成NSValue类型再返回。
(5). 如果找不到Key对应的值,则调用
valueForUndefinedKey抛出NSUndefinedKeyException类型的默认异常。如果不想直接崩溃,可以在对象对应的子类中重写valueForUndefinedKey。