KVC底层原理
2020-02-10 本文已影响0人
CS_SDN
一、概念定义
KVC:Key-value coding (键-值编码)
想理解KVO必须首先理解KVC!足可见KVC的重要性。
常见的API有:
- (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
二、setValue:forKey:的原理

set的搜索规则总结为:
- 查找set<Key>:或_set<Key>命名的setter,按照这个顺序,如果找到的话,调用这个方法并将值传进去(根据需要进行对象转换)。
- 如果没有发现一个简单的setter,但是accessInstanceVariablesDirectly类属性返回YES,则查找一个命名规则为_<key>、_is<Key>、<key>、is<Key>的实例变量。根据这个顺序,如果发现则将value赋值给实例变量。
- 如果没有发现setter或实例变量,则调用setValue:forUndefinedKey:方法,并默认提出一个异常,但是一个NSObject的子类可以提出合适的行为。
其中类属性accessInstanceVariablesDirectly
+ (BOOL)accessInstanceVariablesDirectly{
return YES; ///> 可以直接访问成员变量
// return NO; ///> 不可以直接访问成员变量,
///> 直接访问会报NSUnkonwKeyException错误
}
它表示是否允许读取实例变量的值,如果为YES则在KVC查找的过程中,从内存中读取属性、实例变量的值。如果不允许外界通过KVC对我们的私有属性和成员变量进行操作,则可以设置此值为NO。
三、valueForKey:的原理

get的搜索规则相对于set就有点复杂
- 通过getter方法搜索实例,例如get<Key>, <key>, is<Key>, _<key>的拼接方案。按照这个顺序,如果发现符合的方法,就调用对应的方法并拿着结果跳转到第五步。否则,就继续到下一步。
- 如果没有找到简单的getter方法,则搜索其匹配模式的方法countOf<Key>、objectIn<Key>AtIndex:、<key>AtIndexes:。如果找到其中的第一个和其他两个中的一个,则创建一个集合代理对象NSKeyValueArray,该对象响应所有NSArray的方法并返回该对象。否则,继续到第三步。代理对象随后将NSArray接收到的countOf<Key>、objectIn<Key>AtIndex:、<key>AtIndexes:的消息给符合KVC规则的调用方。当代理对象和KVC调用方通过上面方法一起工作时,就会允许其行为类似于NSArray一样。
- 如果没有找到NSArray简单存取方法,或者NSArray存取方法组。则查找有没有countOf<Key>、enumeratorOf<Key>、memberOf<Key>:命名的方法。如果找到三个方法,则创建一个集合代理对象,该对象响应所有NSSet方法并返回。否则,继续执行第四步。此代理对象随后转换countOf<Key>、enumeratorOf<Key>、memberOf<Key>:方法调用到创建它的对象上。实际上,这个代理对象和NSSet一起工作,使得其表象上看起来是NSSet。
- 如果没有发现简单getter方法,或集合存取方法组,以及接收类方法accessInstanceVariablesDirectly是返回YES的。搜索一个名为_<key>、_is<Key>、<key>、is<Key>的实例,根据他们的顺序。如果发现对应的实例,则立刻获得实例可用的值并跳转到第五步,否则,跳转到第六步。
- 如果取回的是一个对象指针,则直接返回这个结果。如果取回的是一个基础数据类型,但是这个基础数据类型是被NSNumber支持的,则存储为NSNumber并返回。如果取回的是一个不支持NSNumber的基础数据类型,则通过NSValue进行存储并返回。
6.如果所有情况都失败,则调用valueForUndefinedKey:方法并抛出异常,这是默认行为。但是子类可以重写此方法。
四、使用场景
- 动态地设值和取值
这个应用就不多说了,最基本的应用 - 用KVC来访问和修改私有变量
对于KVC来说,一个对象没有自己的隐私,只要它愿意,就可以修改任何私有的东西。不信可以试试在.m文件中声明私有属性或者成员变量,KVC一样可以获取到。 - 多值操作(model和字典互转)
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
主要通过这两个API来实现,也很简单,不多介绍。
- 修改一些系统控件的内部属性
使用runtime来获取Apple不想开放的成员变量,利用KVC进行修改。比如自定义tabbar,textfield等,这个的应用也是比较常见。 - 用KVC实现高阶消息传递
这个应用场景就比较少了,它的意思是在对容器类使用KVC时,valueForKey:将会被传递给容器中的每一个对象,而不是对容器本身进行操作。比如下面的代码:
NSArray* arrStr = @[@"english",@"franch",@"chinese"];
NSArray* arrCapStr = [arrStr valueForKey:@"capitalizedString"];
for (NSString* str in arrCapStr) {
NSLog(@"%@",str);
}
NSArray* arrCapStrLength = [arrStr valueForKeyPath:@"capitalizedString.length"];
for (NSNumber* length in arrCapStrLength) {
NSLog(@"%ld",(long)length.integerValue);
}
打印结果如下:

-
用KVC中的函数来操作集合(集合主要指NSArray和NSSet,不包括NSDictionary)
图片.png
五、关于面试
- iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
willChangeValueForKey:
父类原来的setter
didChangeValueForKey:
内部会触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:)
- 如何手动触发KVO?
手动调用willChangeValueForKey:和didChangeValueForKey:
- 直接修改成员变量会触发KVO么?
不会触发KVO,因为直接修改成员变量并没有走set方法。
-
通过KVC修改属性会触发KVO么?
会触发KVO,如流程图所示
图片.png