KVC
KVC(Key-Value-Coding)是Cocoa框架为我们提供的非常强大的工具,简译为键值编码。iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性,而不是在编译时确定,这也是iOS开发中的黑魔法之一。KVC依赖于RunTime,在Objective-C的动态性方面发挥了重要作用。很多高级的iOS开发技巧都是基于KVC实现的。
KVC的主要功能是直接通过变量名称字符串来访问成员变量,不管是私有的还是公有的,这也就是为什么对于Objective-C来说,没有真正的私有变量。因为一是可以利用RunTime直接获取所有成员变量,二是通过KVC对成员变量进行访问读写。
基本内容
无论是Swift还是Objective-C,KVC的定义都是对NSObject的扩展来实现的(Objective-C中有个显式的NSKeyValueCoding类别名,而Swift没有,也不需要)。所以对于所有直接或者间接继承了NSObject的类型,也就是几乎所有的Objective-C对象都能使用KVC(一些纯Swift类和结构体是不支持KVC的),下面是KVC重要的四个方法
- (id)valueForKey:(NSString *)key; // 直接通过Key来取值
- (void)setValue:(nullable id)value forKey:(NSString *)key; // 通过Key来设值
- (nullable id)valueForKeyPath:(NSString *)keyPath; // 通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; // 通过KeyPath来设值
KVC中key的查找顺序
设置值的查找顺序
1、程序优先调用
set<Key>:属性值方法,代码通过setter方法完成设置。注意,这里的<key>是指成员变量名2、如果没有找到
set<Key>:方法,KVC机制会检查+ (BOOL)accessInstanceVariablesDirectly方法有没有返回YES,默认该方法会返回YES,如果你重写了该方法让其返回NO的话,那么在这一步KVC会执行setValue:forUndefinedKey:方法。在方法返回YES的情况下,紧接着就会查找_<key>,如果没有_<key>成员变量,KVC机制会搜索_is<Key>的成员变量。3、如果该类既没有
set<Key>:方法,也没有_<key>和_is<Key>成员变量,KVC机制再会继续搜索<key>和is<Key>的成员变量。再给它们赋值。4、如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的
setValue:forUndefinedKey:方法,默认是抛出异常,如果实现了该方法,那么不会抛出异常。
具体例子分析
创建一个Person类,实现如下
@interface Person : NSObject
@end
@implementation Person
{
NSString *setName;
NSString *isName;
NSString *_name;
NSString *_isName;
NSString *name;
}
- (void)setName:(NSString *)name {
setName = name;
NSLog(@"setName: %@", name);
}
- (NSString *)getName {
NSLog(@"getName");
return setName;
}
+ (BOOL)accessInstanceVariablesDirectly {
return NO;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"setValueForUndefinedKey: %@", key);
}
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"valueForUndefinedKey: %@", key);
return nil;
}
@end
测试部分
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc]init];
[person setValue:@"newName" forKey:@"name"];
NSString *name = [person valueForKey:@"name"];
NSLog(@"name: %@", name);
}
1、将accessInstanceVariablesDirectly注释掉,运行程序,将执行setName和getName
setName: newName
getName
name: newName
也就是说先调用set<Key>:方法
2、接下来将setName方法注释掉,并且修改getName方法,将返回值修改为_name
//- (void)setName:(NSString *)name {
// setName = name;
// NSLog(@"setName: %@", name);
//}
- (NSString *)getName {
NSLog(@"getName:%@", _name);
return _name;
}
运行程序,结果如下
getName:newName
name: newName
也就是满足了第二步,如果没有找到set<Key>:方法,就会查找_<key>为其赋值
3、注释掉_name,并修改getName方法,返回_isName
@implementation Person
{
NSString *setName;
NSString *isName;
// NSString *_name;
NSString *_isName;
NSString *name;
}
- (NSString *)getName {
NSLog(@"getName:%@", _isName);
return _isName;
}
运行程序,结果如下
getName:newName
name: newName
后面2步就不再验证,有兴趣可以自己尝试。
接下来验证accessInstanceVariablesDirectly属性,代码回复到最初状态,并且将setName和getName注释
@implementation Person
{
NSString *setName;
NSString *isName;
NSString *_name;
NSString *_isName;
NSString *name;
}
//- (void)setName:(NSString *)name {
// setName = name;
// NSLog(@"setName: %@", name);
//}
//
//- (NSString *)getName {
// NSLog(@"getName");
// return setName;
//}
+ (BOOL)accessInstanceVariablesDirectly {
return NO;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"setValueForUndefinedKey: %@", key);
}
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"valueForUndefinedKey: %@", key);
return nil;
}
@end
依然运行程序,结果如下
setValueForUndefinedKey: name
valueForUndefinedKey: name
name: (null)
说明再找不到setName:方法后,不再去找name系列成员变量,而是直接调用setValue:forUndefinedKey:方法
获取值的查找顺序
1、首先按
get<Key>,<key>,is<Key>的顺序查找getter方法,找到直接调用。如果是BOOL、int等内建值类型,会做NSNumber的转换。2、如果上面的
getter没有找到,KVC则会查找countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes格式的方法。如果countOf<Key>方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子类),调用这个代理集合的方法,或者说给这个代理集合发送属于NSArray的方法,就会以countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes这几个方法组合的形式调用。还有一个可选的get<Key>:range:方法。所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名。3、还没找到,查找
countOf<Key>、enumeratorOf<Key>、memberOf<Key>格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所有方法的代理集合。4、还是没找到,如果类方法
accessInstanceVariablesDirectly返回YES。那么按_<key>,_is<Key>,<key>,is<key>的顺序搜索成员名。5、再没找到,调用
valueForUndefinedKey。
在KVC中使用keyPath
开发过程中,一个类的成员变量有可能是自定义类或其他的复杂数据类型,你可以先用KVC获取该属性,然后再次用KVC来获取这个自定义类的属性,但这样是比较繁琐的,对此,KVC提供了一个解决方案,那就是键路径keyPath
- (nullable id)valueForKeyPath:(NSString *)keyPath; // 通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; // 通过KeyPath来设值
具体例子
@interface Account : NSObject
@property (nonatomic, copy) NSString *password;
@end
@implementation Account
@end
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSNumber *age;
@property (nonatomic, copy) NSString *address;
@property (nonatomic, strong) Account *count;
@end
Example
Person *person = [[Person alloc]init];
Account *account = [[Account alloc] init];
account.password = @"xxxx";
person.account = account;
NSString *password1 = [person valueForKeyPath:@"account.password"];
[person setValue:@"yyyy" forKeyPath:@"account.password"];
NSString *password2 = [person valueForKeyPath:@"account.password"];
NSLog(@"password1 = %@, password2 = %@", password1, password2);
// password1 = xxxx, password2 = yyyy
对象关系映射
ORM(Object Relational Mapping,对象关系映射),说白了就是将JSON转换为对象。在iOS开发最初的一段时间,还没有特别好的第三方Model解析库,那时候基本上是使用NSKeyValueCoding提供的方法
Example
// Person.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSNumber *age;
@property (nonatomic, copy) NSString *address;
@end
NS_ASSUME_NONNULL_END
// Person.m
#import "Person.h"
@implementation Person
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"UndefinedKey: %@", key);
}
@end
// ViewController.m
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSDictionary *dic = @{@"name": @"xiao",
@"age": @22,
@"address": @"China",
@"phone": @"186xxxxxxxx"
};
Person *person = [[Person alloc]init];
[person setValuesForKeysWithDictionary:dic];
}
@end
对私有属性的访问
在我们使用一些系统控件时,对一些内部属性系统往往并没有暴露给我们,需要我们使用KVC进行访问。
例如,在使用UITextField时,对于设置placeholder的textColor时只能通过attributedPlaceholder这样的NSAttributedString来设置,并且每次更改都需要这样设置一遍,有些麻烦。这里可以通过KVC来获取_placeholderLabel并对其赋值,修改颜色
UITextField *textField = [[UITextField alloc] initWithFrame:CGRectMake(50, 100, 300, 44)];
textField.borderStyle = UITextBorderStyleRoundedRect;
textField.placeholder = @"please input something....";
UILabel *placeLabel = [textField valueForKey:@"_placeholderLabel"];
placeLabel.textColor = [UIColor redColor];
[self.view addSubview:textField];
效果如下图

