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都是为数组类型准备的。
更多详细内容可以参考这里