KVC
KVC是个啥
**KVC(key-value Coding)**即键值编码,是iOS开发中,一种可以通过键名间接访问和赋值对象属性的机制.官方文档是这么介绍的:
NSKeyValueCoding
A mechanism by which you can access the properties of an object indirectly by name or key.
Overview
The basic methods for accessing an object’s values are setValue:forKey:, which sets the value for the property identified by the specified key, and valueForKey:, which returns the value for the property identified by the specified key. Thus, all of an object’s properties can be accessed in a consistent manner.
The default implementation relies on the accessor methods normally implemented by objects (or to access instance variables directly if need be).
KVC是通过NSObject的分类NSKeyValueCoding(非正式协议)来实现的,所有继承自NSObject的类都可以使用KVC.KVC的实现本质上依赖runtime,通过向对象发送setValue:forKey消息来设置对应的属性.
KVC怎么使用
直接设值 & 取值
- 设值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
在实现了NSKeyValueCoding协议的是类都具有该方法,即继承了NSObject的类都可以使用设值的方法.
该方法的默认实现会按照如下顺序进行:
- 在接收方法的类中搜索是否实现了与模式-set<Key>相匹配的方法,如果实现了该方法,就会继续检查参数的类型:如果参数的类型不是对象指针,但是调用时传入的value为nil,会触发-setNilValueForKey: 的默认实现,抛出NSInvalidArgumentException异常,为了防止该异常可以在实现中重写该方法;如果参数的类型是对象指针类型,就会直接使用该传入的值作为参数调用该方法实现;如果传入的参数是其他类型,则在调用方法之前执行-valueForKey:完成将类型转化为NSNumber/NSValue过程.
- 没有搜索到访问器方法(即-set<Key>),如果方法接受者的类中,方法+accessInstanceVariablesDirectly方法是否返回true/YES,则按照_<key>,_is<key>,<key>, is<key>的查找顺序去查找实例变量,如果查找到这样的实例变量且其对应的类型是对象指针类型,系统会先释放掉旧值,然后对传入的value参数进行retained操作并将value值设置到查找到的变量中,如果实例变量的类型是其他类型会按照步骤一中将类型转化为NSNumber/NSValue之后,设置value到对应的变量中.
- 如果对应的防蚊器方法和实例变量都没有找到,系统会调用-setValue:forUndefinedKey:,该方法的默认实现会抛出一个NSUndefinedKeyException类型的异常,不过你可以在自定义的实现中重写该方法来覆盖原始实现.
Attention:
- 当调用- (void)setValue:(nullable id)value forKey:(NSString *)key; 方法设置属性值时,首先会查找接受者的类中是否实现了-setKey方法,查找不到的情况下会按照_key, _isKey, key, isKey的顺序去查找是否存在对应的实例变量.所以,在对应的setter方法存在时,KVC会优先调用setter方法的进行赋值.
- 如果属性/成员变量本身不是对象指针类型,那么在通过- (void)setValue:(nullable id)value forKey:(NSString *)key;方法进行赋值时,一定要注意不能将value值传入nil ,否则会导致系统抛出异常.如果传入nil的场景无法避免,则一定要在实现中重写
- (void)setNilValueForKey:(NSString *)key;
来覆盖系统的原始实现避免异常导致的执行中断.
- 取值
//取值
- (nullable id)valueForKey:(NSString *)key;
按方法的默认实现会按照如下顺序执行:
- 首先会在接受者的类中查找是否有名字符合-getKey,-key,或者isKey模式的方法,如果存在则会调用该方法.如果方法的返回值类型是对象指针类型,则会直接返回该值。如果方法返回值类型是支持被NSNumber转化的标量类型,则会返回转化之后的NSNumber对象;否则的话,会被转化为NSValue类型的返回值进行返回;
- 如果没有找到访问器方法,则会在接受者的类中寻找符合-countOf<Key>,-indexIn<Key>OfObject:, -objectIn<Key>AtIndex:(对应于NSOrderedSet类定义的原始方法),-<key>AtIndexes:(对应-[NSOrderedSet objectsAtIndexes:])模式的方法,如果查找到计算count,查找索引的方法indexOf,或者其他两个可能方法中的至少一个,则返回一个响应所有NSOrderedSet方法的集合代理对象.如果接收者的类还实现了可选方法-get:range:,则在适合最佳性能时将使用该方法。
- 然后就开始查找-countOf<Key>-enumeratorOf<Key>, -memberOf<Key>:(对应于NSSet类定义的原始方法),如果找到所有这三个方法,则返回一个响应所有NSSet方法的集合代理对象。
- 如果接受者的类中+accessInstanceVariablesDirectly是否返回true/YES,则开始依次查找接收者中是否存在_<key>, _is<Key>, <key>, 或者 is<Key>变量,如果变量存在则按照步骤一中的转化规则返回该变量的值;
- 如果以上都没有找到,则会触发-valueForUndefinedKey:方法,该方法的默认实现是抛出一个NSUndefinedKeyException类型的异常,但是你可以在需要的时候重写该方法来覆盖默认实现.
Attention:
- 当调用- (nullable id)valueForKey:(NSString *)key;方法时,会首先查找接受者的类中是否实现了-getKey,-key,或者-isKey模式的方法,如果没有才会继续查找,而变量_<key>, _is<Key>, <key>, 或者 is<Key>会在最后进行查找,如果没有才会触发-valueForUndefinedKey:方法,抛出异常.
- 如果希望KVC只能在对应的访问器方法存在时才能正常执行,否则不执行,则可以在类中重写方法:
+ (BOOL)accessInstanceVariablesDirectly {
return NO;
}
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"出现异常,该key不存在%@",key);
return nil;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"出现异常,该key不存在%@", key);
}
理解了实现原理之后,使用起来非常简单.以自定Modol对象Address来进行说明:
@interface Country : NSObject
@property (copy, nonatomic) NSString *name;
@end
@implementation Country
@end
@interface Address: NSObject
@property (nonatomic, strong) Country *country;
@property (nonatomic, copy) NSString *province;
@property (nonatomic, copy) NSString *city;
@property (nonatomic, copy) NSString *district;
@property (nonatomic, assign) CGFloat area;
@property (nonatomic, assign) CGSize size;
@end
@implementation Address
@end
在需要的时候正常调用方法即可,例如:
@implementation
int main(int argc, const char * argv[]) {
@autoreleasepool {
//生成对象
Address *add = [[Address alloc] init];
//通过KVC赋值name
[add setValue:@"China" forKey:@"country"];
//通过KVC取值name打印
NSLog(@"man的名字是%@", [add valueForKey:@"country"]);
}
return 0;
}
@end
操作字典
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
通过这两个api可以非常方便批量操作字典中的值,根据keys批量获取字典对象中的值:
Address* add = [Address new];
add.country = @"China";
add.province = @"Guang Dong";
add.city = @"Shen Zhen";
add.district = @"Nan Shan";
NSArray* arr = @[@"country",@"province",@"city",@"district"];
NSDictionary* dict = [add dictionaryWithValuesForKeys:arr]; //把对应key所有的属性全部取出来
NSLog(@"%@",dict);
也可以使用使用字典给字典对象中的对应属性赋值或者进行值替换:
//字典转模型
NSDictionary* modifyDict = @{@"country":@"USA",@"province":@"california",@"city":@"Los angle"};
[add setValuesForKeysWithDictionary:modifyDict]; //用key Value来修改Model的属性
NSLog(@"country:%@ province:%@ city:%@",add.country,add.province,add.city);
Attention
如果在批量设置值的过程中,可能会存在给不存在的key赋值或者获取不选在的key的值的场景时,一定要重写:
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
- (id)valueForUndefinedKey:(NSString *)key;
以防止出现执行异常.
keyPath
KVC除了可以直接获取属性之外还可以按照路径进行访问和设置对应的属性.
例如,通过keyPath来修改country中name属性.
int main(int argc, const char * argv[]) {
@autoreleasepool {
Address *add = [[Address alloc] init];
add.country = [[Country alloc] init];
[add setValue:@"China" forKeyPath:@"country.name"];
NSLog(@"name ---> %@", add.country.name);
}
return 0;
}
这样就很方便地通过路径修改了对应的属性值.
KVC对于标量和结构体类型的处理
在KVC的默认处理中-(id)valueForKey: (NSString *)key会自动将标量类型和结构体转化为对应的NSNumber/NSValue类型,
Address *add = [[Address alloc] init];
add.size = (CGSize){100, 500};
add.area = add.size.width * add.size.height;
id size = [add valueForKey:@"size"];
id area = [add valueForKey:@"area"];
NSLog(@"add.size == %@,add.area == %@", NSStringFromClass([size class]), NSStringFromClass([area class]));
输出结果:
add.size == NSConcreteValue,add.area == __NSCFNumber
而在调用-(void)setValue: (id)value forKey:(NSString *)key时却只能传入id类型,而不能直接使用标量类型,
Address *add = [[Address alloc] init];
CGSize size = (CGSize){100, 500};
[add setValue:[NSValue valueWithCGSize:size] forKey:@"size"];
CGFloat area =size.width * size.height;
[add setValue:@(area) forKey:@"area"];
KVC处理数据统计
- 使用KVC可以实现简单集合运算,如 @avg(平均数), @count(元素个数) , @max(最大值) , @min(最小值) ,@sum(和)等
NSMutableArray <Address *> *arr = [NSMutableArray array];
for (NSInteger index = 0; index < 10; index++) {
Address *add = [[Address alloc] init];
[add setValuesForKeysWithDictionary:@{
@"area" : @(100 + index),
}];
[arr addObject:add];
}
NSNumber* count = [arr valueForKeyPath:@"@count"];
NSLog(@"count:%f",count.floatValue);
id sunArea = [arr valueForKeyPath:@"@sum.area"];
NSLog(@"sunArea---> %@", sunArea);
id avg = [arr valueForKeyPath:@"@avg.area"];
NSLog(@"sunArea---> %@", avg);
- 元素去重:@distinctUnionOfObjects(去重后的元素),@unionOfObjects(所有元素)
例如,需要根据country属性来进行去重筛选:
NSMutableArray <Address *> *arr = [NSMutableArray array];
Country *country = [[Country alloc] init];
for (NSInteger index = 0; index < 10; index++) {
Address *add = [[Address alloc] init];
[add setValuesForKeysWithDictionary:@{
@"country":country,
@"area" : @(100 + index),
}];
[arr addObject:add];
}
id avg = [arr valueForKeyPath:@"@distinctUnionOfObjects.country"];
NSLog(@"sunArea---> %@", avg);
这样就可以按照指定的key值来对元素进行去重操作.
而使用@unionOfObjects则可以快速取出对应的属性集合,例如获取集合中所有area属性的集合:
NSMutableArray <Address *> *arr = [NSMutableArray array];
Country *country = [[Country alloc] init];
for (NSInteger index = 0; index < 10; index++) {
Address *add = [[Address alloc] init];
[add setValuesForKeysWithDictionary:@{
@"country":country,
@"area" : @(100 + index),
}];
[arr addObject:add];
}
id areas = [arr valueForKeyPath:@"@unionOfObjects.area"];
NSLog(@"areas--> %@", areas);
其他使用技巧
用KVC来访问和修改私有变量
在一些场景中,经常会需要获取一些私有的属性,使用OC无法正常访问,但是使用KVC却可以很轻松访问到.例如获取UITextField中删除按钮,然后设置自定义的按钮:
UIButton *clearButton = [self valueForKey:@"_clearButton"];
[clearButton setImage:[UIImage imageNamed:@"delete"] forState:UIControlStateNormal];
Model和字典转换
利用-(void)setValuesForKeysWithDictionary: (NSDictionary *)dictionary可以快速完成给Model赋值,
//字典转模型
Address *add = [[Address alloc] init];
NSDictionary* modifyDict = @{@"country":@"USA",@"province":@"california",@"city":@"Los angle"};
[add setValuesForKeysWithDictionary:modifyDict]; //用key Value来修改Model的属性
NSLog(@"country:%@ province:%@ city:%@",add.country,add.province,add.city);
而通过- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;也可以快速将对象转化为字典对象.
Address *add = [[Address alloc] init];
add.country = ({
Country *country = [[Country alloc] init];
country.name = @"China";
country;
});
add.province = @"浙江";
add.city = @"杭州";
add.district = @"西湖区";
add.size = (CGSize){100, 200};
add.area = 20000;
unsigned int count = 0;
Ivar *propertyList = class_copyIvarList([Address class], &count);
NSMutableArray *ivarNames = [NSMutableArray array];
for(int i = 0 ; i < count ; i ++) {
const char* propertyName = ivar_getName(propertyList[i]);
[ivarNames addObject: [NSString stringWithUTF8String: propertyName]];
}
free(propertyList);
NSDictionary *dictionary = [add dictionaryWithValuesForKeys:ivarNames];
NSLog(@"dictionary ---> %@", dictionary);
操作集合
在原生的集合实现中,对KVC的-(id)valueForKey:(NSString *)key做了特殊的实现,可以实现集合中元素统一调用元素方法:
NSArray <NSString *> *strs = @[@"I like China", @"Hello world", @"it is Objectivec"];
NSArray* arrCapStr = [strs valueForKey:@"capitalizedString"];
NSLog(@"arrCapStr --> %@", arrCapStr);
NSArray <NSNumber *> *lengths = [strs valueForKeyPath:@"capitalizedString.length"];
NSLog(@"lengths---> %@", lengths);