但是需要注意:
苹果对一些系统控件的实现过程中,很多子控件使用了懒加载,即用到时才会去创建实例,所以使用KVC进行访问时,需要注意访问的时机。
例如上面,应该在placeholder设置值之后才访问,否则_placeholderLabel获取为nil,textColor设置无效。
另外一个需要注意的地方:虽然采用KVC访问一些私有成员变量不属于使用私有API,上线时不太会因此被拒绝,但是私有的成员变量可能会随着iOS版本的不同而有所变化。
所以使用KVC访问私有变量时需要谨慎。
控制是否触发setter、getter方法
有些时候为了监控某个属性的值访问情况会重写setter或getter方法,但只在特定的情况下触发,通过其他方式不触发setter或getter,我们可以通过KVC来做。例如:上面的Person类,重写name属性的setter方法,如下:
// Person.m
- (void)setName:(NSString *)name {
_name = name;
NSLog(@"setName: %@", name);
}
// ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
NSDictionary *dic = @{@"name": @"xiao",
@"age": @22,
@"address": @"China",
@"phone": @"186xxxxxxxx"
};
Person *person = [[Person alloc]init];
[person setValuesForKeysWithDictionary:dic];
[person setValue:@"zhangsan" forKey:@"_name"];
}
运行输出
undefinedKey: phone
setName: xiao
只有在 [person setValuesForKeysWithDictionary:dic];时触发过一次setName:的方法,而通过KVC给_name赋值并不会触发,如果也想触发,可以将“_name”改成“name”来实现。
[person setValue:@"zhangsan" forKey:@"name"];
这样运行输出
undefinedKey: phone
setName: xiao
setName: zhangsan
可以根据实际需求,选择使用KVC的方法,出现上面的情况是因为KVC的查找成员变量的机制。
查找成员变量的机制
如果一个实例对象用KVC来访问其成员变量,则会按照以下的顺序来进行查找,例如:我们调用的方法是:
[person setValue:@"aa" forKey:@"name"];
1、访问setName方法
2、访问_name成员变量
3、访问_isName成员变量
4、访问name成员变量
5、访问isName成员变量
以上就是KVC查找的过程,只有在某一步找到才会不继续向下查找,否则会按照上面的顺序逐个查找,如果到最后一个也找不到,那就会调用)setValue:forUndefinedKey:方法。
值得注意的是,KVC的协议NSKeyValueCoding中的accessInstanceVariablesDirectly属性:
@property (class, readonly) BOOL accessInstanceVariablesDirectly;
该属性默认为YES,如果重写返回NO,则下面这些方法都将不起作用
-valueForKey:, -setValue:forKey:, -mutableArrayValueForKey:,
-storedValueForKey:, -takeStoredValue:forKey:, and -takeValue:forKey:
也就是相当于禁止KVC的方法。但是我们在之前使用setValuesForKeysWithDictionary:方法仍然可以使用。
KVC进阶用法
KVC的使用可不仅仅是访问成员变量这么简单,苹果为KVC提供了一些高级的用法,方便开发者在代码中的使用
1、keyPath访问
对于keyPath访问很多人应该不陌生,在一些ORM库中经常会指向通过keyPath来映射赋值。
举个例子:回到刚才的textFiled的场景,我们知道有一个“_placeholderLabel”成员变量,它是一个UILabel的实例,我们获取到该UILabel,并对其进行textColor属性赋值,达到了想要的效果。但实际上可以一步完成上面的操作
UITextField *textField = [[UITextField alloc]initWithFrame:CGRectMake(100, 100, 200, 50)];
textField.placeholder = @"placeholder";
[textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
[self.view addSubview:textField];
2、集合类型的访问
集合类型包括数组、字典和集合,其中,集合还分为有序和无序。对于我们的Person类,现在需要一个属性friends:
@property(nonatomic, strong) NSMutableArray *friends;
然后在Person.m文件中会有提示下面等一系列方法
-(id)objectInFriendsAtIndex:(NSUInteger)index
-(NSArray *)friendsAtIndexes:(NSIndexSet *)indexes
-(NSUInteger)countOfFriends
这是KVC帮我们针对friends属性添加的一系列方法中的几个,表示对friends属性支持KVC集合类型,目的是方便我们使用KVC时对其进行便捷式访问。那么什么是便捷式访问呢?
假设一下,如果我们要通过KVC获取到Person对象的friends属性,并添加一个friend,如果不使用KVC集合类型访问,可能需要这样写:
//方式1
NSMutableArray *friends = [person valueForKey:@"friends"];
[friends addObject:[Person new]];
// 当然也可以使用该方法
[person.friends addObject:[Person new]];
通过KVC方式,我们可以简化方式1的操作
[[person mutableArrayValueForKey:@"friends"] addObject:[Person new]];
除了简化代码这一点,实际上这种KVC集合类型还有一个好处,就是可以对于不可变的集合类型提供安全的可变访问。
上面部分,我们的friends属性是可变数组,如果改成不可变数组NSArray,那么再对其进行添加对象,使用上面的两种方法就会不一样了,前者会直接crash,而后者则会安全访问,即使是不可变数组也可以增加数组元素。
但我们知道对于NSArray是不可变的,这在创建Person实例的时候就已经确定好了,对其添加元素,并不是不可变数组可以添加元素,而是在用KVC进行集合类型访问时,如果是不可变数组,在添加元素时会重新创建一个不可变数组对象,然后将friends属性指向新创建的不可变数组。
虽然使用这种方式不会引起奔溃,但是在创建Person类时,既然将friends属性设置为不可变数组,那么就应该避免再向其添加对象,因为这与最初的逻辑相左。
3、KVC验证
使用KVC也是有风险的,因为通过字符串去访问实例变量,虽然KVC提供类复杂的查找逻辑来帮助找到对应的成员变量,但是仍然会发生找不到的情况。
例如:我们使用-setValue:forKey来对对象进行赋值访问,当-setValue:forUndefinedKey没有实现,而且如果key不存在,将会导致奔溃。
[person setValue:@"123" forKey:@"key"];
如果想避免奔溃,实现-setValue:forUndefinedKey,打印不存在的key,或者使用try-catch,如下使用try-catch捕获异常,得到相关信息
NSDictionary *dic = @{@"name": @"xiao",
@"age": @22,
@"address": @"China"
};
Person *person = [[Person alloc]init];
[person setValuesForKeysWithDictionary:dic];
@try {
[person setValue:@"123" forKey:@"thought"];
} @catch (NSException *exception) {
NSLog(@" %@", exception.userInfo);
} @finally {
}
打印如下:
{
NSTargetObjectUserInfoKey = "<Person: 0x600000ed3840>";
NSUnknownUserInfoKey = thought;
}
NSUnknownUserInfoKey所对应的值就是不存在的key
除了上面的方法,KVC还提供了一种值验证的方法
- (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue
forKey:(NSString *)inKey
error:(out NSError * _Nullable __autoreleasing *)outError
该方法是验证值是否符合key所对应的类型,或者说值类型是否正确。方法中的ioValue参数是要赋值二级指针类型。如果不是我们想要的值,则可以直接改变ioValue的指向,也就是重新指向一个正确的值。
例如:我们现在验证Person实例的字符串属性name,因此调用验证方法来判断
UIColor *color = [UIColor yellowColor];
NSError *error = nil;
BOOL isOK = [person validateValue:&color forKeyPath:@"name" error:&error];
name属性需要接受字符串类型的值,很显然我们传一个color是错误的,然而通过运行发现验证的返回值是YES,表示验证通过,这明显不合理。
根据官方文档描述,对于属性的验证分为是否必需,默认为不必需,如果是不必需,则会直接返回YES,不会对其进行验证,而如果需要验证,则需要在Person.m中实现如下方法
-(BOOL)validateName:(id *)ioValue error:(NSError **)error {
if ([*ioValue isKindOfClass:NSString.class]) {
return YES;
}
return NO;
}
现在再去验证,会发现返回NO,符合我们的预期。
4、函数操作
同样还是对于一些集合类型的数据,我们希望可以利用共同性去做一些快捷的操作,例如求平均值和求和等,不需要再去for循环或者枚举。
例如:有一个Person类型的数组,想求所有person的age之和
NSDictionary *dic1 = @{@"name": @"1",
@"age": @22
};
NSDictionary *dic2 = @{@"name": @"2",
@"age": @21
};
NSDictionary *dic3 = @{@"name": @"3",
@"age": @23
};
Person *person1 = [[Person alloc]init];
Person *person2 = [[Person alloc]init];
Person *person3 = [[Person alloc]init];
[person1 setValuesForKeysWithDictionary:dic1];
[person2 setValuesForKeysWithDictionary:dic2];
[person3 setValuesForKeysWithDictionary:dic3];
NSArray *persons = @[person1, person2, person3];
NSNumber *sumAge = [persons valueForKeyPath:@"@sum.age"];
NSLog(@"sumAge: %@", sumAge); // 66
上面获取的就是所有人的age之和,不用for循环,直接使用KVC实现。
注意:使用的是KeyPath,并且sum前面加一个@表示是数组特有的键.
除此之外,还可以求出数组的平均值、最大值、最小值
NSNumber *count = [persons valueForKeyPath:@"@count"];
NSLog(@"count: %@", count); // 3
NSNumber *ageAve = [persons valueForKeyPath:@"@avg.age"];
NSLog(@"avg: %@", ageAve); // 22
NSNumber *maxAge = [persons valueForKeyPath:@"@max.age"];
NSLog(@"max: %@", maxAge); // 23
NSNumber *minAge = [persons valueForKeyPath:@"@min.age"];
NSLog(@"min: %@", minAge); // 21
在NSKeyValueCoding.h文件中,定义了一系列的NSKeyValueOperator,而这些Operator都是为数组类型准备的。
更多详细内容可以参考这里