OC底层 - KVC
前言
一、KVC的基础
二、KVC底层原理
三、 自定义KVC
“ KVC是Key-Value Coding 的简称,即键值编码,提供一种机制 (NSKeyValueCoding非正式协议启用的一种机制) 来间接访问对象的属性。而不是通过Setter、Getter方法访问。”这是KVC的基本概念,很像JAVA的反射。
在日常的开发中, 我们和KVC打交道的地方很多,所以很有必要系统的总结一下KVC的使用和原理。
非正式协议也就是类别或者类扩展
一、KVC的基础
可以看苹果官方的教程:苹果开发者学习文档
1、KVC的基本使用
@interface MyTestClass : NSObject {
@public
NSString *name;
}
@property (nonatomic, assign) int age;
上面的声明,我们可以通过来Setter或者指针的形式设置值。
- (void)viewDidLoad {
[super viewDidLoad];
MyTestClass *testObj = [MyTestClass alloc];
testObj.age = 10;
testObj->name = @"magic";
}
而用KVC设置如下:
[testObj setValue:@"geng" forKey:@"name"];
进入setValue:forKey:方法,可以看到是NSObject(NSKeyValueCoding)的分类。也有数组和字典等类型的分类。而setValue:forKey:的源码是在Foundation库中,并没有开源(稍后在分析)。
2、KVC - 数组
@interface MyTestClass : NSObject
.....
@property (nonatomic, copy) NSArray *array;
@end
......
- (void)viewDidLoad {
[super viewDidLoad];
.....
//修改数组
//第一种:
NSArray *array = [testObj valueForKey:@"array"];
array = @[@"3",@"2",@"1"];
[testObj setValue:array forKey:@"array"];
NSLog(@"%@",testObj.array);
// 遍历
NSEnumerator *enumerator = [testObj.array objectEnumerator];
NSString* str = nil;
while (str = [enumerator nextObject]) {
NSLog(@"%@", str);
}
//第二种: (深拷贝了一份)
NSMutableArray *mArray = [testObj mutableArrayValueForKey:@"array"];
mArray[0] = @"4";
NSLog(@"%@",testObj.array);
}
针对第二种方法:
因为通过mutableArrayValueForKey获取的NSMutableArray是NSKeyValueSlowMutableArray,当mArray设置值时,会重新深拷贝一个数组对象 (注意这里是深拷贝数组容器) 赋值给原数组(testObj.array)。所以性能问题有待考虑。
3、KVC - 字典
@interface MyTestModel : NSObject
@property (nonatomic, copy) NSString *name1;
@property (nonatomic, assign) int name2;
@property (nonatomic, assign) bool name3;
@property (nonatomic, copy) NSObject *name4;
@property (nonatomic, assign) double name5;
@end
先创建一个model,model中有不同数据类型的属性。
- (void)testDic {
MyTestModel *model = [[MyTestModel alloc] init];
NSDictionary *dics = @{@"name1":@"1",
@"name2":@"2",
@"name3":@"3",
@"name4":@"4",
@"name5":@"5"};
//字典转模型
[model setValuesForKeysWithDictionary:dics];
}
然后通过NSKeyValueCoding中的setValuesForKeysWithDictionary:方法来进行字典转模型。并且可以自动转成我们定义的类型。
这样处理字典转模型的方法,在很多年前我经常用到。
需要注意的是:
(1)如果字典中的key和model中的属性不对照时,会直接崩溃。原因是:"[<MyTestModel 0x600000a515c0> setValue:forUndefinedKey:]:this class is not key value coding-compliant for the key name6.",找不到对应的key。但是反过来,model中有多余的属性时,不会崩溃。
解决崩溃的方法:可以重写setValue:forUndefinedKey:方法。
(2)当使用 setValue:forKey: 把一个 nil 值赋值给纯量(如 float, int, …)时,会崩溃,setNilValueForKey: 会被调用。
解决崩溃的方法:重写setNilValueForKey:
4、KVC - 结构体
typedef struct {
int x;
int y;
} TestXY;
.....
- (void)testStruct {
MyTestClass *testObj = [MyTestClass alloc];
//设置
TestXY xy = {1,2};
NSValue *value = [NSValue valueWithBytes:&xy objCType:@encode(TestXY)];
[testObj setValue:value forKey:@"xy"];
NSLog(@"%i,%i",testObj.xy.x,testObj.xy.y);
//获取
NSValue *gValue = [testObj valueForKey:@"xy"];
TestXY gXy;
[gValue getValue:&gXy];
NSLog(@"%i,%i",gXy.x,gXy.y);
}
5、KVC - keyPath
typedef struct {
int x;
int y;
} TestXY;
@interface MyTestClass : NSObject {
@public
NSString *name;
}
@property (nonatomic, assign) int age;
@property (nonatomic, copy) NSArray *array;
@property (nonatomic, assign) TestXY xy;
@property (nonatomic, strong) MyTestModel *model;
@end
我们也可以通过路由的方式来访问和设置值:
- (void)testKeyPath {
MyTestClass *testObj = [MyTestClass alloc];
MyTestModel *model = [MyTestModel alloc];
model.name1 = @"test";
testObj.model = model;
NSLog(@"%@",testObj.model.name1);
//设置
[testObj setValue:@"noTest" forKeyPath:@"model.name1"];
//获取
NSString *name1 = [testObj valueForKeyPath:@"model.name1"];
NSLog(@"%@",name1);
}
二、KVC底层原理
结合KVC官方文档说明:
1、KVC设置值的流程
(1)Look for the first accessor named set: or _set, in that order. If found, invoke it with the input value (or unwrapped value, as needed) and finish.
我创建一个model,以“name”为例子。
@interface MyTheoryModel : NSObject {
//成员变量
@public
NSString *_name;
NSString *_isName;
NSString *name;
NSString *isName;
}
//这里不用属性来演示了,因为属性会默认生成set方法。
//@property (nonatomic, copy) NSString *name;
@end
//外层通过setValue:forKey:设置 :
- (void)testTheory {
MyTheoryModel *model = [[MyTheoryModel alloc] init];
//注意这里设置的key是name。
[model setValue:@"1" forKey:@"name"];
}
当我们在外层调用:“ [model setValue:@"1" forKey:@"name"];” 时,首先找到3种setter方法:set -> _set -> setIs (这一种应该是文档漏写了)。
如果找到了任何一个,则调用setter处理.
@implementation MyTheoryModel
#pragma mark - setKey 流程分析 set<Key> -> _set<Key> -> setIs<Key>
- (void)setName:(NSString *)name {
NSLog(@"%s,%@",__FUNCTION__,name);
}
- (void)_setName:(NSString *)name {
NSLog(@"%s,%@",__FUNCTION__,name);
}
- (void)setIsName:(NSString *)name {
NSLog(@"%s,%@",__FUNCTION__,name);
}
如果没有找到以上三种setter方法,看第(2)步骤。
(2)If no simple accessor is found, and if the class method accessInstanceVariablesDirectly returns YES, look for an instance variable with a name like _, _is, , or is, in that order. If found, set the variable directly with the input value (or unwrapped value) and finish.
先判断 + (BOOL)accessInstanceVariablesDirectly 方法是否返回YES。(默认返回YES)
如果返回YES,则会按照_key->_isKey->key->isKey的顺序去搜索成员变量。
找到任意一个则进行赋值:
意思是:当去掉_name,就会给_isName赋值。
当同时去掉_name和_isName就会给name赋值。
当同时去掉_name、_isName和name,就会给isName赋值。
如果找不到成员变量或者accessInstanceVariablesDirectly返回NO,会走第(3)步。
(3)Upon finding no accessor or instance variable, invoke setValue:forUndefinedKey:. This raises an exception by default, but a subclass of NSObject may provide key-specific behavior.
如果 “setter方法” 或者 “实例变量”都没找到,系统会执行该对象的setValue:forUndefinedKey:方法。默认抛出异常,所以我们使用KVC进行解析模型数据时,要重写实现setValue:forUndefinedKey:函数,否则会崩溃。
整体流程图如下:

