写在前面
平常开发中经常用到KVC赋值取值、字典转模型,但KVC的底层原理又是怎样的呢?
Demo
一、KVC初探
1.KVC定义及API
KVC(Key-Value Coding)
是利用
NSKeyValueCoding
非正式协议实现的一种机制,对象采用这种机制来提供对其属性的间接访问
写下KVC代码并点击跟进
setValue
会发现
NSKeyValueCoding
是在
Foundation
框架下
- KVC通过对
的扩展来实现的——所有集成了NSObject
的类可以使用KVCNSObject
-
等也遵守KVC协议NSArray、NSDictionary、NSMutableDictionary、NSOrderedSet、NSSet
- 除少数类型(结构体)以外都可以使用KVC
int main(int argc, const char * argv[]) { @autoreleasepool { FXPerson *person = [FXPerson new]; [person setValue:@"Felix" forKey:@"name"]; [person setValue:@"Felix" forKey:@"nickname"]; } return 0; } 复制代码
KVC
常用方法,这些也是我们在日常开发中经常用到的
// 通过 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
类别的其它方法
// 默认为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; 复制代码
2.拓展——自动生成的setter和getter方法
试想一下编译器要为成千上万个属性分别生成
setter
和
getter
方法那不得歇菜了嘛
于是乎苹果开发者们就运用
通用原则
给所有属性都提供了同一个入口——
objc-accessors.mm
中
setter
方法根据
修饰符不同
调用不同方法,最后统一调用
reallySetProperty
方法

