一、前言
提起 KVC
,大多数的第一反应是 setValue: forKey:
以及 setValue: forKeyPath:
,这也就是我们的所说的键值编码(Key-value coding),键值编码是一种由 NSKeyValueCoding
非正式协议启用的机制,对象采用该协议来提供对其属性的间接访问。当对象符合键值编码时,可以通过简洁、统一的消息传递接口通过字符串参数对其属性进行寻址。详细解释可以进入官方文档查阅。接下来就一起跟我进入 KVC
的底层原理探索吧。
二、KVC 初探
1.KVC 的几种使用方式
创建一个 Person 类,在类中添加一些属性。
Person.h
typedef struct {
float x, y, z;
} ThreeFloats;
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSArray *array;
@property (nonatomic, strong) NSMutableArray *mArray;
@property (nonatomic, assign) int age;
@property (nonatomic) ThreeFloats threeFloats;
@property (nonatomic, strong) Student *student;
@end
1.1基本类型使用
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)valueForKey:(NSString *)key;
// 给person对象 name 属性赋值和取值
[person setValue:@"流年匆匆" forKey:@"name"];
[person valueForKey:@"name"];
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
// 嵌套属性访问
Student *student = [[Student alloc] init];
student.name = @"xx";
person.student = student;
[person setValue:@"学生" forKeyPath:@"student.name"];
NSLog(@"%@",[person valueForKeyPath:@"student.name"]);
1.2集合类型使用
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
// 修改不可变数组array的第一个值,
person.array = @[@"1",@"2",@"3"];
// 方法一,修改为1
NSArray *array = @[@"1",@"2",@"3"];
[person setValue:array forKey:@"array"];
// 方法二,kvc的方法,修改为10
NSMutableArray *arrayM = [person mutableArrayValueForKey:@"array"];
arrayM[0] = @"10";
1.3集合类型使用
- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;
// 字典转模型
NSDictionary* dict = @{@"name":@"流年匆匆",@"age":@18};
[person setValuesForKeysWithDictionary:dict];
1.4集合类型使用
ThreeFloats floats = {1., 2., 3.};
// 非对象类型,需转换成相应的NSValue
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSValue *reslutValue = [person valueForKey:@"threeFloats"];
NSLog(@"value = %@",reslutValue);
// 创建一个同类型结构体用来接收reslutValue
ThreeFloats th;
[reslutValue getValue:&th] ;
NSLog(@"%f - %f - %f",th.x,th.y,th.z);
2. setValue:forKey: 底层原理探索
当我们调用 setValue:forKey: 的时候是怎么样将值赋值到我们的对象里去的呢?
根据上面官方文档得知:
1.第一步会先去对象里面查找是否有 set
2.如果没有找到访问器并且类方法 accessInstanceVariablesDirectly 返回 YES,则会按顺序去查找名称为 _
3.如果方法和实例变量都没找到,则会调用 setValue:forUndefinedKey: 方法。
说明: 这里的 "key" 指成员变量名字, 书写格式需要符合 KVC 的命名规则。
2.1 setKey: 方法验证
创建 Person 类,并添加四个实例变量,以及添加 setName:、_setName:、accessInstanceVariablesDirectly 方法。(下面验证都是以这个对象为准,实例变量不变,方法变)
@interface Person : NSObject{
@public
NSString *_name;
NSString *_isName;
NSString *name;
NSString *isName;
}
@implementation Person
//MARK: - setKey 的流程分析
- (void)setName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
- (void)_setName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
@end
Person *person = [[Person alloc] init];
[person setValue:@"流年匆匆" forKey:@"name"];
结果:依次访问顺序
-[Person setName:] - 流年匆匆
-[Person _setName:] - 流年匆匆
如果将所有 set 方法注释,accessInstanceVariablesDirectly 返回 NO,则会报 '[
2.2 accessInstanceVariablesDirectly 返回 YES 后的实例变量验证
@implementation Person
#pragma mark - 关闭或开启实例变量赋值
+ (BOOL)accessInstanceVariablesDirectly{
return YES;
}
@end
Person *person = [[Person alloc] init];
[person setValue:@"流年匆匆" forKey:@"name"];
NSLog(@"_name:%@-_isName:%@-name:%@-isName%@",person->_name,person->_isName,person->name,person->isName);
NSLog(@"_isName:%@-name:%@-isName:%@",person->_isName,person->name,person->isName);
NSLog(@"name:%@-isName%@",person->name,person->isName);
NSLog(@"isName:%@",person->isName);
将 accessInstanceVariablesDirectly 返回 YES,再将实例变量 _name、_isName、name、isName 按顺序注释运行(NSLog也要依次注释哦),得到的结果会是以下输出。
1.KVC探索[4370:1716833] _name:流年匆匆-_isName:(null)-name:(null)-isName:(null)
2.KVC探索[4417:1720210] _isName:流年匆匆-name:(null)-isName:(null)
3.KVC探索[4445:1722057] name:流年匆匆-isName(null)
4.KVC探索[4468:1723450] isName:流年匆匆
3.valueForKey: 底层原理探索
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.
// 中间是集合类型的,我们分析的是对象类型,所以跳过,有兴趣的可以自己看看
4.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.
5.If the retrieved property value is an object pointer, simply return the result.
If the value is a scalar type supported by NSNumber, store it in an NSNumber instance and return that.
If the result is a scalar type not supported by NSNumber, convert to an NSValue object and return that.
6.If all else fails, invoke valueForUndefinedKey:. This raises an exception by default, but a subclass of NSObject may provide key-specific behavior.
根据官方文档总体来说:
1.第一步会先去按顺序查找 get
2.如果没有找到访问器并且类方法 accessInstanceVariablesDirectly 返回 YES,则按顺序搜索名为 _
3.如果方法和实例变量都没有找到,则调用 valueForUndefinedKey: 方法。
3.1 getKey: 方法验证
@implementation Person
//MARK: - valueForKey 流程分析 - get, , is, or _,
- (NSString *)getName{
return NSStringFromSelector(_cmd);
}
- (NSString *)name{
return NSStringFromSelector(_cmd);
}
- (NSString *)isName{
return NSStringFromSelector(_cmd);
}
- (NSString *)_name{
return NSStringFromSelector(_cmd);
}
Person *person = [[Person alloc] init];
NSLog(@"取值:%@",[person valueForKey:@"name"]);
@end
结果:依次访问顺序
1.KVC探索[4725:1958586] 取值:getName
2.KVC探索[4749:1978501] 取值:name
3.KVC探索[4749:1978501] 取值:isName
4.KVC探索[4749:1978501] 取值:_name
如果将所有 get 方法注释,accessInstanceVariablesDirectly 返回 NO,则会报 '[
3.2 accessInstanceVariablesDirectly 返回 YES 后实例变量验证
#pragma mark - 关闭或开启实例变量赋值
+ (BOOL)accessInstanceVariablesDirectly{
return YES;
}
Person *person = [[Person alloc] init];
person->_name = @"_name";
person->_isName = @"_isName";
person->name = @"name";
person->isName = @"isName";
NSLog(@"取值:%@",[person valueForKey:@"name"]);
将 accessInstanceVariablesDirectly 返回 YES,再将实例变量 _name、_isName、name、isName 按顺序注释运行(赋值代码也要依次注释哦),得到的结果会是以下输出。
1.KVC探索[4792:2019099] 取值:_name
2.KVC探索[4792:2019099] 取值:_isName
3.KVC探索[4792:2019099] 取值:name
4.KVC探索[4792:2019099] 取值:isName
4. KVC 防崩溃处理
当我们在使用 setValue:forKey: 或者 valueForKey: 的时候,由于 key 需要自己手写且没有提示,所以很可能会不小心写错,然后就会报 setValue:forUndefinedKey: 或者 valueForUndefinedKey: 的崩溃,如何防止这种崩溃呢?直接在当前类实现 - (void)setValue:(id)value forUndefinedKey:(NSString *)key 和 - (id)valueForUndefinedKey:(NSString *)key 即可。
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"来了");
}
- (id)valueForUndefinedKey:(NSString *)key {
return nil;
}
//MARK: 空置防崩溃
- (void)setNilValueForKey:(NSString *)key{
NSLog(@"设置 %@ 是空值",key);
}
//MARK: - 键值验证 - 容错 - 派发 - 消息转发
- (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;
}
5. 拓展
除了 KVC 能给对象属性赋值之外,其实我们经常用的是点语法,例: person.name = @"流年匆匆";,这种方法最终都会调用 reallySetProperty 函数对属性进行赋值,而又根据属性修饰符的不同,参数也是不一样的,看下面源码一目了然。
6. 总结
1.使用 setValue:forKey: 的时候,会先去顺序查找对象是否有 set
2.如果没有找到,并且类方法 accessInstanceVariablesDirectly 返回 YES,则会按顺序去查找名称为 _
3.如果又没找到方法和实例变量,则会调用 setValue:forUndefinedKey: 方法,如果对象没有实现 setValue:forUndefinedKey: 则会报 '[
4.在使用 valueForKey: 的时候,先去按顺序查找对象是否有 get
5.如果没有找到,并且类方法 accessInstanceVariablesDirectly 返回 YES,则会按顺序搜索名为 _
6.如果又没找到方法和实例变量,则调用 valueForUndefinedKey: 方法。如果对象没有实现 valueForUndefinedKey: 则会报 '[