二十九、KVC(简介和部分原理)

697 阅读9分钟

本文由快学吧个人写作,以任何形式转载请表明原文出处。

KVC非常重要,只要还研究对象这个东西,就都避不开KVC的理念。

一、准备资料

KVC官方文档

二、KVC简介

  1. 全称 : key value coding,中文叫键值编码。

  2. 特性和主要功能 :

(1). 无论成员变量是公有还是私有的,KVC都可以通过变量名称(字符串类型)直接访问成员变量。

(2). 践行了OC的动态性,KVC无需调用明确的存取方法,可以动态的访问和修改对象的属性。

  1. KVC是要依赖runtime的。

  2. KVC本身是一种机制,它是根据NSKeyValueCoding的非正式协议使用的,NSKeyValueCoding是NSObject的分类。

  3. KVC提供了更简单,更统一的消息接口。

  4. 适用范围

(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的影响因素

图片.png

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 :

图片.png

VC中 :

图片.png

逐步运行 :

  1. JDMan的三个set方法都不注释
  2. 注释掉setJdName。
  3. 继续注释掉_setJdName。

三个运行结果如下:

(1). 不注释 :

kvc调用的是setJdName。

图片.png

(2). 注释掉setJdName。

kvc调用的是_setJdName。

图片.png

(3). 注释掉setJdName和_setJdName

kvc调用的是setIsJdName。

图片.png

第2步

如果上述的三个方法都没找到,就是都没实现,并且类方法accessInstanceVariablesDirectly返回的是YES(默认就是YES)。直接找实例变量进行赋值,顺序为 : _<Key>--->_is<Key> ---><Key>--->is<Key>。

如果accessInstanceVariablesDirectly返回的是NO,则进入第3步。如果这里的实例变量也都没有,则也进入第3步。

对第2点举例 :

JDMan代码 :

注释掉上述的3个set方法,让逻辑走到第2点中来。并且保证accessInstanceVariablesDirectly的返回值是YES。另外,需要在外部访问JDMan的实例变量,所以实例变量前要加@public

图片.png

VC代码 :

key是jdName。

图片.png

运行结果 :

图片.png

逐步运行 :

(1). 注释掉_jdName。把vc里面的NSLog里的_jdName也要删除了。 (2). 继续注释掉_isJdName。 (3). 继续注释掉jdName

逐步运行结果 :

图片.png

图片.png

图片.png

第3步

上述的两步中的set方法和实例变量都找不到。就会执行setValue:forUndefinedKey:抛出异常,异常默认是NSUndefinedKeyException。但是NSObject的子类可以重写这个方法,避免崩溃。

对第3点举例 :

注释掉JDMan中所有自己写的东西。然后重写一下setValue:forUndefinedKey:如下 :

图片.png

VC中 :

图片.png

运行结果 :

图片.png

正是我重写的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. 针对第1步 :

JDMan代码 :

图片.png

VC代码 :

图片.png

运行结果 :

图片.png

并没有直接取我给jdName设置的值,而是取了getJdName方法的返回值。验证了valueForKey的第1步。后续的逐步注释和上面的setValue:ForKey:一样,篇幅有限,不再多举例。

  1. 针对第2步 :

JDMan代码 :

图片.png

VC代码 :

图片.png

运行结果 :

图片.png

所以数组也是要先走第一步的。如果没有第一步的getter方法,就会进入第二步。

去掉get<Key>,也就是注释掉getJdArr,就是进入第二步了 :

图片.png

运行结果 :

图片.png

后面步骤就不举例了,和setValue:forKey:是一样的。

五、总结

  1. KVC本身是一种机制,遵循NSKeyValueCoding非正式协议(NSKeyValueCoding就是NSObject的分类),提供了更简单,更统一的接口来访问对象的变量。
  2. KVC是依赖于runtime的,并且因为它可以动态的访问和修改变量的属性值,所以它也践行了OC是一门动态语言。
  3. 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_isJdNamejdNameisJdName,则赋值的顺序为 : _<Key>--->_<isKey>---><Key>---><isKey>。

(3). 如果找不到key对应的变量,就会执行setValue:forUndefinedKey:抛出异常,异常默认是NSUndefinedKeyException。但是NSObject的子类可以重写这个方法,避免崩溃。

  1. 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