补充1:当使用property声明属性的时候,会默认生成set形式的set方法。
补充2:对于第(2)步,可能有点绕,再举个例子就清楚了:
比如我们在外层设置的是 : [model setValue:@"1" forKey:@"_name"];
注意设置key是"_name"。当accessInstanceVariablesDirectly为YES时,我们会按照__name->_is_name->_name->is_name的顺序去搜索。
2、KVC取值的流程
(1)Search the instance for the first accessor method found with a name like get, , is, or _, in that order. If found, invoke it and proceed to step 5 with the result. Otherwise proceed to the next step.
当我们在外层调用:“[model valueForKey:@"name"]” 时,首先找到4种getter方法:get ->-> is -> _,以name为例:getName->name->isName->_name。
#pragma mark - getKey 流程分析 get<Key>-><Key>->is<Key>->_<key>
- (NSString *)getName {
return NSStringFromSelector(_cmd);
}
- (NSString *)name {
return NSStringFromSelector(_cmd);
}
- (NSString *)isName {
return NSStringFromSelector(_cmd);
}
- (NSString *)_name {
return NSStringFromSelector(_cmd);
}
如果找到以上4种getter方法,则做相应处理。
如果没有找到,走第(2)步流程:
(2)If no simple accessor method or group of collection access methods is found, and if the receiver's class method accessInstanceVariablesDirectly returns YES, search for an instance variable named _, _is, , or is, in that order. If found, directly obtain the value of the instance variable and proceed to step 5. Otherwise, proceed to step 6.
判断+ (BOOL)accessInstanceVariablesDirectly函数是否返回YES,如果为YES,则访问成员变量,顺序为:_key->_isKey->key->isKey。和设置值时(没有找到setter方法时),就会按照_key->_isKey->key->isKey设置值的流程是一一对应的。
如果找不到成员变量或accessInstanceVariablesDirectly返回NO时,走第(3)步
(3)If all else fails, invoke valueForUndefinedKey:. This raises an exception by default, but a subclass of NSObject may provide key-specific behavior.
均找不到,则调用valueForUndefinedKey:抛出异常。
整体的取值流程图:

