iOS 底层探究:KVC

518 阅读5分钟

这是我参与8月更文挑战的第14天,活动详情查看:8月更文挑战

KVC的全称是Key-Value Coding,翻译成中文就是键值编码,键值编码是一种有NSKeyValueCoding非正式协议启用的机制,对象采用该协议来提供对其属性的间接访问。既可以通过一个字符串key来访问某个属性。这种简洁访问机制补充了实例变量及其相关的刚问起方法所提供的直接访问。

1. KVC设值

一般来说,对对象属性赋值有2种方式。

  • 直接通过setter方法赋值
  • 通过KVC键值编码的相关API赋值

image.png 查看setValueForKey方法,发现其在Foundation里面。而Foundation框架是不开源的,那么接下来就去苹果的官方文档查找。 Key-Value Coding Programming Guide 在这里看到setValueForKey方法的流程如下:

  • 第一步:按顺序查找名为set<Key>,_set<Key>或者setIs<Key>的setter方法。如果找到就调用它。
    • 只要实现任意一个,那么就会将调用这个方法将属性的值设为传进来的值。如果存在多个,则调用顺序在前面的
    • 如果没有则进入第二步
  • 第二步:如果三个setter方法都没有实现,那么就检查accessInstanceVariablesDirectly是否返回YES,是的话就则按顺序查找名为_<Key>,_is<Key>,<Key>is<Key>的实例变量。否则就跳到第三步。
    • 只要找到任意一个,那么就会对实例变量进行赋值,如果存在多个,则赋值顺序在前面的。
    • 如果没有就进入第三步。
  • 第三步:如果找不到setter方法实例变量,系统会执行该对象的setValue:forUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常。

setValueForKey流程图

image.png

2. KVC取值

kvc取值方法是valueForKey:,方法流程如下:

  • 第一步:首先按照get<Key>,<Key>,is<Key>,_<Key>的方法顺序查找getter方法
    • 如果找到,则进入第五步
    • 如果没找到,则进入第二步
  • 第二步:如果没有实现getter方法,KVC会查找countOf<Key>,objectIn<Key>AtIndex:<Key>AtIndexes:
    • 如果找到countOf<Key>和其他两个中的一个方法,创建一个响应所有NSArray方法的集合代理对象即NSKeyValueArray,并返回该对象。否则就会去第三步。
    • 代理对象随后将接收到的所有NSArray消息转换为countOf<Key>,objectIn<Key>AtIndex:<Key>AtIndexes:消息的某种组合,发送消息给创建它的键值编码对象。如果原始对象也实现了一个名为get<Key>:range:的可选方式,代理对象也会在适当的时候使用该方法。实际上,代理对象与键值编码对象一起工作来使属性可以像NSArray一样使用,即使他不是NSArray。
  • 第三步:如果没有实现普通的getter方法或者array的组合getter方法,那么就会同时查找countOf<Key>,enumeratorOf<Key>memberOF<Key>这三个方法
    • 如果这三个方法都找到了,创建一个集合代理对象来响应所有的NSSet方法并返回它。否则继续步骤4
    • 这个代理对象随后将它收到的任何NSSet纤细转换成countOf<Key>,enumeratorOf<Key>,memberOf<Key>的一些组合:发送消息到创建它的对象。实际上,代理对象与键值编码对象一起工作来使属性可以像NSSet一样使用,即使它不是NSSet。
  • 第四步:如果没有实现普通的getter方法或者集合的组合getter方法,并且accessInstanceVariableDirectly返回YES,那么就会按顺序寻找实例变量_<Key>,_is<Key>,<Key>is<Key>
    • 只要找到任意一个,那么就会取得这个实例变量的值并且去到第五步。如果存在多个,则获得顺序在前面的实例变量的值。然后去第五步
    • 否则就去第六步
  • 第五步:返回结果
    • 如果搜索到的属性值是对象指针,只需返回结果。
    • 如果该值是NSNumber支持的变量类型,将其存储在NSNumber实例中并返回该对象
    • 如果结果是NSNumber不支持的变量类型,将其转换为NSValue对象并返回该对象
  • 第六步:如果上面5步的方法均失败,系统会执行该对象的valueForUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常

valueForKey流程图

image.png

3. 自定义KVC

看过了设值和取值,我们可以根据苹果官方给出的查找规则进行自定义KVC的实现

3.1 自定义设值

  1. 判断key是否为空

image.jpeg

  1. 查找是否有setter方法set<Key>:,_set<Key>,setIs<Key>,如果有则实现并返回.

image.png 3. 判断accessInstanceVariablesDirectly的返回值是否为YES,是则往下走,否则,抛出异常。

image.png 4.查找自己的ivar列表中是否包含实例变量_<Key>,_is<Key>,<Key>,is<Key>,找到就赋值

image.png 5.如果都搜索不到,就抛出异常

image.jpeg

3.2 自定义取值

1.判断key非空

image.png 2. 按顺序查找方法:get<Key>,<Key>,countOf<Key>,objectIn<Key>AtIndex

image.png 3. 判断accessInstanceVariablesDirectly的返回值是否为YES,可以则往下走,否则抛出异常。

image.png 4.查找自己的ivar列表中是否包含实例变量_<Key>,_is<Key>,<Key>,is<Key>,找到就赋值

image.png 5.抛出异常

image.png

4. 使用场景

4.1 动态设值和取值

  • 通过Key读取和存储:setValue:forKey:valueForKey:
  • 通过keyPath读取和存储:setvalue:forKeyPath:valueForKeyPath:

4.2 通过KVC访问和修改私有变量

在日常开发中,对于类的私有属性,在外部定义的对象,是无法直接访问私有属性的。但是对于KVC而言,一个对象没有自己的隐私,所以可以通过KVC修改和访问任何私有属性

4.3 多值操作

模型字典转换

//字典转模型 
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues; 
//模型转字典 
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

4.4 修改一些系统空间的内部属性

在日常开发中,很多UI控件都在其内部由多控件组合而成,这些内部控件苹果并没有提供访问的API,但是使用KVC可以解决这个问题,例如:自定义tabbar、修改UITextField中的placeHolderText

4.5 用KVC实现高阶消息传递

在对容器使用KVC时,valueForKey:将会被传递给容器中的每一个对象,而不是对容器本身进行操作,结果会被添加到返回的容器中,这样,可以很方便的操作集合,然后返回另一个集合