【iOS开发】—— KVC
- 一. KVC的定义
- key和keyPath的区别
- 用法:
- 批量复制操作
- 字典模型相互转化
- KVC的其他方法
- KVC原理
- 赋值原理
- 取值原理
一. KVC的定义
KVC(Key-value coding)键值编码,就是指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定,这也是iOS开发中的黑魔法之一。很多高级的iOS开发技巧都是基于KVC实现的。
KVC的定义都是对NSObject的扩展来实现的,Objective-C中有个显式的NSKeyValueCoding类别名,所以对于所有继承了NSObject的类型,都能使用KVC(一些纯Swift类和结构体是不支持KVC的,因为没有继承NSObject),下面是KVC最为重要的四个方法:
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
key和keyPath的区别
- key:只能接受当前类所具有的属性,不管是自己的,还是从父类继承过来的。
- keypath:除了能接受当前类的属性,还能接受当前类属性的属性,即可以接受关系链。
用法:
key:
Person* person = [[Person alloc] init];
[person setValue:@"I am Father" forKey:@"name"];
NSLog(@"%@", [person valueForKey:@"name"]);
输出结果:
keyPath:
person.son = [[PersonSon alloc] init];
[person setValue:@"I am Son" forKeyPath:@"son.sonName"];
NSLog(@"%@", [person.son valueForKey:@"sonName"]);
输出结果:
批量复制操作
Person* personFirst = [[Person alloc] init];
[personFirst setValue:@"lcy" forKey:@"name"];
[personFirst setValue:@"20" forKey:@"age"];
[personFirst setValue:@"男" forKey:@"sex"];
NSLog(@"name = %@, age = %ld, sex = %@",personFirst.name, (long)personFirst.age, personFirst.sex);
NSDictionary* dictionary1 = [personFirst dictionaryWithValuesForKeys:@[@"name", @"age", @"sex"]];
NSLog(@"dictionary1 = %@", dictionary1);
NSDictionary* dicioinary2 = @{@"name": @"lyt", @"age": @11, @"sex": @"男"};
Person* personSecond = [[Person alloc] init];
[personSecond setValuesForKeysWithDictionary:dictioinary2];
NSLog(@"name = %@, age = %ld, sex = %@",personSecond.name, (long)personSecond.age, personSecond.sex);
输出结果:
字典模型相互转化
如果model属性和dic不匹配,可以重写方法-(void)setValue:(id)value forUndefinedKey:(NSString *)key。
重点:-(void)setValue:(id)value forUndefinedKey:(NSString *)key 方法在函数中有定义,但是没有实现需要自己来实现,从而供后面来调用。如果自己不重写的话,遇到Key不存在,且KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface StudentModel : NSObject
@property (nonatomic, strong) NSString* name;
@property (nonatomic, strong) NSString* age;
@property (nonatomic, strong) NSString* studentSex;
@end
NS_ASSUME_NONNULL_END
#import "StudentModel.h"
@implementation StudentModel
- (void) setValue:(id)value forUndefinedKey:(NSString *)key {
if ([key isEqualToString:@"sex"]) {
self.studentSex = (NSString*) value;
}
}
@end
//main函数
NSDictionary* dictionary = @{@"name": @"stu1", @"age": @66, @"sex": @"nv"};
StudentModel* model = [[StudentModel alloc] init];
[model setValuesForKeysWithDictionary:dictionary];
NSLog(@"model.name: %@", model.name);
NSLog(@"model.age: %@", model.age);
NSLog(@"model.sex: %@", model.studentSex);
NSDictionary* tempdict = [model dictionaryWithValuesForKeys:@[@"name", @"age", @"studentSex"]];
NSLog(@"tempdict = %@", tempdict);
输出结果:
KVC的其他方法
// 默认返回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;
// 使用字典为Model赋值
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
// 输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
KVC原理
赋值原理
在日常开发中,针对对象属性的赋值,一般有以下两种方式:
- 直接通过setter方法赋值;
- 通过KVC键值编码的相关API赋值;
下面针对使用最多的KVC设值方法:setValue:forKey,来进行其底层原理的探索。
当调用setValue:forKey:设置属性value时,其底层的执行流程为:
- 【第一步】首先查找是否有这三种setter方法,按照查找顺序为set:-> _set -> setIs
- 如果有其中任意一个setter方法,则直接设置属性的value(主注意:key是指成员变量名,首字符大小写需要符合KVC的命名规范)
- 如果都没有,则进入【第二步】
- 【第二步】:如果没有第一步中的三个简单的setter方法,则查找accessInstanceVariablesDirectly是否返回YES,
- 如果返回YES,则查找间接访问的实例变量进行赋值,查找顺序为:_ -> _is -> -> is
- 如果找到其中任意一个实例变量,则赋值。
- 如果都没有,则进入【第三步】
- 如果返回NO,则进入【第三步】
- 如果返回YES,则查找间接访问的实例变量进行赋值,查找顺序为:_ -> _is -> -> is
- 【第三步】如果setter方法 或者 实例变量都没有找到,系统会执行该对象的setValue:forUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常。
综上所述,KVC通过 setValue:forKey: 方法设值的流程以设置LGPerson的对象person的属性name为例,如下图所示:
取值原理
当调用valueForKey:时,其底层的执行流程如下:
- 【第一步】首先查找getter方法,按照get< Key> -> < key> -> is< Key> -> _< key> 的方法顺序查找,
- 如果找到,则进入【第五步】。
- 如果没有找到,则进入【第二步】。
- 【第二步】如果第一步中的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的标准命名方法,包括方法签名。)
- 如果没有找到这三个访问数组的,请继续进入【第三步】。
- 【第三步】如果没有找到上面的几种方法,则会同时查找 countOf < Key>,enumeratorOf< Key>和memberOf< Key> 这三个方法
-
如果这三个方法都找到,则会创建一个响应所有NSSet方法的集合代理对象,并返回该对象,此代理对象随后将其收到的所有NSSet消息转换为countOf< Key>,enumeratorOf< Key>和memberOf< Key>:消息的某种组合,用于创建它的对象
-
如果还是没有找到,则进入【第四步】
- 【第四步】如果还没有找到,检查类方法InstanceVariablesDirectly是否YES,依次搜索 _< key>,_is< Key>,< key>或is< Key> 的实例变量。
- 如果搜到,直接获取实例变量的值,进入【第五步】
- 【第五步】根据搜索到的属性值的类型,返回不同的结果
- 如果是对象指针,则直接返回结果。
- 如果是NSNumber支持的标量类型,则将其存储在NSNumber实例中并返回它。
- 如果是是NSNumber不支持的标量类型,请转换为NSValue对象并返回该对象。
- 【第六步】如果上面5步的方法均失败,系统会执行该对象的valueForUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常。
综上所述,KVC通过 valueForKey: 方法取值的流程以设置LGPerson的对象person的属性name为例,如下图所示: