KVC
的全称是Key-Value Coding
,翻译成中文是键值编码
,键值编码是由NSKeyValueCoding
非正式协议启用的一种机制,对象采用该协议来间接访问其属性
。即可以通过一个字符串key
来访问某个属性。这种间接访问机制补充了实例变量及其相关的访问器方法所提供的直接访问。
相关API
通过KVC的方法跳转我们可以发现它属于Foundation
里面,也就是说没开源
.
常用API:
- 根据
key
值取/赋值:
/* Return an array containing the results of invoking -valueForKey: on each of the receiver's elements. The returned array will contain NSNull elements for each instance of -valueForKey: returning nil.
*/
- (id)valueForKey:(NSString *)key;
/* Invoke -setValue:forKey: on each of the receiver's elements.
*/
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- 根据
keyPath
取/赋值:
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
其他API:
//默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
+ (BOOL)accessInstanceVariablesDirectly;
//KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//如果Key不存在,且KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
- (nullable id)valueForUndefinedKey:(NSString *)key;
//和上一个方法一样,但这个方法是赋值。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//如果你在SetValue方法时面给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;
//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
KVC 底层原理探索
上面说了,KVC是闭源的
,所以我们没法查看源码.但还有其他方法:
-
反编译: 通过
Hopper
反编译,可以得到伪源码(比较麻烦). -
站在别人的肩膀上: 在
GitHub
上找大神的自定义KVC
demo. - 问Apple: 直接去developer.apple.com上找苹果的文档.
在这里,我们通过Key-Value Coding Programming Guide苹果官方文档来研究:
KVC赋值:
当调用setValue:forKey:
设置属性value
时,其底层的执行流程为:
-
首先查找是否有这三种
setter
方法,按照查找顺序为set<Key>:-> _set<Key> -> setIs<Key>
(1). 如果
有其中任意一个
setter方法,则直接设置属性的value
(主注意:key是指成员变量名,首字符大小写需要符合KVC的命名规范
);
(2). 如果都没有
,则进入 [第2步]; 如果没有找到第1步中的三个
setter
方法,则查找accessInstanceVariablesDirectly
是否返回YES
,
(1). 如果返回YES
,则查找间接访问的实例变量进行赋值,查找顺序为:_<key> -> _is<Key> -> <key> -> is<Key>
. 如果找到其中任意一个实例变量
,则赋值
. 如果都没有,则进入[第3步];
(2). 如果返回NO,则进入[第3步];
- 如果
setter方法
或者实例变量
都没有找到,系统会执行该对象的setValue:forUndefinedKey:
方法,默认抛出NSUndefinedKeyException类型的异常
如下图:
KVC取值:
由官方文档分析可以知道:
当调用valueForKey:时,其底层的执行流程如下:
首先查找getter方法,按照
get<Key> -> <key> -> is<Key> -> _<key>
的方法顺序查找,
(1). 如果找到
,则进入[第5步];
(2). 如果都没有
,则进入 [第2步];如果第一步中的getter方法没有找到,KVC会查找
countOf <Key>和objectIn <Key> AtIndex :和<key> AtIndexes :
(1). 如果找到countOf <Key>
和其他两个中的一个,则会创建一个响应所有NSArray
方法的集合代理对象
,并返回该对象,即NSKeyValueArray
,是NSArray
的子类
。代理对象随后将接收到的所有NSArray消息转换为countOf<Key>,objectIn<Key> AtIndex:和<key>AtIndexes:
消息的某种组合,用来创建键值编码对象。如果原始对象还实现了一个名为get<Key>:range:
之类的可选方法,则代理对象也将在适当时使用该方法(注意:方法名的命名规则要符合KVC的标准命名方法,包括方法签名。);
(2). 如果没有找到这三个访问数组的,则进入[第3步];如果没有找到上面的几种方法,则会同时查找
countOf <Key>,enumeratorOf<Key>和memberOf<Key>
这三个方法
(1). 如果这三个方法都找到,则会创建一个响应所有NSSet方法的集合代理对象
,并返回该对象,此代理对象随后将其收到的所有NSSet
消息转换为countOf<Key>,enumeratorOf<Key>和memberOf<Key>:
消息的某种组合,用于创建它的对象;
(2). 如果还是没有找到,则进入 [第4步];如果还没有找到,检查类方法
InstanceVariablesDirectly
是否YES
,依次搜索_<key>,_is<Key>,<key>或is<Key>
的实例变量
(1). 如果搜到,直接获取实例变量的值,进入 [第5步];根据搜索到的
属性值的类型
,返回不同的结果
(1). 如果是对象指针
,则直接返回结果;
(2). 如果是NSNumber支持
的标量类型,则将其存储在NSNumber实例
中并返回它;
(3). 如果是NSNumber不支持
的标量类型,请转换为NSValue对象
并返回该对象-
如果上面5步的方法均失败,系统会执行该对象的
setValue:forUndefinedKey:
方法,默认抛出NSUndefinedKeyException
类型的异常
如下图所示:
路由访问,即keyPath:
在日常开发中,一个类的成员变量有可能是自定义类或者其他的复杂数据类型,一般的操作是,我们可以先通过KVC获取该属性,然后再通过KVC获取自定义类的属性,就是比较麻烦,还有另一种比较简便的方法,就是使用KeyPath
即路由
,涉及以下两个方法:setValue:forKeyPath:
和 valueForKeyPath:
//通过KeyPath来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;
//通过KeyPath来设值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
参考如下的案例
//LCPerson类
@interface LCPerson : NSObject
@property (nonatomic, copy) NSString *age;
@property (nonatomic, strong) LCStudent *student;
@end
//LCStudent类
@interface LCStudent : NSObject
@property (nonatomic, copy) NSString *name;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
LCPerson *person = [[LCPerson alloc] init];
LCStudent *student = [LCStudent alloc];
student.name = @"LC";
person.student = student;
//根据kvc - keyPath路由修改student的subject属性的值
[person setValue:@"嘻嘻" forKeyPath:@"student.name"];
NSLog(@"%@",[person valueForKeyPath:@"student.name"]);
}
return 0;
}
//*************打印结果*************
2020-10-27 09:55:08.512833+0800 001-KVC简介[58089:6301894] 改变前:CJL
2020-10-27 09:55:08.512929+0800 001-KVC简介[58089:6301894] 改变后:嘻嘻
使用场景:
1、动态设值和取值
- 常用的可以通过
setValue:forKey:
和valueForKey:
- 也可以通过
路由
的方式setValue:forKeyPath:
和valueForKeyPath:
2、通过KVC访问和修改私有变量
在日常开发中,对于类的私有属性
,在外部定义的对象,是无法直接访问私有属性的,但是对于KVC而言,一个对象没有自己的隐私
,所以可以通过KVC修改和访问任何私有属性
3、多值操作(model和字典互转)
model和字典的转换可以通过下面两个KVC的API实现
//字典转模型
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
//模型转字典
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
4、修改一些系统空间的内部属性
在日常开发中,我们知道,很多UI控件都是在其内部由多个UI空间组合而成,这些内部控件苹果并没有提供访问的API,但是使用KVC可以解决这个问题,常用的就是自定义tabbar
、个性化UITextField
中的placeHolderText
5、用KVC实现高阶消息传递
在对容器类使用KVC
时,valueForKey:将会被传递给容器中的每一个对象,而不是对容器本身进行操作,结果会被添加到返回的容器中,这样,可以很方便的操作集合 来返回 另一个集合
如下所示
//KVC实现高阶消息传递
- (void)transmitMsg{
NSArray *arrStr = @[@"english", @"franch", @"chinese"];
NSArray *arrCapStr = [arrStr valueForKey:@"capitalizedString"];
for (NSString *str in arrCapStr) {
NSLog(@"%@", str);
}
NSArray *arrCapStrLength = [arrCapStr valueForKeyPath:@"capitalizedString.length"];
for (NSNumber *length in arrCapStrLength) {
NSLog(@"%ld", (long)length.integerValue);
}
}
//********打印结果********
2020-10-27 11:33:43.377672+0800 LCustom[60035:6380757] English
2020-10-27 11:33:43.377773+0800 LCustom[60035:6380757] Franch
2020-10-27 11:33:43.377860+0800 LCustom[60035:6380757] Chinese
2020-10-27 11:33:43.378233+0800 LCustom[60035:6380757] 7
2020-10-27 11:33:43.378327+0800 LCustom[60035:6380757] 6
2020-10-27 11:33:43.378417+0800 LCustom[60035:6380757] 7