目录
前言
在平时的开发中我们经常用到KVC赋值取值、字典转模型,这篇文章我们来探索一下KVC的底层原理。
KVC定义及API
KVC(Key-Value Coding)
是利用NSKeyValueCoding
非正式协议实现的一种机制,对象采用这种机制来提供对其属性的间接访问。
NSKeyValueCoding
在Foundation
框架下:
-
KVC
是通过对NSObject
的扩展来实现的 ------ 只要继承了NSObject
的类都可以使用KVC
-
NSArray、NSDictionary、NSMutableDictionary、NSOrderedSet、NSSet
等也遵守KVC
协议 -
除少数类型(结构体)以外都可以使用
KVC
KVC常用方法:
objectivec
// 通过 key 设值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
// 通过 key 取值
- (nullable id)valueForKey:(NSString *)key;
// 通过 keyPath 设值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
// 通过 keyPath 取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;
NSKeyValueCoding
类别的其它方法:
objectivec
// 默认为YES。 如果返回为YES,如果没有找到 set<Key> 方法的话, 会按照_key, _isKey, key, isKey的顺序搜索成员变量, 返回NO则不会搜索
+ (BOOL)accessInstanceVariablesDirectly;
// 键值验证, 可以通过该方法检验键值的正确性, 然后做出相应的处理
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
// 如果key不存在, 并且没有搜索到和key有关的字段, 会调用此方法, 默认抛出异常。两个方法分别对应 get 和 set 的情况
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
// setValue方法传 nil 时调用的方法
// 注意文档说明: 当且仅当 NSNumber 和 NSValue 类型时才会调用此方法
- (void)setNilValueForKey:(NSString *)key;
// 一组 key对应的value, 将其转成字典返回, 可用于将 Model 转成字典
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
KVC的使用
关于KVC的使用,其实笔者在之前已经有过很详细的分析了(详情请见博客【iOS】KVC),但是这里由于要分析KVC的源码,还是把基本的接口和用法再整理一遍
首先定义两个类方便后续使用

基本类型
对于基本类型KVC的使用,要注意NSInteger
这类的属性赋值时要转成NSNumber
或NSString

打印的结果如下:

集合类型

打印结果:

访问非对象类型------结构体
-
对于非对象类型的赋值总是把它先转成
NSValue
类型再进行存储 -
取值时转成对应类型后再使用

打印结果:

集合操作符
聚合操作符
-
@avg: 返回操作对象指定属性的平均值
-
@count: 返回操作对象指定属性的个数
-
@max: 返回操作对象指定属性的最大值
-
@min: 返回操作对象指定属性的最小值
-
@sum: 返回操作对象指定属性值之和
数组操作符
-
@distinctUnionOfObjects: 返回操作对象指定属性的集合--去重
-
@unionOfObjects: 返回操作对象指定属性的集合
嵌套操作符
-
@distinctUnionOfArrays: 返回操作对象(嵌套集合)指定属性的集合--去重,返回的是 NSArray
-
@unionOfArrays: 返回操作对象(集合)指定属性的集合
-
@distinctUnionOfSets: 返回操作对象(嵌套集合)指定属性的集合--去重,返回的是 NSSe

层层嵌套
通过forKeyPath
对实例变量(student)进行取值赋值通过forKeyPath
对实例变量(student)进行取值赋值

打印结果:

KVC底层原理
设值过程
KVC底层其实就是一个按顺序查找的过程:
-
按
set<Key>:
、_set<Key>:
顺序查找对象中是否有对应的方法 -
判断
accessInstanceVariablesDirectly
结果-
为YES时按照
_<key>
、_is<Key>
、<key>
、is<Key>
的顺序查找成员变量,找到了就赋值;找不到就跳转第3步 -
为NO时跳转第3步
-
-
调用
setValue:forUndefinedKey:
。默认情况下会引发一个异常,但是继承于NSObject
的子类可以重写该方法就可以避免崩溃并做出相应措施
取值过程
取值过程是类似的流程:
-
按照
get<Key>
、<key>
、is<Key>
、_<key>
顺序查找对象中是否有对应的方法 -
查找是否有
countOf<Key>
和objectIn<Key>AtIndex:
方法(对应于NSArray
类定义的原始方法)以及<key>AtIndexes:
方法(对应于NSArray
方法objectsAtIndexes:
)-
如果找到其中的第一个(
countOf<Key>
),再找到其他两个中的至少一个,则创建一个响应所有NSArray
方法的代理集合对象,并返回该对象(即要么是countOf<Key> + objectIn<Key>AtIndex:
,要么是countOf<Key> + <key>AtIndexes:
,要么是countOf<Key> + objectIn<Key>AtIndex: + <key>AtIndexes:
) -
如果没有找到,跳转到第3步
-
-
查找名为
countOf<Key>
、enumeratorOf<Key>
和memberOf<Key>
这三个方法(对应于NSSet
类定义的原始方法)-
如果找到这三个方法,则创建一个响应所有NSSet方法的代理集合对象,并返回该对象
-
如果没有找到,跳转到第4步
-
-
判断
accessInstanceVariablesDirectly
,为YES时按照_<key>
、_is<Key>
、<key>
、is<Key>
的顺序查找成员变量,找到了就取值 -
判断取出的属性值
-
属性值是对象,直接返回
-
属性值不是对象,但是可以转化为
NSNumber
类型,则将属性值转化为NSNumber
类型返回 -
属性值不是对象,也不能转化为
NSNumber
类型,则将属性值转化为NSValue
类型返回
-
-
调用
valueForUndefinedKey:
.默认情况下会引发一个异常,但是继承于NSObject
的子类可以重写该方法就可以避免崩溃并做出相应措施
自定义KVC
我们可以自定义KVC
setter方法

