OC底层原理22:KVC底层原理分析

张建 lol

前言

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

KVC相关API

常用方法

主要有以下四种常用的方法:

  • 通过 key 设值/取值
1
2
3
4
5
// 通过key来取值
- (nullable id)valueForKey:(NSString *)key;

// 通过key来设值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
  • 通过 keyPath(即路由)设值/取值
1
2
3
4
5
// 通过keyPath来取值
-(nullable id)valueForKeyPath:(NSString *)keyPath;

// 通过keyPath来设值
-(void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
  • 其他方法
1
2
3
默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
是否可以直接访问实例变量
+(BOOL)accessInstanceVariablesDirectly;
1
2
KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
1
2
这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
1
2
如果Key不存在,且KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
- (nullable id)valueForUndefinedKey:(NSString *)key;
1
2
和上一个方法一样,但这个方法是设值。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
1
2
如果你在SetValue方法时面给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;
1
2
输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

KVC设值底层原理

  1. 在日常开发中,针对对象属性的 赋值,一般有以下两种方式
  • 直接通过 setter 方法赋值
  • 通过 KVC键值编码 的相关API赋值
1
2
3
4
5
ZJPerson * person = [[ZJPerson alloc] init];
// 1.一般setter方法
person.name = @"ZJ_哈哈";
// 2.KVC方法
[person setValue:@"ZJ_嘻嘻" forKey:@"name"];

下面针对使用最多的 KVC 设值方法:seValue:forKey,来进行底层原理的探索

  1. 首先进入 seValue:forKey 的声明,发现是在 Foundation 框架中,而Foundation 框架是不开源的,有以下几种方式可以去探索底层:
  • 通过 Hopper 反汇编,查看伪代码
  • 通过苹果 官方文档
  • Github 搜索是否有相关的demo
  1. 在这里,我们通过 Key-Value Coding Programming Guide 苹果官方文档来研究,针对设值流程,有如下说明:

当调用 seValue:forKey 设值属性 value 时,其底层的执行流程为:

  • 【第一步】 首先查找是否有这三种 setter 方法,按照查找顺序为 set<Key> 、_set<Key>、setIs<Key>

    • 如果 有其中任意一个 setter方法,则 直接设值属性的value(注意:key是指成员变量名,首字母的大小写需要符合KVC的命名规范

    • 如果都 没有 ,则进入 【第二步】

  • 【第二步】 如果没有第一步的三个简单的setter方法,则查找accessInstanceVariablesDirectly 是否返回 YES

    • 如果返回 YES,则查找间接访问的 实例变量 进行赋值,查找顺序为:_<key>、_is<Key>、<key>、is<Key>

      • 如果找到其中任意一个实例变量,则赋值

      • 如果都没有,则进入 【第三步】

    • 如果返回NO,则进入 【第三步】

  • 【第三步】 如果 setter方法 或者 实例变量 都没有找到,系统会执行该对象的 setValue:forUnderfinedKey: 方法,默认抛出 NSUnderfinedKeyException 类型的异常

  1. 综上所述,KVC通过 seValue:forKey 方法设值的流程,以ZJPerson的对象person的属性name为例,如下图所示:

KVC取值底层原理

同样的,我们可以通过官方文档分析KVC取值的底层原理

当调用 valueForKey 时,其底层的执行流程如下:

  • 【第一步】 首先查找 getter 方法,按照:get<Key>、<key>、is<Key>、_<key> 的方法顺序查找

    • 如果 找到 ,则进入 【第五步】
    • 如果 没有找到 ,则进入 【第二步】
  • 【第二步】 如果第一步中的 getter 方法没有找到,KVC会查找:countOf<Key>、objectIn<Key> AtIndex:、<key>AtIndex:

    • 如果找到 countOf<Key> 和其他两个中的一个,则会创建一个响应所有NSArray 方法的 集合代理对象,并返回该对象,即 NSKeyValueArray,是NSArray 的子类。代理对象随后将接收到的所有 NSArray 消息转换为countOf<Key>、objectIn<Key> AtIndex:、<key>AtIndex: 消息的某种组合,用来创建键值编码对象。如果原始对象还实现了一个名为 get<Key>:range:之类的可选方法,则代理对象也将在适当时使用该方法(注意:方法名的命名规则要符合KVC的标准命名方法,包括方法签名。)

    • 如果没有找到这三个访问数组,则继续进入 【第三步】

  • 【第三步】 如果没有找到上面的几种方法,则会同时查找 countOf <Key>,enumeratorOf<Key>和memberOf<Key> 这三个方法

    • 如果这三个方法都找到,则会创建一个响应 所有NSSet方法的集合代理对象 ,并返回该对象,此代理对象随后将其收到的所有 NSSet 消息转换为 countOf<Key>,enumeratorOf<Key>和memberOf<Key>: 消息的某种组合,用于创建它的对象

    • 如果还是没有找到,则进入 【第四步】

  • 【第四步】 如果还没有找到,检查类方法 InstanceVariablesDirectly 是否 YES,依次搜索 _<key>,_is<Key><key>is<Key> 的实例变量

    • 如果搜到,直接获取实例变量的值,进入 【第五步】
  • 【第五步】 根据搜索到的属性值的类型,返回不同的结果

    • 如果是 对象指针,则直接返回结果

    • 如果是 NSNumber支持 的标量类型,则将其 存储在NSNumber实例 中并返回它

    • 如果是是 NSNumber不支持 的标量类型,请 转换为NSValue对象 并返回该对象

  • 【第六步】 如果上面5步的方法 均失败,系统会执行该对象的valueForUndefinedKey: 方法,默认抛出 NSUndefinedKeyException 类型的异常

综上所述,KVC通过 valueForKey: 方法取值的流程以设值 ZJPerson 的对象person的属性 name 为例,如下图所示:

自定义KVC

原理:通过给 NSObject 添加 分类ZJKVC,实现自定义的 zj_setValue:forKey:zj_valueForKey: 方法,根据苹果官方文档提供的查找规则进行实现:

1
2
3
4
5
6
@interface NSObject (ZJKVC)
// 设值
- (void)zj_setValue:(nullable id)value forKey:(NSString *)key;
// 取值
- (nullable id)zj_valueForKey:(NSString *)key;
@end

自定义KVC设值

自定义KVC设值

  • 1、判断 key非空

  • 2、查找 setter 方法,顺序是:setXXX、_setXXX、setIsXXX

  • 3、判断是否响应 accessInstanceVariablesDirectly 方法,即间接访问实例变量

    • 返回 YES,继续下一步设值
    • 返回 NO,则崩溃
  • 4、间接访问变量赋值(只会走一次),顺序是:_key、_isKey、key、isKey

    • 定义一个手机实例变量的可变数组
    • 通过class_getInstanceVariable 方法,获取响应的ivar
    • 通过object_setIvar方法,对响应的ivar设值值
  • 5、如果找不到相关实例变量,则抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@implementation NSObject (ZJKVC)
// 设值
- (void)zj_setValue:(nullable id)value forKey:(NSString *)key{
// 1.判断key是否存在
if (key == nil || key.length == 0) {
return;
}

// 2.找setter方法,顺序是:setXXX、_setXXX、setIsXXX
NSString * Key = key.capitalizedString;
// key 要大写
NSString * setKey = [NSString stringWithFormat:@"set%@:", Key];
NSString * _setKey = [NSString stringWithFormat:@"_set%@:", Key];
NSString * setIsKey = [NSString stringWithFormat:@"setIs%@:", Key];

// 判断是否有方法
if ([self zj_performSelectorWithMethodName:setKey value:value]) {
NSLog(@"*************%@*************", setKey);
return;
}else if ([self zj_performSelectorWithMethodName:_setKey value:value]) {
NSLog(@"*************%@*************", _setKey);
return;
}if([self zj_performSelectorWithMethodName:setIsKey value:value]){
NSLog(@"*************%@*************", setIsKey);
return;
}

// 3、判断是否响应accessInstanceVariablesDirectly方法,即间接访问实例变量,返回YES,继续下一步设值,如果是NO,则崩溃
if (![self.class accessInstanceVariablesDirectly]) {
@throw [NSException exceptionWithName:@"ZJUnKnownKeyException" reason:[NSString stringWithFormat:@"***[%@ valueForUnderfinedKey:] this class is not key value coding-compliant for the key name.***"] userInfo:nil];
}

// 4.间接访问变量赋值,顺序为:_key、_isKey、key、isKey
// 4.1 定义一个手机实例变量的可变数组
NSMutableArray * mArray = [self getIvarListName];
// _key、_isKey、key、isKey
NSString * _key = [NSString stringWithFormat:@"_%@:", Key];
NSString * _isKey = [NSString stringWithFormat:@"_is%@:", Key];
NSString * isKey = [NSString stringWithFormat:@"is%@:", Key];
if ([mArray containsObject:_key]) {
// 4.2 获取响应的 ivar
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
// 4.3 对相应的 ivar 设值值
object_setIvar(self, ivar, value);
return;
}else if ([mArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
object_setIvar(self, ivar, value);
return;
}else if ([mArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
object_setIvar(self, ivar, value);
return;
}else if ([mArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
object_setIvar(self, ivar, value);
return;
}

// 5.如果找不到则抛出异常
@throw [NSException exceptionWithName:@"ZJUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}

相关方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#pragma mark - 相关方法
- (BOOL)zj_performSelectorWithMethodName:(NSString *)methodName value:(id)value{
if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
// 如果你确定不会发生内存泄漏的情况下,可以使用如下的语句来忽略掉这条警告
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic pop
return YES;
}
return NO;
}

- (id)performSelectorWithMethodName:(NSString *)methodName{
if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
// 如果你确定不会发生内存泄漏的情况下,可以使用如下的语句来忽略掉这条警告
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [self performSelector:NSSelectorFromString(methodName) ];
#pragma clang diagnostic pop
}
return nil;
}

获取成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#pragma mark -获取成员变量名
- (NSMutableArray *)getIvarListName{
// 创建可变数组,用于存储ivar成员变量
NSMutableArray * mArray = [NSMutableArray arrayWithCapacity:1];
unsigned int count = 0;
// 获取类的成员变量列表
Ivar * ivars = class_copyIvarList([self class], &count);
// 遍历成员变量列表
for (int i = 0; i<count; i++) {
Ivar ivar = ivars[i];
// 获取成员变量名字字符
const char * ivarNameChar = ivar_getName(ivar);
// 将字符转换成字符串
NSString * ivarName = [NSString stringWithUTF8String:ivarNameChar];
NSLog(@"ivarName == %@", ivarName);
// 存入可变数组
[mArray addObject:ivarName];
}
//释放成员变量列表
free(ivars);
return mArray;
}
@end

自定义KVC取值

  • 1、判断 key非空

  • 2、查找相应方法,顺序是 get<Key>、<key>、countOf<Key>、objectIn<Key>AtIndex

  • 3、判断是否能够直接赋值实例变量,即判断是否响应 accessInstanceVariablesDirectly 方法,间接访问实例变量

    • 返回 YES,继续下一步取值
    • 返回 NO,则崩溃
  • 4、间接访问实例变量,顺序是 _<key>、_is<Key>、<key>、is<Key>

    • 定义一个手机实例变量的可变数组
    • 通过class_getInstanceVariable方法,获取相应的ivar
    • 通过object_getIvar方法,返回相应的ivar的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#pragma mark -取值
- (nullable id)zj_valueForKey:(NSString *)key{

// 1、判断非空
if (key == nil || key.length == 0) {
return nil;
}

// 2、找到相关方法:get<Key>、<key>、countOf<Key> 、objectIn<Key>AtIndex
// key 要大写
NSString * Key = key.capitalizedString;
// 拼接方法
NSString * getKey = [NSString stringWithFormat:@"get%@",Key];
NSString * countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
NSString * objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
return [self performSelector:NSSelectorFromString(getKey)];
}else if ([self respondsToSelector:NSSelectorFromString(key)]){
return [self performSelector:NSSelectorFromString(key)];
}
// 集合类型
else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]){
if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
for (int i = 0; i<num-1; i++) {
num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
}
for (int j = 0; j<num; j++) {
id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
[mArray addObject:objc];
}
return mArray;
}
}

#pragma clang diagnostic pop

// 3、判断是否响应`accessInstanceVariablesDirectly`方法,即间接访问实例变量,返回YES,继续下一步设值,如果是NO,则崩溃
if (![self.class accessInstanceVariablesDirectly]) {
@throw [NSException exceptionWithName:@"ZJLUnKnownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}

// 4.找相关实例变量进行赋值,顺序为:_<key>、 _is<Key>、 <key>、 is<Key>
// 4.1 定义一个收集实例变量的可变数组
NSMutableArray *mArray = [self getIvarListName];
// 例如:_name -> _isName -> name -> isName
NSString * _key = [NSString stringWithFormat:@"_%@",key];
NSString * _isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString * isKey = [NSString stringWithFormat:@"is%@",Key];
if ([mArray containsObject:_key]) {
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
return object_getIvar(self, ivar);;
}

return @"";
}

使用路由访问,即keyPath

在日常开发中,一个 类的成员变量 有可能 是自定义类或者其他的复杂数据类型,一般的操作是,我们可以先通过 KVC 获取该属性,然后再通过 KVC 获取自定义类的属性,就是比较麻烦,还有另一种比较简单的方法,就是使用 KeyPath路由,涉及一下两个方法: setValue:forKeyPath:valueForKeyPath:

1
2
3
4
// 通过KeyPath来设值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
// 通过KeyPath来取值
- (nullable id)valueForKeyPath:(NSString *)kePath;

参考如下的案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ZJPerson类
@interface ZJPerson : NSObject
@property (nonatomic, copy) NSString *age;
@property (nonatomic, strong) ZJStudent *student;
@end

// ZJStudent类
@interface ZJStudent : NSObject
@property (nonatomic, copy) NSString *name;
@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
ZJPerson *person = [[ZJPerson alloc] init];
ZJStudent *student = [ZJStudent alloc];
student.name = @"ZJ";
person.student = student;
NSLog(@"改变前:%@",[person valueForKeyPath:@"student.name"]);

// 根据kvc - keyPath路由修改student的subject属性的值
[person setValue:@"嘻嘻" forKeyPath:@"student.name"];
NSLog(@"改变后:%@",[person valueForKeyPath:@"student.name"]);
}
return 0;
}

//*************打印结果*************
2021-5-27 09:55:08.512833+0800 001-KVC简介[58089:6301894] 改变前:ZJ
2021-5-27 09:55:08.512929+0800 001-KVC简介[58089:6301894] 改变后:嘻嘻

KVC的使用场景

  • 1、动态设值和取值

    • 常用的可以通过 setValue:forKey:valueForKey:

    • 也可以通过路由的方式 setValue:forKeyPath:valueForKeyPath:

  • 2、通过KVC访问和修改私有变量

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

  • 3、多值操作(model和字典互转)

model和字典的转换可以通过下面两个KVC的API实现

1
2
3
4
5
6
// 字典转模型
- (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: 将会被传递给容器中的每一个对象,而不是对容器本身进行操作,结果会被添加到返回的容器中,这样,可以很方便的操作集合 来返回 另一个集合

如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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);
}
}

//********打印结果********
2021-5-27 11:33:43.377672+0800 CJLCustom[60035:6380757] English
2021-5-27 11:33:43.377773+0800 CJLCustom[60035:6380757] Franch
2021-5-27 11:33:43.377860+0800 CJLCustom[60035:6380757] Chinese
2021-5-27 11:33:43.378233+0800 CJLCustom[60035:6380757] 7
2021-5-27 11:33:43.378327+0800 CJLCustom[60035:6380757] 6
2021-5-27 11:33:43.378417+0800 CJLCustom[60035:6380757] 7
  • Post title:OC底层原理22:KVC底层原理分析
  • Post author:张建
  • Create time:2021-01-17 16:33:44
  • Post link:https://redefine.ohevan.com/2021/01/17/OC底层原理/OC底层原理22:KVC底层原理分析/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.