KVC详解

KVC详解

1.1 前言
1
在日常的开发中,我们通常会用到KVC进行赋值,或者访问一些私有属性。那么KVC是什么,它的原理又是怎样的?接下来一起去探究分析下。
1.2 何为KVC

KVC即健值编码,是一种由NSKeyValueCoding非正式协议(非正式协议通俗叫法就是类别,官方这么叫)启用的机制。对象采用该机制来提供对其属性的间接访问。当对象符合键值编码时,其属性可通过字符串参数通过简洁、统一的消息传递接口进行寻址。

键值编码是一个基本概念,它是许多其他 Cocoa 技术的基础,例如 key-value observing, Cocoa bindings, Core Data, and AppleScript-ability。在某些情况下,键值编码还有助于简化您的代码。

1.3 KVC常用的API如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
@property (class, readonly) BOOL accessInstanceVariablesDirectly;

//直接通过Key来取值
- (nullable id)valueForKey:(NSString *)key;

//通过Key来设值
- (void)setValue:(nullable id)value forKey:(NSString *)key;

//通过KeyPath来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;

//通过KeyPath来设值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

//如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
- (nullable id)valueForUndefinedKey:(NSString *)key;

//和上一个方法一样,但这个方法是设值。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

也提供了验证,设置nil的方法

1
2
3
4
5
6
7
//KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;

//如果你在SetValue方法时面给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;

数组、字典、集合类型API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));

- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;

- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;

- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));

- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;

//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

当然这些API都在Foundation框架下NSObject的一个(NSKeyValueCoding)类别里面,其详细的内部实现原理详见官方文档中关于KVC的解释说明

1.4 KVC赋值取值的过程
1.4.1 赋值 setValue:forKey:
  • 按照顺序查找名为set<Key>或者_set<Key>的方法,如果找到就对它赋值。
  • 如果没找到,如果属性 accessInstanceVariablesDirectly 返回值为YES,就会按顺序去找实例变量_<key>,_is<Key>,<key>,或者is<Key>,找到后并对它赋值。
  • 如果没有找到就会走setValue:forUndefinedKey:方法

setValue:forKey:流程图

1.4.2 取值 valueForKey:
  • 在实例方法中搜索第一个名称为getName、name、isName或_name的访问器方法。如果找到了,就调用它
  • 如果没找到(除去集合类型),先检验类方法accessInstanceVariablesDirectly实现,并返回YES,然后依次搜索实例变量_name, _isName, name,或isName,如果找到就直接获取实例变量的值并返回
  • 如果没有找到就会走valueForUndefinedKey:方法

valueForKey 流程图

1.5 自定义KVC
1.5.1 自定义KVC赋值
  • 先判断key是否存在
  • 再按顺序判断相关的set方法:setName:->_setName->setIsName 拼接三个方法的名字,再依次调用 respondsToSelector 方法判断能不能响应,如果能响应就用 performSelector 方法执行调用
  • 如果累方法 accessInstanceVariablesDirectly 返回值为NO,则抛出异常
  • 如果返回值为YES,则先获取类的实例变量名字数组,然后根据相关的实例方法的名字按顺序判断是否在数组中,如果存在就获取实例变量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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
- (void)yc_setValue:(nullable id)value forKey:(NSString *)key {
if (key == nil || key.length == 0) {
return;
}
NSString *Key = key.capitalizedString; //首字母大写
// 拼接相关的方法名
NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];

// 按顺序判断是否实现三个实例方法
if ([self yc_performSelectorWithMethodName:setKey value:value]) {
NSLog(@"___ %@ ___",setKey);
return;
} else if ([self yc_performSelectorWithMethodName:_setKey value:value]) {
NSLog(@"___ %@ ___",_setKey);
return;
} else if ([self yc_performSelectorWithMethodName:setIsKey value:value]) {
NSLog(@"___ %@ ___",setIsKey);
return;
}

if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"yc_UnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}

// 获取所有实例变量的名字
NSMutableArray *mArray = [self getIvarListName];
// 拼接出需要的实例变量名字
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);
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;
}

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

- (BOOL)yc_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;
}

// 获取实例变量的名字
- (NSMutableArray *)getIvarListName{
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;
}
1.5.2 自定义KVC取值
  • 先判断key是否存在,不存在或者为空时,返回nil
  • 再按顺序判断相关的get实例方法:getName: -> name -> isName -> _name,先拼接出这几个方法的名字,再依次调用respondsToSelector方法判断能不能响应,如果能响应就用performSelector方法执行调用
  • 如果类方法accessInstanceVariablesDirectly返回值为NO,则抛出异常
  • 如果返回YES,则先获取类的实例变量名字数组,然后根据相关实例方法的名字按顺序判断是否在数组中,如果存在就获取实例变量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
- (nullable id)yc_valueForKey:(NSString *)key {
if (key == nil || key.length == 0) {
return nil;
}

// 首字母大写
NSString *Key = key.capitalizedString;
// 拼接 方法名字,部分和实例变量名字相同
NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
NSString *_key = [NSString stringWithFormat:@"_%@",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(isKey)]) {
return [self performSelector:NSSelectorFromString(isKey)];
} 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

// 判断是否能够直接赋值实例变量
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"YC_UnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
// 获取实例变量名字
NSMutableArray *mArray = [self getIvarListName];

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);;
}
@throw [NSException exceptionWithName:@"YC_UnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self, NSStringFromSelector(_cmd)] userInfo:nil];
return @"";
}