来到
reallySetProperty
再根据内存偏移量取出属性,根据修饰符完成不同的操作
- 在第一个属性
赋值时,此时的内存偏移量为8,刚好偏移name
所占内存(8字节)来到isa
name
- 在第二个属性
赋值时,此时的内存偏移量为16,刚好偏移nickname
所占内存(8+8)来到isa、name
nickname
至于是哪里调用的
objc_setProperty_nonatomic_copy
?
并不是在objc源码中,而在llvm源码中发现了它,根据它一层层找上去就能找到源头
二、KVC使用
相信大部分阅读本文的小伙伴们都对KVC的使用都比较了解了,但笔者建议还是看一下查漏补缺
typedef struct { float x, y, z; } ThreeFloats; @interface FXPerson : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) NSInteger age; @property (nonatomic, copy) NSArray *family; @property (nonatomic) ThreeFloats threeFloats; @property (nonatomic, strong) FXFriend *friends; @end @interface FXFriend : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) NSInteger age; @end 复制代码
1.基本类型
注意一下NSInteger这类的属性赋值时要转成NSNumber或NSString
FXPerson *person = [FXPerson new]; [person setValue:@"Felix" forKey:@"name"]; [person setValue:@(18) forKey:@"age"]; NSLog(@"名字%@ 年龄%@", [person valueForKey:@"name"], [person valueForKey:@"age"]); 复制代码
打印结果:
2020-03-08 14:06:20.913692+0800 FXDemo[2998:151140] 名字Felix 年龄18 复制代码
2.集合类型
两种方法对数组进行赋值,更推荐使用第二种方法
FXPerson *person = [FXPerson new]; person.family = @[@"FXPerson", @"FXFather"]; // 直接用新的数组赋值 NSArray *temp = @[@"FXPerson", @"FXFather", @"FXMother"]; [person setValue:temp forKey:@"family"]; NSLog(@"第一次改变%@", [person valueForKey:@"family"]); // 取出数组以可变数组形式保存,再修改 NSMutableArray *mTemp = [person mutableArrayValueForKeyPath:@"family"]; [mTemp addObject:@"FXChild"]; NSLog(@"第二次改变%@", [person valueForKey:@"family"]); 复制代码
打印结果:
2020-03-08 14:06:20.913794+0800 FXDemo[2998:151140] 第一次改变( FXPerson, FXFather, FXMother ) 2020-03-08 14:06:20.913945+0800 FXDemo[2998:151140] 第二次改变( FXPerson, FXFather, FXMother, FXChild ) 复制代码
3.访问非对象类型——结构体
- 对于非对象类型的赋值总是把它先转成NSValue类型再进行存储
- 取值时转成对应类型后再使用
FXPerson *person = [FXPerson new]; // 赋值 ThreeFloats floats = {180.0, 180.0, 18.0}; NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)]; [person setValue:value forKey:@"threeFloats"]; NSLog(@"非对象类型%@", [person valueForKey:@"threeFloats"]); // 取值 ThreeFloats th; NSValue *currentValue = [person valueForKey:@"threeFloats"]; [currentValue getValue:&th]; NSLog(@"非对象类型的值%f-%f-%f", th.x, th.y, th.z); 复制代码
打印结果:
2020-03-08 14:06:20.914088+0800 FXDemo[2998:151140] 非对象类型{length = 12, bytes = 0x000034430000344300009041} 2020-03-08 14:06:20.914182+0800 FXDemo[2998:151140] 非对象类型的值180.000000-180.000000-18.000000 2020-03-08 14:06:20.914333+0800 FXDemo[2998:151140] ( 18, 19, 20, 21, 22, 23 ) 复制代码
4.集合操作符
- 聚合操作符
-
: 返回操作对象指定属性的平均值@avg
-
: 返回操作对象指定属性的个数@count
-
: 返回操作对象指定属性的最大值@max
-
: 返回操作对象指定属性的最小值@min
-
: 返回操作对象指定属性值之和@sum
-
- 数组操作符
-
: 返回操作对象指定属性的集合--去重@distinctUnionOfObjects
-
: 返回操作对象指定属性的集合@unionOfObjects
-
- 嵌套操作符
-
: 返回操作对象(嵌套集合)指定属性的集合--去重,返回的是 NSArray@distinctUnionOfArrays
-
: 返回操作对象(集合)指定属性的集合@unionOfArrays
-
: 返回操作对象(嵌套集合)指定属性的集合--去重,返回的是 NSSet@distinctUnionOfSets
-
集合操作符用得少之又少。下面举个
FXPerson *person = [FXPerson new]; NSMutableArray *friendArray = [NSMutableArray array]; for (int i = 0; i < 6; i++) { FXFriend *f = [FXFriend new]; NSDictionary* dict = @{ @"name":@"Felix", @"age":@(18+i), }; [f setValuesForKeysWithDictionary:dict]; [friendArray addObject:f]; } NSLog(@"%@", [friendArray valueForKey:@"age"]); float avg = [[friendArray valueForKeyPath:@"@avg.age"] floatValue]; NSLog(@"平均年龄%f", avg); int count = [[friendArray valueForKeyPath:@"@count.age"] intValue]; NSLog(@"调查人口%d", count); int sum = [[friendArray valueForKeyPath:@"@sum.age"] intValue]; NSLog(@"年龄总和%d", sum); int max = [[friendArray valueForKeyPath:@"@max.age"] intValue]; NSLog(@"最大年龄%d", max); int min = [[friendArray valueForKeyPath:@"@min.age"] intValue]; NSLog(@"最小年龄%d", min); 复制代码
打印结果:
2020-03-08 14:06:20.914503+0800 FXDemo[2998:151140] 平均年龄20.500000 2020-03-08 14:06:20.914577+0800 FXDemo[2998:151140] 调查人口6 2020-03-08 14:06:20.914652+0800 FXDemo[2998:151140] 年龄总和123 2020-03-08 14:06:20.914739+0800 FXDemo[2998:151140] 最大年龄23 2020-03-08 14:06:20.914832+0800 FXDemo[2998:151140] 最小年龄18 复制代码
5.层层嵌套
通过
forKeyPath
对实例变量(friends)进行取值赋值
FXPerson *person = [FXPerson new]; FXFriend *f = [[FXFriend alloc] init]; f.name = @"Felix的朋友"; f.age = 18; person.friends = f; [person setValue:@"Feng" forKeyPath:@"friends.name"]; NSLog(@"%@", [person valueForKeyPath:@"friends.name"]); 复制代码
打印结果:
2020-03-08 14:06:20.914927+0800 FXDemo[2998:151140] Feng 复制代码
三、KVC底层原理
由于
NSKeyValueCoding
的实现在
Foundation
框架,但它又不开源,我们只能通过KVO官方文档来了解它
1.设值过程
官方文档上对Setter方法的过程进行了这样一段讲解
- 按
、set<Key>:
顺序查找对象中是否有对应的方法_set<Key>:
- 找到了直接调用设值
- 没有找到跳转第2步
- 判断
结果accessInstanceVariablesDirectly
- 为YES时按照
、_<key>
、_is<Key>
、<key>
的顺序查找成员变量,找到了就赋值;找不到就跳转第3步is<Key>
- 为NO时跳转第3步
- 调用
。默认情况下会引发一个异常,但是继承于setValue:forUndefinedKey:
的子类可以重写该方法就可以避免崩溃并做出相应措施NSObject
2.取值过程
同样的官方文档上也给出了Getter方法的过程
- 按照
、get<Key>
、<key>
、is<Key>
顺序查找对象中是否有对应的方法_<key>
- 如果有则调用getter,执行第5步
- 如果没有找到,跳转到第2步
- 查找是否有
和countOf<Key>
方法(对应于objectIn<Key>AtIndex:
类定义的原始方法)以及NSArray
方法(对应于<key>AtIndexes:
方法NSArray
)objectsAtIndexes:
- 如果找到其中的第一个
,再找到其他两个中的至少一个,则创建一个响应所有 NSArray方法的代理集合对象,并返回该对象(即要么是(countOf<Key>)
,要么是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>
- 为NO时跳转第6步
- 判断取出的属性值
- 属性值是对象,直接返回
- 属性值不是对象,但是可以转化为
类型,则将属性值转化为NSNumber
类型返回NSNumber
- 属性值不是对象,也不能转化为
类型,则将属性值转化为NSNumber
类型返回NSValue
- 调用
。默认情况下会引发一个异常,但是继承于valueForUndefinedKey:
的子类可以重写该方法就可以避免崩溃并做出相应措施NSObject
四、自定义KVC
根据KVC的设值过程、取值过程,我们可以自定义KVC的setter方法和getter方法,但是这一切都是根据官方文档做出的猜测,自定义KVC只能在一定程度上取代系统KVC,大致流程几乎一致:实现了 setValue:forUndefinedKey: 、 valueForUndefinedKey: 的调用,且 accessInstanceVariablesDirectly 无论为true为false,都能保持两次调用
新建一个
NSObject+FXKVC
的分类,.h开放两个方法,.m引入
<objc/runtime.h>
-
- (void)fx_setValue:(nullable id)value forKey:(NSString *)key;
-
- (nullable id)fx_valueForKey:(NSString *)key;
1.自定义setter方法
- 非空判断
if (key == nil || key.length == 0) return; 复制代码
- 找到相关方法
、set<Key>
、_set<Key>
,若存在就直接调用setIs<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 fx_performSelectorWithMethodName:setKey value:value]) { NSLog(@"*********%@**********",setKey); return; } else if ([self fx_performSelectorWithMethodName:_setKey value:value]) { NSLog(@"*********%@**********",_setKey); return; } else if ([self fx_performSelectorWithMethodName:setIsKey value:value]) { NSLog(@"*********%@**********",setIsKey); return; } 复制代码
- 判断是否能够直接赋值实例变量,不能的情况下就调用
或抛出异常setValue:forUndefinedKey:
NSString *undefinedMethodName = @"setValue:forUndefinedKey:"; IMP undefinedIMP = class_getMethodImplementation([self class], NSSelectorFromString(undefinedMethodName)); if (![self.class accessInstanceVariablesDirectly]) { if (undefinedIMP) { [self fx_performSelectorWithMethodName:undefinedMethodName value:value key:key]; } 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; } 复制代码
- 找相关实例变量进行赋值
NSMutableArray *mArray = [self getIvarListName]; 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); 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; } 复制代码
- 调用
或抛出异常setValue:forUndefinedKey:
if (undefinedIMP) { [self fx_performSelectorWithMethodName:undefinedMethodName value:value key:key]; } 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]; } 复制代码
在这里笔者存在一个疑问:没有实现setValue:forUndefinedKey:时,当前类可以响应respondsToSelector这个方法,但是直接performSelector会崩溃,所以改用了判断IMP是否为空
2.自定义getter方法
- 非空判断
if (key == nil || key.length == 0) return nil; 复制代码
- 找相关方法
、get<Key>
,找到就返回(这里使用<key>
消除警告)-Warc-performSelector-leaks
NSString *Key = key.capitalizedString; NSString *getKey = [NSString stringWithFormat:@"get%@",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)]; } #pragma clang diagnostic pop 复制代码
- 对
进行操作:查找NSArray
、countOf<Key>
方法objectIn<Key>AtIndex
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(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 复制代码
- 判断是否能够直接赋值实例变量,不能的情况下就调用
或抛出异常valueForUndefinedKey:
NSString *undefinedMethodName = @"valueForUndefinedKey:"; IMP undefinedIMP = class_getMethodImplementation([self class], NSSelectorFromString(undefinedMethodName)); 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]; } } 复制代码
- 找相关实例变量,找到了就返回
NSMutableArray *mArray = [self getIvarListName]; 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);; } 复制代码
- 调用
或抛出异常valueForUndefinedKey:
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]; } 复制代码
3.封装的方法
这里简单封装了几个用到的方法
-
安全调用方法及传两个参数fx_performSelectorWithMethodName:value:key:
- (BOOL)fx_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; } 复制代码
-
安全调用方法及传参fx_performSelectorWithMethodName:key:
- (BOOL)fx_performSelectorWithMethodName:(NSString *)methodName key:(id)key { if ([self respondsToSelector:NSSelectorFromString(methodName)]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [self performSelector:NSSelectorFromString(methodName) withObject:key]; #pragma clang diagnostic pop return YES; } return NO; } 复制代码
-
取成员变量getIvarListName
- (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中还有一些异常小技巧,在前文中已经提及过,这里再总结一下
五、KVC异常小技巧
1.技巧一——自动转换类型
- 用int类型赋值会自动转成__NSCFNumber
[person setValue:@18 forKey:@"age"]; [person setValue:@"20" forKey:@"age"]; NSLog(@"%@-%@", [person valueForKey:@"age"], [[person valueForKey:@"age"] class]); 复制代码
- 用结构体类型类型赋值会自动转成NSConcreteValue
ThreeFloats floats = {1.0, 2.0, 3.0}; NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)]; [person setValue:value forKey:@"threeFloats"]; NSLog(@"%@-%@", [person valueForKey:@"threeFloats"], [[person valueForKey:@"threeFloats"] class]); 复制代码
2.技巧二——设置空值
有时候在设值时设置空值,可以通过重写
setNilValueForKey
来监听,但是以下代码只有打印一次
// Int类型设置nil [person setValue:nil forKey:@"age"]; // NSString类型设置nil [person setValue:nil forKey:@"subject"]; @implementation FXPerson - (void)setNilValueForKey:(NSString *)key { NSLog(@"设置 %@ 是空值", key); } @end 复制代码
这是因为
setNilValueForKey
只对NSNumber类型有效
3.技巧三——未定义的key
对于未定义的key我们可以通过重写
setValue:forUndefinedKey:
、
valueForUndefinedKey:
来监听
@implementation FXPerson - (void)setValue:(id)value forUndefinedKey:(NSString *)key { NSLog(@"未定义的key——%@",key); } - (id)valueForUndefinedKey:(NSString *)key { NSLog(@"未定义的key——%@",key); return @"未定义的key"; } @end 复制代码
4.技巧四——键值验证
一个比较鸡肋的功能——键值验证,可以自行展开做重定向
NSError *error; NSString *name = @"Felix"; if (![person validateValue:&name forKey:@"names" error:&error]) { NSLog(@"%@",error); }else{ NSLog(@"%@", [person valueForKey:@"name"]); } @implementation FXPerson - (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing _Nullable *)outError { if([inKey isEqualToString:@"name"]){ [self setValue:[NSString stringWithFormat:@"里面修改一下: %@",*ioValue] forKey:inKey]; return YES; } *outError = [[NSError alloc]initWithDomain:[NSString stringWithFormat:@"%@ 不是 %@ 的属性",inKey,self] code:10088 userInfo:nil]; return NO; } @end 复制代码
写在后面
我们平时开发中经常用到KVC,理解KVC的使用和原理对我们会有很大帮助,具体可以下载Demo操作一下
正在跳转 (iOS交流裙 密码:123)