首先在头文件中加入这个方法,在.m文件中引入<objc/runtime.h>这个库
然后开始实现流程,大致流程如下:
objectivec
- (void)cj_setValue:(nullable id)value forKey:(NSString *)key {
// 1:非空判断一下
if (key == nil || key.length == 0) return;
// 2:找到相关方法 set<Key> _set<Key> setIs<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 cj_performSelectorWithMethodName:setKey value:value]) {
NSLog(@"*********%@**********",setKey);
return;
} else if ([self cj_performSelectorWithMethodName:_setKey value:value]) {
NSLog(@"*********%@**********",_setKey);
return;
} else if ([self cj_performSelectorWithMethodName:setIsKey value:value]) {
NSLog(@"*********%@**********",setIsKey);
return;
}
NSString *undefinedMethodName = @"setValue:forUndefinedKey:";
IMP undefinedIMP = class_getMethodImplementation([self class], NSSelectorFromString(undefinedMethodName));
// 3:判断是否能够直接赋值实例变量
if (![self.class accessInstanceVariablesDirectly]) {
if (undefinedIMP) {
[self cj_performSelectorWithMethodName:undefinedMethodName value:value key:key];
} else {
@throw [NSException exceptionWithName:@"TCJUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];
}
return;
}
// 4.找相关实例变量进行赋值
// 4.1 定义一个收集实例变量的可变数组
NSMutableArray *mArray = [self getIvarListName];
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
// _<key> _is<Key> <key> 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:如果找不到相关实例
if (undefinedIMP) {
[self cj_performSelectorWithMethodName:undefinedMethodName value:value key:key];
} else {
@throw [NSException exceptionWithName:@"TCJUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];
}
}
getter方法
第一步是加入库和声明方法,和setter方法相同,实现方法的过程如下:
objectivec
- (nullable id)cj_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
NSString *undefinedMethodName = @"valueForUndefinedKey:";
IMP undefinedIMP = class_getMethodImplementation([self class], NSSelectorFromString(undefinedMethodName));
// 3:判断是否能够直接赋值实例变量
if (![self.class accessInstanceVariablesDirectly]) {
if (undefinedIMP) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [self performSelector:NSSelectorFromString(undefinedMethodName) withObject:key];
#pragma clang diagnostic pop
} else {
@throw [NSException exceptionWithName:@"FXUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] 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);;
}
if (undefinedIMP) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [self performSelector:NSSelectorFromString(undefinedMethodName) withObject:key];
#pragma clang diagnostic pop
} else {
@throw [NSException exceptionWithName:@"FXUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];
}
return nil;
}
过程几个用到的方法封装如下:
//安全调用方法及传两个参数
- (BOOL)cj_performSelectorWithMethodName:(NSString *)methodName value:(id)value key:(id)key {
if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:NSSelectorFromString(methodName) withObject:value withObject:key];
#pragma clang diagnostic pop
return YES;
}
return NO;
}
//安全调用方法及传一个参数
- (BOOL)cj_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;
}
KVC异常小技巧
自动转换类型
-
用
int
类型赋值会自动转成__NSCFNumber
-
用结构体类型赋值会自动转成
NSConcreteValue
设置空值
有时候在设值时设置空值,可以通过重写setNilValueForKey
来监听,但是setNilValueForKey
只对NSNumber
类型有效
objectivec
// Int类型设置nil
[person setValue:nil forKey:@"age"];
// NSString类型设置nil
[person setValue:nil forKey:@"subject"];
@implementation TCJPerson
- (void)setNilValueForKey:(NSString *)key {
NSLog(@"设置 %@ 是空值", key);
}
@end
未定义的key
未定义的key可以用setValue:forUndefinedKey:
、valueForUndefinedKey:
来监听