准备工作
KVC协议定义
KVC
是NSKeyValueCoding
的简写,键值编码是由NSKeyValueCoding
非正式协议启用的一种机制,对象采用该机制来提供对其属性
的间接访问。当对象符合键值编码时,其属性可通过字符串参数通过简洁、统一的消息传递接口
进行寻址
。这种间接访问机制补充了实例变量及其相关访问器方法提供的直接访问。
KVC在Objective-C中的定义
KVC
的定义都是对NSObject的扩展
来实现的,查看setValueForKey
方法,发现其在Foundation
里面,而Foundation
框架是不开源
的,只能在苹果官方文档查找。见下图:
KVC提供的API方法
- 我们可以通过官方提供的文档进行查看(文章开头有链接)
- 苹果对一些容器类比如
NSArray
或者NSSe
t等,KVC
有着特殊的实现。
常用方法
对于所有继承了NSObject
的类型,也就是几乎所有的Objective-C
对象都能使用KVC
,下面是KVC
最为重要的四个方法:
- (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
类别中还有其他的方法,当我们遇到适合的需求时,就能够派上用场了。方法如下:
// 默认返回YES,表示如果没有找到Set方法的话,会按照_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;
// 输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
KVC常用案例
-
结构体的处理
KVC
在进行结构体处理时,需要用到NSValue
,设值时,将结构体封装成NSValue
,进行键值设值
;取值同样返回NSValue
,然后按照结构体格式进行解析
,见下面代码:
// 结构体
ThreeFloats floats = {1.,2.,3.};
// 封装成NSValue
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
// 设值
[person setValue:value forKey:@"threeFloats"];
// 取值
NSValue *value1 = [person valueForKey:@"threeFloats"];
// 结构体解析
ThreeFloats th;
[value1 getValue:&th];
NSLog(@"%f-%f-%f",th.x,th.y,th.z);
-
字典处理(模型转换)
字典可以实现与模型进行装换,也可以通过键值数组从模型中获取字典数据。实现代码如下:
- (void)dictionaryTest{
// 字典
NSDictionary* dict = @{
@"name":@"Cooci",
@"nick":@"KC",
@"subject":@"iOS",
@"age":@18,
@"length":@180
};
// 模型
LGStudent *p = [[LGStudent alloc] init];
// 字典转模型
[p setValuesForKeysWithDictionary:dict];
// 键值数组
NSArray *array = @[@"name",@"age"];
// 从模型中获取响应的字典数据
NSDictionary *dic = [p dictionaryWithValuesForKeys:array];
NSLog(@"%@",dic);
}
KVC设值取值顺序
KVC
的使用相信是没什么难度的,但是它寻找key
的过程是怎么样子的呢?以下就进行分析。
设值
当调用setValue:forKey:
代码时,会有什么的内部操作呢?我在其官方文章中找到下图:
上图的意思是:
setValue:forKey:
的默认实现,给定key
和value
参数作为输入,尝试将名为key
的属性设置为value
,在接收调用的对象内部,使用以下过程:按顺序查找名为 set<Key>:
或 _set<Key>
的第一个访问器。 如果找到,则使用输入值(或根据需要展开的值)调用它并完成。如果未找到简单访问器,并且类方法 accessInstanceVariablesDirectly
返回 YES
,则按顺序查找名称类似于 _<key>
、_is<Key>
、<key>
或 is<Key>
的实例变量。 如果找到,直接使用输入值(或解包值)设置变量并完成。在未找到访问器或实例变量时,调用
setValue:forUndefinedKey:
。 默认情况下,这会引发异常,但 NSObject
的子类可能会提供特定于键的行为。
根据上面的解析可以总结为以下的几点:
- 按顺序查找名为
set<Key>
,_set<Key>
或者setIs<Key>
的setter
访问器顺序查找,如果找到就调用。只要实现任意一个
,那么就会将调用这个方法,将属性的值设为传进来的值
。 - 如果没有找到这些
setter
方法,KVC
机制会检查+ (BOOL)accessInstanceVariablesDirectly
方法有没有返回YES
,默认该方法会返回YES
,如果重写了该方法让其返回NO
的话,那么在这一步KVC
会执行setValue:forUndefinedKey:
方法。 - 如果返回
YES
,KVC
机制会优先搜索该类里面有没有名为_<Key>
的成员变量,无论该变量是在类接口处定义,还是在类实现处定义,也无论用了什么样的访问修饰符,只在存在以_<Key>
命名的变量,KVC
都可以对该成员变量赋值。 -
KVC
机制再会继续搜索_is<Key>
、<key>
和is<key>
的成员变量,再给它们赋值。 - 如果上面列出的方法或者成员变量
都不存在
,系统将会执行该对象的setValue:forUndefinedKey:
方法,默认是抛出异常。
以[person setValue:@"newName" forKey:@"name"];
为例,得出结论:
- 优先通过
setter
方法,进行属性设置,调用顺序是:setName
_setName
setIsName
- 果以上方法均未找到,并且
accessInstanceVariablesDirectly
返回YES
,则通过成员变量
进行设置,顺序是:_name
_isName
name
isName
注意:以上可以通过案例进行演示,我就不在这里演示了。
补充说明accessInstanceVariablesDirectly
尝试重写+ (BOOL)accessInstanceVariablesDirectly
方法让其返回NO
,如果KVC
没有找到set<Key>
、_set<Key>
、setIs<Key>
相关方法时,会直接用setValue:forUndefinedKey:
方法。我们用代码来测试一下上面的KVC机制:
@interface LGPerson : NSObject
{
@public
NSString *_isName;
NSString *name;
NSString *isName;
NSString *_name;
}
@end
@implementation LGPerson
+(BOOL)accessInstanceVariablesDirectly{
return NO;
}
-(id)valueForUndefinedKey:(NSString *)key{
NSLog(@"出现异常,该key不存在%@",key);
return nil;
}
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
NSLog(@"出现异常,该key不存在%@",key);
}
// 设置方法全部注释掉
// -(void)setName:(NSString*)name{
// toSetName = name;
// }
// - (void)_setName:(NSString *)name{
// NSLog(@"%s - %@",__func__,name);
// }
// - (void)setIsName:(NSString *)name{
// NSLog(@"%s - %@",__func__,name);
// }
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LGPerson* person = [LGPerson new];
[person setValue:@"NewName" forKey:@"name"];
NSString* name = [person valueForKey:@"name"];
NSLog(@"value for key : %@",name);
NSLog(@"取值_name:%@",person->_name);
NSLog(@"取值_isName:%@",person->_isName);
NSLog(@"取值name:%@",person->name);
NSLog(@"取值isName:%@",person->isName);
}
return 0;
}
运行结果:
这说明了重写+(BOOL)accessInstanceVariablesDirectly
方法让其返回NO
后,KVC
找不到set<Key>
等方法后,不再去找<Key>
系列成员变量,而是直接调用setValue:forUndefinedKey:
,如果我们自身的类不需要KVC
机制的话可以这样子写。
KVC设值流程图
取值
同理,在调用valueForKey:
时候会发生什么呢?根据官方的文档得出:
根据官方文档得出
valueForKey:
的机制如下:
- 首先按
get<Key>
,<Key>
,is<Key>
,_<Key>
的顺序方法查找getter
方法,找到的话会直接调用,如果是BOOL
或者Int
等值类型, 会将其包装成一个NSNumber
对象。 - 如果上面的
getter
没有找到,KVC
则会查找countOf<Key>
,objectIn<Key>AtIndex
或<Key>AtIndexes
格式的方法。如果countOf<Key>
方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray
所有方法的代理集合(它是NSKeyValueArray
,是NSArray
的子类),调用这个代理集合的方法,或者说给这个代理集合发送属于NSArray
的方法,就会以countOf<Key>
,objectIn<Key>AtIndex
或At<Key>Indexes
这几个方法组合的形式调用。还有一个可选的get<Key>:range:
方法。所以你想重新定义KVC
的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC
的标准命名方法,包括方法签名
。 - 如果上面的方法没有找到,那么会同时查找
countOf<Key>
,enumeratorOf<Key>
,memberOf<Key>
格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet
所的方法的代理集合,和上面一样,给这个代理集合发NSSet
的消息,就会以countOf<Key>
,enumeratorOf<Key>
,memberOf<Key>
组合的形式调用。 - 如果还没有找到,再检查类方法
+ (BOOL)accessInstanceVariablesDirectly
,如果返回YES(默认行为),那么和先前的设值一样,会按_<Key>
,_is<Key>
,<Key>
,is<Key>
的顺序搜索成员变量名,这里不推荐这么做,因为这样直接访问实例变量破坏了封装性
,使代码更脆弱
。如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly
返回NO
的话,那么会直接调用valueForUndefinedKey:
。 - 还没有找到的话,调用
valueForUndefinedKey:
以[person valueForKey:@"name"];
为例
-
getter
方法的调用顺序是:getName
name
isName
_name
- 如果以上方法没有找到,
accessInstanceVariablesDirectly
返回YES
,则直接返回成员变量,获取顺序依然是:_name
_isName
name
isName
注意:以上可以通过案例进行演示,我就不在这里演示了。
KVC取值流程图
代码验证KVC取值(需要的话就拷贝运行即可)
@interface LGPerson : NSObject
{
@public
NSString *_isName;
NSString *name;
NSString *isName;
NSString *_name;
}
@end
@implementation LGPerson
+(BOOL)accessInstanceVariablesDirectly{
return NO;
}
-(id)valueForUndefinedKey:(NSString *)key{
NSLog(@"出现异常,该key不存在%@",key);
return nil;
}
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
NSLog(@"出现异常,该key不存在%@",key);
}
// 设置方法全部注释掉
// -(void)setName:(NSString*)name{
// toSetName = name;
// }
// - (void)_setName:(NSString *)name{
// NSLog(@"%s - %@",__func__,name);
// }
// - (void)setIsName:(NSString *)name{
// NSLog(@"%s - %@",__func__,name);
// }
// 取值方法
//- (NSString *)getName{
// return NSStringFromSelector(_cmd);
//}
//- (NSString *)name{
// return NSStringFromSelector(_cmd);
//}
//- (NSString *)isName{
// return NSStringFromSelector(_cmd);
//}
//- (NSString *)_name{
// return NSStringFromSelector(_cmd);
//}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LGPerson* person = [LGPerson new];
[person setValue:@"NewName" forKey:@"name"];
NSString* name = [person valueForKey:@"name"];
NSLog(@"value for key : %@",name);
NSLog(@"取值_name:%@",person->_name);
NSLog(@"取值_isName:%@",person->_isName);
NSLog(@"取值name:%@",person->name);
NSLog(@"取值isName:%@",person->isName);
}
return 0;
}
在KVC中使用keyPath
除了对当前对象的属性
进行赋值外,还可以对其更深层
的对象进行赋值。例如,对当前对象的location
属性的country
属性进行赋值。KVC
进行多级访问时,直接类似于属性调用一样用点语法进行访问即可。
[person setValue:@"" forKeyPath:@"location.country"];
通过keyPath
对数组进行取值时,并且数组中存储的对象类型都相同
,可以通过valueForKeyPath:
方法指定取出数组中所有对象的某个字段。例如下面例子中,通过valueForKeyPath:
将数组中所有对象的name
属性值取出,并放入一个数组中返回。
NSArray *names = [array valueForKeyPath:@"name"];
异常处理
当根据KVC
搜索规则,没有搜索到对应的key
或者keyPath
,则会调用对应的异常方法。异常方法的默认实现,在异常发生时会抛出一个异常,并且应用程序Crash
。见下图:
重写以下两个方法,防止
crash
发生:
-(id)valueForUndefinedKey:(NSString *)key{
NSLog(@"出现异常,该key不存在%@",key);
return nil;
}
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
NSLog(@"出现异常,该key不存在%@",key);
}
再次运行程序,发现不再崩溃:
为了合理处理
KVC
发出的异常,我们还可以这样子处理:
- (void)setNilValueForKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
[self setValue:@"" forKey:@”age”];
} else {
[super setNilValueForKey:key];
}
}
自定义KVC的实现
根据苹果官方文档提供的设值、取值规则,我们可以自己进行KVC
的自定义实现。见下面实现代码:
// KVC 自定义
@implementation NSObject (LGKVC)
// 设置
- (void)lg_setValue:(nullable id)value forKey:(NSString *)key{
// 1: 判断什么 key
if (key == nil || key.length == 0) {
return;
}
// 2: setter set<Key>: or _set<Key>,
// key 要大写
NSString *Key = key.capitalizedString;
// 拼接方法
NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
// 是否存在方法
if ([self lg_performSelectorWithMethodName:setKey value:value]) {
NSLog(@"*********%@**********",setKey);
return;
}else if ([self lg_performSelectorWithMethodName:_setKey value:value]) {
NSLog(@"*********%@**********",_setKey);
return;
}else if ([self lg_performSelectorWithMethodName:setIsKey value:value]) {
NSLog(@"*********%@**********",setIsKey);
return;
}
// 3: 判断是否响应 accessInstanceVariablesDirectly 返回YES NO 奔溃
// 3:判断是否能够直接赋值实例变量——NO
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
// 4: 间接变量
// 获取 ivar -> 遍历 containsObjct -
// 4.1 定义一个收集实例变量的可变数组
NSMutableArray *mArray = [self getIvarListName];
// _<key> _is<Key> <key> is<Key>
// 拼接成员变量
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
// 是否存在对应的变量
if ([mArray containsObject:_key]) {
// 4.2 获取相应的 ivar
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
// 4.3 对相应的 ivar 设置值
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
object_setIvar(self , ivar, value);
return;
}
// 5:如果找不到相关实例
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}
// 取值
- (nullable id)lg_valueForKey:(NSString *)key{
// 1:刷选key 判断非空
if (key == nil || key.length == 0) {
return nil;
}
// 2:找到相关方法 get<Key> <key> countOf<Key> objectIn<Key>AtIndex
// key 要大写
NSString *Key = key.capitalizedString;
// 拼接方法
NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
return [self performSelector:NSSelectorFromString(getKey)];
}else if ([self respondsToSelector:NSSelectorFromString(key)]){
return [self performSelector:NSSelectorFromString(key)];
}else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]){
if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
for (int i = 0; i<num-1; i++) {
num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
}
for (int j = 0; j<num; j++) {
id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
[mArray addObject:objc];
}
return mArray;
}
}
#pragma clang diagnostic pop
// 3:判断是否能够直接赋值实例变量-YES、NO
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
// 4.找相关实例变量进行赋值
// 4.1 定义一个收集实例变量的可变数组
NSMutableArray *mArray = [self getIvarListName];
// _<key> _is<Key> <key> is<Key>
// _name -> _isName -> name -> isName
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
// 判断是否存在对应的成员变量
if ([mArray containsObject:_key]) {
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
return object_getIvar(self, ivar);;
}
return @"";
}
#pragma mark **- 相关方法**
- (BOOL)lg_performSelectorWithMethodName:(NSString *)methodName value:(id)value{
if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic pop
return YES;
}
return NO;
}
- (id)performSelectorWithMethodName:(NSString *)methodName{
if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [self performSelector:NSSelectorFromString(methodName) ];
#pragma clang diagnostic pop
}
return nil;
}
- (NSMutableArray *)getIvarListName{
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i<count; i++) {
Ivar ivar = ivars[i];
const char *ivarNameChar = ivar_getName(ivar);
NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
NSLog(@"ivarName == %@",ivarName);
[mArray addObject:ivarName];
}
free(ivars);
return mArray;
}
@end