补充1:当使用property声明属性的时候,会默认生成 Key 形式的get方法。
补充2:关于第(2)步:
@interface MyTheoryModel : NSObject {
@public
NSString *_name;
NSString *_isName;
NSString *name;
NSString *isName;
}
.....
- (void)test2 {
MyTheoryModel *model = [[MyTheoryModel alloc] init];
model->name = @"1";
NSLog(@"%@",[model valueForKey:@"name"]);
}
当执行test2的时候,打印出的为null。
原因是:valueForKey首先会去找_name,然后找_isName,只有当MyTheoryModel中没有_name和_isName成员变量的时候,才会去找name。即使name有值,也不会直接去访问它,所以打印为null。
三、 自定义KVC
可以参考大神根据IOS Foundation框架反编译写的KVC:DIS_KVC_KVO
我们简单的自定义,只大概实现两个关键方法:
@interface NSObject (MyKVC)
- (void)my_setValue:(id)value forKey:(NSString *)key;
- (id)my_valueForKey:(NSString *)key;
@end
具体实现:
#import "NSObject+MyKVC.h"
#import <objc/runtime.h>
@implementation NSObject (MyKVC)
- (void)my_setValue:(id)value forKey:(NSString *)key {
//1.判断key是否存在
if (key == nil) {
@throw [NSException exceptionWithName:@"MyInvalidArgumentException"
reason:[NSString stringWithFormat:@"*** -[%@ setValue:forKey:]: attempt to set a value for a nil key",NSStringFromClass(self.class)]
userInfo:nil];
}
if (key.length == 0) {
@throw [NSException exceptionWithName:@"MyUnknownKeyException"
reason:[NSString stringWithFormat:@"[<%@ %p> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key .",NSStringFromClass(self.class),self]
userInfo:nil];
}
//2.先判断是否有set方法,setKey -> _setKey -> setIsKey
//2.1 key的首写字母大写
NSString *Key = key.capitalizedString;
//2.2拼接方法
NSString *setKey = [NSString stringWithFormat:@"set%@",Key];
NSString *_setKey = [NSString stringWithFormat:@"_set%@",Key];
NSString *setIsKey = [NSString stringWithFormat:@"setIs%@",Key];
//2.3 判断是否存在对应的set方法
if ([self my_performSelectorWithMethodName:setKey withObject:value]) {
return;
} else if ([self my_performSelectorWithMethodName:_setKey withObject:value]) {
return;
} else if ([self my_performSelectorWithMethodName:setIsKey withObject:value]) {
return;
}
//3.判断是否响应 accessInstanceVariablesDirectly
if (![self.class accessInstanceVariablesDirectly]) {
@throw [[NSException alloc] initWithName:@"MyUnknownKeyException"
reason:[NSString stringWithFormat:@"[<%@ %p> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key name.",NSStringFromClass(self.class),self]
userInfo:nil];
}
//4.间接变量
//4.1 获取变量的数组
NSMutableArray *ivarArray = [self getIvarListName];
//_key -> _isKey -> key -> isKey
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
if ([ivarArray containsObject:_key]) {
//4.2 获取相应的ivar
Ivar ivar = class_getInstanceVariable(self.class, _key.UTF8String);
//4.3 对应的ivar 设置值
object_setIvar(self, ivar, value);
return;
} else if ([ivarArray containsObject:_isKey]) {
//4.2 获取相应的ivar
Ivar ivar = class_getInstanceVariable(self.class, _isKey.UTF8String);
//4.3 对应的ivar 设置值
object_setIvar(self, ivar, value);
return;
} else if ([ivarArray containsObject:key]) {
//4.2 获取相应的ivar
Ivar ivar = class_getInstanceVariable(self.class, key.UTF8String);
//4.3 对应的ivar 设置值
object_setIvar(self, ivar, value);
return;
} else if ([ivarArray containsObject:isKey]) {
//4.2 获取相应的ivar
Ivar ivar = class_getInstanceVariable(self.class, isKey.UTF8String);
//4.3 对应的ivar 设置值
object_setIvar(self, ivar, value);
return;
}
//5.如果再找不到
@throw [NSException exceptionWithName:@"MyUnknownKeyException"
reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)]
userInfo:nil];
}
- (id)my_valueForKey:(NSString *)key {
//1.判断key是否存在
if (key == nil) {
@throw [NSException exceptionWithName:@"MyInvalidArgumentException"
reason:[NSString stringWithFormat:@"*** -[%@ valueForKey:]: attempt to retrieve a value for a nil key",NSStringFromClass(self.class)]
userInfo:nil];
}
if (key.length == 0) {
@throw [NSException exceptionWithName:@"MyUnknownKeyException"
reason:[NSString stringWithFormat:@"[<%@ %p> valueForUndefinedKey:]: this class is not key value coding-compliant for the key .",NSStringFromClass(self.class),self]
userInfo:nil];
}
//2.找到相关方法:getKey -> key -> isKey -> _key
NSString *Key = key.capitalizedString;
NSString *getKey = [NSString stringWithFormat:@"get%@:",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@:",Key];
NSString *_key = [NSString stringWithFormat:@"_%@:",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(isKey)]) {
return [self performSelector:NSSelectorFromString(isKey)];
} else if ([self respondsToSelector:NSSelectorFromString(_key)]) {
return [self performSelector:NSSelectorFromString(_key)];
}
#pragma clang diagnostic pop
// 3:判断是否能够直接赋值实例变量
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"MyUnknownKeyException"
reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self]
userInfo:nil];
}
// 4.按照 _key,_isKey,key,isKey 顺序查询实例变量
NSMutableArray *mArray = [self getIvarListName];
_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
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);
}
// 5.抛出异常
@throw [NSException exceptionWithName:@"MyUnknownKeyException"
reason:[NSString stringWithFormat:@"****[%@ %@]: valueForUndefinedKey:%@.****",self,NSStringFromSelector(_cmd),key]
userInfo:nil];
return nil;
}
#pragma mark - Private
- (BOOL)my_performSelectorWithMethodName:(NSString *)methodName withObject:(id)object {
if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:NSSelectorFromString(methodName) withObject:object];
#pragma clang diagnostic pop
return true;
}
return false;
}
/*
struct objc_ivar {
char * _Nullable ivar_name OBJC2_UNAVAILABLE;
char * _Nullable ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
*/
- (NSMutableArray *)getIvarListName {
NSMutableArray *ivarArray = [[NSMutableArray alloc] init];
unsigned int count = 0;
//objc_ivar 结构体指针
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];
[ivarArray addObject:ivarName];
}
free(ivars);
return ivarArray;
}
@end