KVC

1、什么是KVC?

KVC(Key-value coding)键值编码,是指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定,这也是iOS开发中的黑魔法之一。

2、KVC相关的方法?

主要的方法:

1
2
3
4
5
6
7
8
//直接通过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;

其他的方法:

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就不这样搜索
+ (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;

3、实现原理是什么?

当调用setValue:属性值 forKey:@”name“的代码时,底层的执行机制如下:

1、程序优先调用set:属性值方法,代码通过setter方法完成设置。注意,这里的是指成员变量名,首字母大小写要符合KVC的命名规则,下同

2、如果没有找到setName:方法,KVC机制会检查+ (BOOL)accessInstanceVariablesDirectly方法有没有返回YES,默认该方法会返回YES,如果你重写了该方法让其返回NO的话,那么在这一步KVC会执行setValue:forUndefinedKey:方法,不过一般开发者不会这么做。所以KVC机制会搜索该类里面有没有名为的成员变量,无论该变量是在类接口处定义,还是在类实现处定义,也无论用了什么样的访问修饰符,只在存在以命名的变量,KVC都可以对该成员变量赋值。

3、如果该类即没有set:方法,也没有_成员变量,KVC机制会搜索_is的成员变量。

4、和上面一样,如果该类即没有set:方法,也没有_和_is成员变量,KVC机制再会继续搜索和is的成员变量。再给它们赋值。

5、如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的setValue:forUndefinedKey:方法,默认是抛出异常。

当调用valueForKey:@”name“的代码时,KVC对key的搜索方式不同于setValue:属性值 forKey:@”name“,其搜索方式如下:

1、首先按get,,is的顺序方法查找getter方法,找到的话会直接调用。如果是BOOL或者Int等值类型, 会将其包装成一个NSNumber对象。

2、如果上面的getter没有找到,KVC则会查找countOf,objectInAtIndex或AtIndexes格式的方法。如果countOf方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子类),调用这个代理集合的方法,或者说给这个代理集合发送属于NSArray的方法,就会以countOf,objectInAtIndex或AtIndexes这几个方法组合的形式调用。还有一个可选的get:range:方法。所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名。

3、如果上面的方法没有找到,那么会同时查找countOf,enumeratorOf,memberOf格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合,和上面一样,给这个代理集合发NSSet的消息,就会以countOf,enumeratorOf,memberOf组合的形式调用。

可能上面的两条查找方案不好理解,简单来说就是如果你在自己的类自定义了KVC的实现,并且实现了上面的方法,那么你可以将返回的对象当数组(NSArray)用

4、如果还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),那么和先前的设值一样,会按_,_is,,is的顺序搜索成员变量名,这里不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱。如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly返回NO的话,那么会直接调用valueForUndefinedKey:

5、还没有找到的话,调用valueForUndefinedKey:

大致实现原理如下:

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
@interface NSObject(MYKVC)
-(void)setMyValue:(id)value forKey:(NSString*)key;
-(id)myValueforKey:(NSString*)key;

@end
@implementation NSObject(MYKVC)
-(void)setMyValue:(id)value forKey:(NSString *)key{
if (key == nil || key.length == 0) { //key名要合法
return;
}
if ([value isKindOfClass:[NSNull class]]) {
[self setNilValueForKey:key]; //如果需要完全自定义,那么这里需要写一个setMyNilValueForKey,但是必要性不是很大,就省略了
return;
}
if (![value isKindOfClass:[NSObject class]]) {
@throw @"must be s NSObject type";
return;
}

NSString* funcName = [NSString stringWithFormat:@"set%@:",key.capitalizedString];
if ([self respondsToSelector:NSSelectorFromString(funcName)]) { //默认优先调用set方法
[self performSelector:NSSelectorFromString(funcName) withObject:value];
return;
}
unsigned int count;
BOOL flag = false;
Ivar* vars = class_copyIvarList([self class], &count);
for (NSInteger i = 0; i<count; i++) {
Ivar var = vars[i];
NSString* keyName = [[NSString stringWithCString:ivar_getName(var) encoding:NSUTF8StringEncoding] substringFromIndex:1];

if ([keyName isEqualToString:[NSString stringWithFormat:@"_%@",key]]) {
flag = true;
object_setIvar(self, var, value);
break;
}


if ([keyName isEqualToString:key]) {
flag = true;
object_setIvar(self, var, value);
break;
}
}
if (!flag) {
[self setValue:value forUndefinedKey:key];//如果需要完全自定义,那么这里需要写一个self setMyValue:value forUndefinedKey:key,但是必要性不是很大,就省略了
}
}

