iOS 底层学习【KVC】
1.KVC 协议定义
键值编码是由NSKeyValueCoding非正式协议
启用的一种机制,对象采用该机制来提供对其属性的间接访问
。当对象符合键值编码时,其属性可通过字符串参数通过简洁、统一的消息传递接口
进行寻址。这种间接访问机制补充了实例变量及其相关访问器方法提供的直接访问。
本文收录:掘金【gufs 镜像】《iOS底层学习——KVC》
KVC 在 Objective-C 中的定义
KVC
的定义都是对NSObject
的扩展来实现的(Objective-C
中有个显式的NSKeyValueCoding
类别名-分类
)。查看setValueForKey
方法,发现其在Foundation
里面,而Foundation
框架是不开源
的,只能在苹果官方文档查找。见下图:
2.KVC 提供的 API 方法
我们可以学习解读苹果的官方文档,对
KVC
有更深的理解。苹果对一些容器类比如
NSArray
或者NSSet
等,KVC
有着特殊的实现。常用方法
对于所有继承了
NSObject
的类型,也就是几乎所有的Objective-C
对象都能使用KVC
,下面是KVC
最为重要的四个方法:
特殊方法
当然
NSKeyValueCoding
类别中还有其他的一些方法,这些方法在碰到特殊情况或者有特殊需求还是会用到的。
结构体处理
KVC
在进行结构体处理时,需要用到NSValue
,设值时,将结构体封装成NSValue
,进行键值设值;取值同样返回NSValue
,然后按照结构体格式进行解析,见下面代码:
字典处理(模型转换)
字典可以实现与模型进行装换,也可以通过键值数组从模型中获取字典数据。见下面代码:
3.KVC 设值取值顺序
KVC
是怎么使用的,我们都很清楚,那么KVC
在内部是按什么样的顺序来寻找key
的呢?这是我们要探索的重点。
1.设值
当调用setValue:forKey:
代码时,底层的执行机制是怎样的呢?在官方文档中有相关的说明,见下图:
翻译过来的意思是:
setValue:forKey:
的默认实现,给定key
和value
参数作为输入,尝试将名为key
的属性设置为value
,在接收调用的对象内部,使用以下过程:按顺序查找名为set<Key>:
或_set<Key>
的第一个访问器。 如果找到,则使用输入值(或根据需要展开的值)调用它并完成。如果未找到简单访问器,并且类方法accessInstanceVariablesDirectly
返回YES
,则按顺序查找名称类似于_<key>
、_is<Key>
、<key>
或is<Key>
的实例变量。 如果找到,直接使用输入值(或解包值)设置变量并完成。 在未找到访问器或实例变量时,调用setValue:forUndefinedKey:
。 默认情况下,这会引发异常,但NSObject
的子类可能会提供特定于键的行为。根据上的官方内容,可以得出如下实现机制:
按顺序查找名为
set<Key>
,_set<Key>
或者setIs<Key>
的setter
访问器顺序查找,如果找到就调用它只要实现任意一个,那么就会将调用这个方法,将属性的值设为传进来的值
如果没有找到这些
setter
方法,KVC
机制会检查+ (BOOL)accessInstanceVariablesDirectly
方法有没有返回YES
,默认该方法会返回YES
,如果重写了该方法让其返回NO
的话,那么在这一步KVC
会执行setValue:forUndefinedKey:
方法;如果返回
YES
,KVC
机制会优先搜索该类里面有没有名为_<Key>
的成员变量,无论该变量是在类接口处定义,还是在类实现处定义,也无论用了什么样的访问修饰符,只在存在以_<Key>
命名的变量,KVC
都可以对该成员变量赋值KVC
机制再会继续搜索_is<Key>
、<key>
和is<key>
的成员变量,再给它们赋值如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的
setValue:forUndefinedKey:
方法,默认是抛出异常。以
[person setValue:@"newName" forKey:@"name"];
为例,可以得出以下结论:优先通过
setter
方法,进行属性设置,调用顺序是:setName
_setName
setIsName
如果以上方法均未找到,并且
accessInstanceVariablesDirectly
返回YES
,则通过成员变量进行设置,顺序是:_name
_isName
name
isName
可通过案例进行验证,这里不再展示。
accessInstanceVariablesDirectly
说明重写
+ (BOOL)accessInstanceVariablesDirectly
方法让其返回NO
,这样的话,如果KVC
没有找到set<Key>
、_set<Key>
、setIs<Key>
相关方法时,会直接用setValue:forUndefinedKey:
方法。我们用代码来测试一下上面的KVC
机制:
运行结构见下图:
这说明了重写+(BOOL)accessInstanceVariablesDirectly
方法让其返回NO
后,KVC
找不到set<Key>
等方法后,不再去找<Key>
系列成员变量,而是直接调用setValue:forUndefinedKey:
方法,所以开发者如果不想让自己的类实现KVC
,就可以这么做。
KVC 设值流程图
2.取值
当调用valueForKey:
的代码时,底层的执行机制又是怎样的呢?在官方文档中有相关的说明,见下图:
根据上的官方内容,翻译之后可以得出如下实现机制:
首先按
get<Key>
,<Key>
,is<Key>
,_<Key>
的顺序方法查找getter
方法,找到的话会直接调用,如果是BOOL
或者Int
等值类型, 会将其包装成一个NSNumber
对象。如果上面的
getter
没有找到,KVC
则会查找countOf<Key>
,objectIn<Key>AtIndex
或<Key>AtIndexes
格式的方法。如果countOf<Key>
方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray
所有方法的代理集合(它是NSKeyValueArray
,是NSArray
的子类),调用这个代理集合的方法,或者说给这个代理集合发送属于NSArray
的方法,就会以countOf<Key>
,objectIn<Key>AtIndex
或At<Key>Indexes
这几个方法组合的形式调用。还有一个可选的get<Key>:range:
方法。所以你想重新定义KVC
的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC
的标准命名方法,包括方法签名。如果上面的方法没有找到,那么会同时查找
countOf<Key>
,enumeratorOf<Key>
,memberOf<Key>
格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet
所的方法的代理集合,和上面一样,给这个代理集合发NSSet
的消息,就会以countOf<Key>
,enumeratorOf<Key>
,memberOf<Key>
组合的形式调用。如果还没有找到,再检查类方法
+ (BOOL)accessInstanceVariablesDirectly
,如果返回YES
(默认行为),那么和先前的设值一样,会按_<Key>
,_is<Key>
,<Key>
,is<Key>
的顺序搜索成员变量名,这里不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱。如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly
返回NO
的话,那么会直接调用valueForUndefinedKey:
还没有找到的话,调用
valueForUndefinedKey:
以
[person valueForKey:@"name"];
为例getter
方法的调用顺序是:getName
name
isName
_name
如果以上方法没有找到,accessInstanceVariablesDirectly 返回 YES,则直接返回成员变量,获取顺序依然是:
_name
_isName
name
isName
可通过案例进行验证,这里不再展示。
KVC 取值流程图
可以通过下面的代码对以上结论进行验证!
4.在 KVC 中使用 keyPath
除了对当前对象的属性进行赋值外,还可以对其更深层的对象进行赋值。例如,对当前对象的location
属性的country
属性进行赋值。KVC
进行多级访问时,直接类似于属性调用一样用点语法进行访问即可。
通过keyPath
对数组进行取值时,并且数组中存储的对象类型都相同,可以通过valueForKeyPath:
方法指定取出数组中所有对象的某个字段。例如下面例子中,通过valueForKeyPath:
将数组中所有对象的name
属性值取出,并放入一个数组中返回。
5.异常处理
当根据KVC
搜索规则,没有搜索到对应的key
或者keyPath
,则会调用对应的异常方法。异常方法的默认实现,在异常发生时会抛出一个异常,并且应用程序Crash
。见下图:
我们可以重写下面两个方法:
重写这两个方法之后,运行程序不再崩溃,见下图:
但是我们可以根据业务需要,合理的处理KVC
导致的异常。比如下面的处理方式:
6.自定义 KVC
根据苹果官方文档提供的设值、取值规则,我们可以自己进行 KVC 的自定义实现。见下面实现代码:
评论