关于KVC的实现原理,网上有好多相关的文章,大同小异,感觉还是形成自己的语言写下来比较好,特写此文。
KVC是什么
kvc全称是(Key-value coding)键值编码,定义在NSKeyValueCoding.h
文件中。平常我们通过setter、getter方法来设置和修改对象的属性,实际上KVC属于一种更加灵活的操作方式,这种方式我们可以通过Key名直接访问对象的属性,或者给对象的属性赋值。
协议方法
比较常用的四个方法
- (nullable 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来设值
在NSKeyValueCoding
中提供了KVC通用的访问方法,分别是getter方法valueForKey:
和setter方法setValue:forKey:
,以及其衍生的keyPath方法,这两个方法各个类通用的。并且由KVC提供默认的实现,我们也可以自己重写对应的方法来改变实现。
使用keyPath方法
forKeyPath中可以利用.运算符, 就可以一层一层往下查找对象的属性
当然 在一般的修改一个对象的属性的时候,forKey和forKeyPath,没什么区别。如:
Person *p = [Person new];
[p setValue:@"jack" forKey:@"name"];
[p setValue:@"jack" forKeyPath:@"name"];
这两个是没有区别的。
但是若是层次结构深一点, 比如说Person里面有个Dog,Dog里有个Bone,如果给Bone里面的type赋值的时候,则可以[p setValue:@"猪骨" forKeyPath:@"dog.bone.type"]
调用顺序
基础Setter搜索模式
苹果官方文档中说的是:链接大致翻译为:
-
按顺序查找名为
set<Key>:
或_set<Key>
命名的setter。 如果找到,则用输入值调用它并完成。 -
如果没有找到简单的setter,并且类方法
accessInstanceVariablesDirectly
返回YES
,则按照该顺序寻找名称类似于_<key>
,_is<Key>
,<key>
或<Key>
的实例变量。 如果找到,直接用输入值设置变量并结束 -
如果找不到
Setter
或实例变量,调用setValue:forUndefinedKey:
.并抛出异常。
基础Getter搜索模式
苹果官方文档说的是:链接大致翻译为:
-
按照该顺序搜索名为
get<Key>
,<key>
,is<Key>
或_<key>
的第一个Getter实例。 如果找到,调用它并继续执行第5步并显示结果。 否则,请继续下一步。 -
如果没有找到简单的Getter方法,则在实例中搜索名称与模式
countOf<Key>
和objectIn<Key>AtIndex:
(与NSArray类定义的原始方法相对应)和<key>AtIndexes:
(对应于NSArray方法objectsAtIndexes:
)。
如果找到其中的第一个和其他两个中的至少一个,则创建一个集合代理对象,该对象响应所有NSArray方法并返回该对象。否则,请继续执行步骤3。
代理对象随后将它接收到的任何NSArray消息转换为countOf<Key>
,objectIn<Key>AtIndex:
,和<Key> AtIndexes:
消息到创建它的键值编码兼容对象的某种组合。如果原始对象也实现了一个名为get<Key>:range:
的可选方法,则代理对象也会在适当的时候使用它。 -
如果上面的方法没有找到,那么会同时查找
countOf<Key>
,enumeratorOf<Key>
,memberOf<Key>
格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合,和上面一样,给这个代理集合发NSSet的消息,就会以countOf<Key>
,enumeratorOf<Key>
,memberOf<Key>
组合的形式调用 -
如果没有发现简单getter方法,或集合存取方法组,以及接收类方法
accessInstanceVariablesDirectly
是返回YES的。搜索一个名为_<key>
、_is<Key>
、<key>
、is<Key>
的实例,根据他们的顺序。如果发现对应的实例,则立刻获得实例可用的值并跳转到第5步,否则,跳转到第6步。 -
如果取回的是一个对象指针,则直接返回这个结果。
如果取回的是一个基础数据类型,但是这个基础数据类型是被NSNumber
支持的,则存储为NSNumber
并返回。
如果取回的是一个不支持NSNumber
的基础数据类型,则通过NSValue
进行存储并返回。 -
如果所有情况都失败,则调用
valueForUndefinedKey:
方法并抛出异常,这是默认行为。
异常处理
处理不存在的key
在使用KVC时,如果没有找到对应的key或者keyPath,则会抛出NSUndefinedKeyException
的异常,并导致程序crash,此时我们可以考虑重写setValue: forUndefinedKey:
方法与valueForUndefinedKey:
方法来避免这个问题。
处理nil值
如果属性的类型是基础类型(int,float,double等),如果将属性设置成nil,程序就会抛出NSInvalidArgumentException
的异常,并导致Crash,此时我们可以重写setNilValueForKey:
方法来处理这个问题。
属性验证
KVC提供了对key或者keyPath进行验证的方法
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;
当开发者需要验证能不能用KVC设定某个值时,可以调用validateValue: forKey:
这个方法来验证,如果这个类的开发者实现了-(BOOL)validate<Key>:error:
这个方法,那么KVC就会直接调用这个方法来返回,如果没有,就直接返回YES,注意!注意!注意(重要的事情说三遍)KVC在设值时不会主动去做验证,需要开发者手动去验证。
Example
@implementation Person
- (BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError{
NSString *name = *ioValue;
if ([name isEqualToString:@"Lucy"]) {
return YES;
}
return NO;
}
Person *add = [Person new];
NSError *error;
NSString *name = @"Jack";
if (![add validateValue:&name forKey:@"name" error:&error]) {
NSLog(@"error");
}
KVC字典转模型
将后台json数据中的字典转模型,比较好的框架有SBJSON、JSONKit、MJExtension、YYModel等。但是一些简单的数据,我们可以自己用代码来实现转换。一般我都会写NSObjec的类扩展。实现代码如下:
- (id)initWithDict:(NSDictionary *)dict{
if (self = [self init]) {
[self setValuesForKeysWithDictionary:dict];
}
return self;
}
+ (id)modelWithDict:(NSDictionary *)dict{
return [[self alloc] initWithDict:dict];
}
在使用的时候直接调用modelWithDict:
就行了,但是有个弊端就是如果找不到对应的key值,就会报错导致Crash,此时只要在对应的Model类重写setValue:forUndefinedKey:
方法即可。
网友评论