-(id)myValueforKey:(NSString *)key{
if (key == nil || key.length == 0) {
return [NSNull new]; //其实不能这么写的
}
//这里为了更方便,我就不做相关集合的方法查询了
NSString* funcName = [NSString stringWithFormat:@"gett%@:",key.capitalizedString];
if ([self respondsToSelector:NSSelectorFromString(funcName)]) {
return [self performSelector:NSSelectorFromString(funcName)];
}

unsigned int count;
BOOL flag = false;
Ivar* vars = class_copyIvarList([self class], &count);
for (NSInteger i = 0; i<count; i++) {
Ivar var = vars[i];
NSString* keyName = [[NSString stringWithCString:ivar_getName(var) encoding:NSUTF8StringEncoding] substringFromIndex:1];
if ([keyName isEqualToString:[NSString stringWithFormat:@"_%@",key]]) {
flag = true;
return object_getIvar(self, var);
break;
}
if ([keyName isEqualToString:key]) {
flag = true;
return object_getIvar(self, var);
break;
}
}
if (!flag) {
[self valueForUndefinedKey:key];//如果需要完全自定义,那么这里需要写一个self myValueForUndefinedKey,但是必要性不是很大,就省略了
}
return [NSNull new]; //其实不能这么写的
}
@end

4、KVC 如何处理异常?

KVC中最常见的异常就是不小心使用了错误的key,或者在设值中不小心传递了nil的值,KVC中有专门的方法来处理这些异常。

通常在用KVC操作Model时,抛出异常的那两个方法是需要重写的。虽然一般很小出现传递了错误的Key值这种情况,但是如果不小心出现了,直接抛出异常让APP崩溃显然是不合理的。一般在这里直接让这个key打印出来即可,或者有些特殊情况需要特殊处理。通常情况下,KVC不允许你要在调用setValue:属性值 forKey:@”name“(或者keyPath)时对非对象传递一个nil的值。很简单,因为值类型是不能为nil的。如果你不小心传了,KVC会调用setNilValueForKey:方法。这个方法默认是抛出异常,所以一般而言最好还是重写这个方法。

1
2
3
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
- (void)setNilValueForKey:(NSString *)key;

5、KVC如何处理非对象和自定义对象 ?

不是每一个方法都返回对象,但是valueForKey:总是返回一个id对象,如果原本的变量类型是值类型或者结构体,返回值会封装成NSNumber或者NSValue对象。这两个类会处理从数字,布尔值到指针和结构体任何类型。然后开发者需要手动转换成原来的类型。尽管valueForKey:会自动将值类型封装成对象,但是setValue:forKey:却不行。你必须手动将值类型转换成NSNumber或者NSValue类型,才能传递过去。

对于自定义对象,KVC也会正确地设值和取值。因为传递进去和取出来的都是id类型,所以需要开发者自己担保类型的正确性,运行时Objective-C在发送消息时会检查类型,如果错误会直接抛出异常。

6、KVC处理容器类?

对象的属性可以是一对一的,也可以是一对多的。一对多的属性要么是有序的(数组),要么是无序的(集合)。

不可变的有序容器属性(NSArray)和无序容器属性(NSSet)一般可以使用valueForKey:来获取。比如有一个叫items的NSArray属性,你可以用valurForKey:@”items”来获取这个属性。前面valueForKey:的key搜索模式中,我们发现其实KVC使用了一种更灵活的方式来管理容器类。苹果的官方文档也推荐我们实现这些这些特殊的访问器。
而当对象的属性是可变的容器时,对于有序的容器,可以用下面的方法:

(NSMutableArray *)mutableArrayValueForKey:(NSString *)key;```
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

该方法返回一个可变有序数组,如果调用该方法,KVC的搜索顺序如下:

1、搜索insertObject:in<Key>AtIndex: , removeObjectFrom<Key>AtIndex: 或者 insert<Key>AdIndexes , remove<Key>AtIndexes 格式的方法
如果至少找到一个insert方法和一个remove方法,那么同样返回一个可以响应NSMutableArray所有方法代理集合(类名是NSKeyValueFastMutableArray2),那么给这个代理集合发送NSMutableArray的方法,以insertObject:in<Key>AtIndex: , removeObjectFrom<Key>AtIndex: 或者 insert<Key>AdIndexes , remove<Key>AtIndexes组合的形式调用。还有两个可选实现的接口:replaceOnjectAtIndex:withObject:,replace<Key>AtIndexes:with<Key>:。

2、如果上步的方法没有找到,则搜索set<Key>: 格式的方法,如果找到,那么发送给代理集合的NSMutableArray最终都会调用set<Key>:方法。 也就是说,mutableArrayValueForKey:取出的代理集合修改后,用set<Key>: 重新赋值回去去。这样做效率会低很多。所以推荐实现上面的方法。

3、如果上一步的方法还还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),会按_<key>,<key>,的顺序搜索成员变量名,如果找到,那么发送的NSMutableArray消息方法直接交给这个成员变量处理。

4、如果还是找不到,则调用valueForUndefinedKey:。


而对于无序的容器,可以用下面的方法:

- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
该方法返回一个可变的无序数组如果调用该方法,KVC的搜索顺序如下:

1、搜索addObject<Key>Object: , remove<Key>Object: 或者 add<Key> , remove<Key> 格式的方法
如果至少找到一个insert方法和一个remove方法,那么同样返回一个可以响应NSMutableSet所有方法代理集合(类名是NSKeyValueFastMutableSet2),那么给这个代理集合发送NSMutableSet的方法,以addObject<Key>Object: , remove<Key>Object: 或者 add<Key> , remove<Key>组合的形式调用。还有两个可选实现的接口:intersect<Key> , set<Key>: 。
如果receiver是ManagedObject,那么就不会继续搜索。

2、如果上一步的方法没有找到,则搜索set<Key>: 格式的方法,如果找到,那么发送给代理集合的NSMutableSet最终都会调用set<Key>:方法。 也就是说,mutableSetValueForKey取出的代理集合修改后,用set<Key>: 重新赋值回去去。这样做效率会低很多。所以推荐实现上面的方法。

3、如果上一步的方法还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),会按_<key>,<key>的顺序搜索成员变量名,如果找到,那么发送的NSMutableSet消息方法直接交给这个成员变量处理。

4、如果还是找不到,调用valueForUndefinedKey:
可见,除了检查receiver是ManagedObject以外,其搜索顺序和mutableArrayValueForKey基本一致。

# 7、KVC 如何处理字典?

当对NSDictionary对象使用KVC时,valueForKey:的表现行为和objectForKey:一样。所以使用valueForKeyPath:用来访问多层嵌套的字典是比较方便的。
KVC里面还有两个关于NSDictionary的方法

//是指输入一组key,返回这组key对应的属性,再组成一个字典。

  • (NSDictionary )dictionaryWithValuesForKeys:(NSArray<NSString > *)keys;
    //是用来修改Model中对应key的属性,字典里传属性名和值
  • (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;

    1
    2
    3
    4
    5


    # 8、KVC校验值得正确性?

    KVC提供了属性值,用来验证key对应的Value是否可用的方法
  • (BOOL)validateValue:(inout id nullable * nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

    1
    2

    这个方法的默认实现是去探索类里面是否有一个这样的方法:`-(BOOL)validate<Key>:error:`如果有这个方法,就调用这个方法来返回,没有的话就直接返回YES

@implementation Address
-(BOOL)validateCountry:(id )value error:(out NSError _Nullable __autoreleasing )outError{ //在implementation里面加这个方法,它会验证是否设了非法的value
NSString
country = *value;
country = country.capitalizedString;
if ([country isEqualToString:@”Japan”]) {
return NO; //如果国家是日本,就返回NO,这里省略了错误提示,
}
return YES;
}
@end

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

如上面的代码,当开发者需要验证能不能用KVC设定某个值时,可以调用validateValue: forKey:这个方法来验证,如果这个类的开发者实现了-(BOOL)validate<Key>:error:这个方法,那么KVC就会直接调用这个方法来返回,如果没有,就直接返回YES,注意,KVC在设值时不会主动去做验证,需要开发者手动去验证。所以即使你在类里面写了验证方法,但是KVC因为不会去主动验证,所以还是能够设值成功。

# 9、具体使用场景有哪些?

1、动态地取值和设值
利用KVC动态的取值和设值是最基本的用途了

2、用KVC来访问和修改私有变量
对于类里的私有属性,Objective-C是无法直接访问的,但是KVC是可以的。

3、Model和字典转换

充分地运用KVC和Objc的runtime组合的技巧,可以实现很多功能,比如给Model实现统一的description用于打印model

4、修改一些控件的内部属性

很多UI控件都由很多内部UI控件组合而成的,但是Apple度没有提供这访问这些控件的API,这样我们就无法正常地访问和修改这些控件的样式。而KVC在大多数情况可下可以解决这个问题。最常用的就是个性化UITextField中的placeHolderText相关属性,颜色啥的。
当日具体属性名称可以通过Runtime相关方法来获取

5、更方便的操作集合

Apple对KVC的valueForKey:方法作了一些特殊的实现,比如说NSArray和NSSet这样的容器类就实现了这些方法。所以可以用KVC很方便地操作集合
比如 求和、首字母变大写等等。。

用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);
}
打印结果
2016-04-20 16:29:14.239 KVCDemo[1356:118667] English
2016-04-20 16:29:14.240 KVCDemo[1356:118667] Franch
2016-04-20 16:29:14.240 KVCDemo[1356:118667] Chinese
2016-04-20 16:29:14.240 KVCDemo[1356:118667] 7
2016-04-20 16:29:14.241 KVCDemo[1356:118667] 6
2016-04-20 16:29:14.241 KVCDemo[1356:118667] 7

1
2
3
4
5
6
7
8
方法capitalizedString被传递到NSArray中的每一项,这样,NSArray的每一员都会执行capitalizedString并返回一个包含结果的新的NSArray。从打印结果可以看出,所有String都成功以转成了大写。
同样如果要执行多个方法也可以用valueForKeyPath:方法。它先会对每一个成员调用 capitalizedString方法,然后再调用length,因为lenth方法返回是一个数字,所以返回结果以NSNumber的形式保存在新数组里

用KVC中的函数操作集合

KVC同时还提供了很复杂的函数,主要有下面这些
①简单集合运算符
简单集合运算符共有 `@avg, @count , @max , @min ,@sum5` 种,目前还不支持自定义。

@interface Book : NSObject
@property (nonatomic,copy) NSString* name;
@property (nonatomic,assign) CGFloat price;
@end
@implementation Book
@end

Book book1 = [Book new];
book1.name = @”The Great Gastby”;
book1.price = 22;
Book
book2 = [Book new];
book2.name = @”Time History”;
book2.price = 12;
Book *book3 = [Book new];
book3.name = @”Wrong Hole”;
book3.price = 111;

Book *book4 = [Book new];
book4.name = @”Wrong Hole”;
book4.price = 111;

NSArray arrBooks = @[book1,book2,book3,book4];
NSNumber
sum = [arrBooks valueForKeyPath:@”@sum.price”];
NSLog(@”sum:%f”,sum.floatValue);
NSNumber avg = [arrBooks valueForKeyPath:@”@avg.price”];
NSLog(@”avg:%f”,avg.floatValue);
NSNumber
count = [arrBooks valueForKeyPath:@”@count”];
NSLog(@”count:%f”,count.floatValue);
NSNumber min = [arrBooks valueForKeyPath:@”@min.price”];
NSLog(@”min:%f”,min.floatValue);
NSNumber
max = [arrBooks valueForKeyPath:@”@max.price”];
NSLog(@”max:%f”,max.floatValue);

打印结果
2016-04-20 16:45:54.696 KVCDemo[1484:127089] sum:256.000000
2016-04-20 16:45:54.697 KVCDemo[1484:127089] avg:64.000000
2016-04-20 16:45:54.697 KVCDemo[1484:127089] count:4.000000
2016-04-20 16:45:54.697 KVCDemo[1484:127089] min:12.000000
2016-04-20 16:45:54.697 KVCDemo[1484:127089] max:111.000000

1
2
3
4
5
6
7

②对象运算符
比集合运算符稍微复杂,能以数组的方式返回指定的内容,一共有两种:
@distinctUnionOfObjects
@unionOfObjects
它们的返回值都是NSArray,区别是前者返回的元素都是唯一的,是去重以后的结果;后者返回的元素是全集。
用法如下:

NSLog(@”distinctUnionOfObjects”);
NSArray arrDistinct = [arrBooks valueForKeyPath:@”@distinctUnionOfObjects.price”];
for (NSNumber
price in arrDistinct) {
NSLog(@”%f”,price.floatValue);
}
NSLog(@”unionOfObjects”);
NSArray arrUnion = [arrBooks valueForKeyPath:@”@unionOfObjects.price”];
for (NSNumber
price in arrUnion) {
NSLog(@”%f”,price.floatValue);
}

2016-04-20 16:47:34.490 KVCDemo[1522:128840] distinctUnionOfObjects
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 111.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 12.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 22.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] unionOfObjects
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 22.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 12.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 111.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 111.000000
`

前者会将重复的价格去除后返回所有价格,后者直接返回所有的图书价格。(因为只返回价格,没有返回图书,感觉用处不大。)
③Array和Set操作符
这种情况更复杂了,说的是集合中包含集合的情况,我们执行了如下的一段代码:
@distinctUnionOfArrays
@unionOfArrays
@distinctUnionOfSets
@distinctUnionOfArrays:该操作会返回一个数组,这个数组包含不同的对象,不同的对象是在从关键路径到操作器右边的被指定的属性里
@unionOfArrays 该操作会返回一个数组,这个数组包含的对象是在从关键路径到操作器右边的被指定的属性里和@distinctUnionOfArrays不一样,重复的对象不会被移除
@distinctUnionOfSets 和@distinctUnionOfArrays类似。因为Set本身就不支持重复。

10、参考资料

1、iOS开发技巧系列—详解KVC