KVC底层原理分析

一、前言

提起 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<NSString *, id> *)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: 的时候是怎么样将值赋值到我们的对象里去的呢?


image.png

根据上面官方文档得知:

1.第一步会先去对象里面查找是否有 set<Key>、_set<Key>、setIs<Key> 的访问器(即方法)。
2.如果没有找到访问器并且类方法 accessInstanceVariablesDirectly 返回 YES,则会按顺序去查找名称为 _<key>、_is< key>、<key> 或 <key> 的实例变量,如果找到直接设置变量并完成。
3.如果方法和实例变量都没找到,则会调用 setValue:forUndefinedKey: 方法。
<br>说明: 这里的 "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,则会报 '[<Person 0x6000033a5860> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key xxx.' 崩溃。

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<Key>, <key>, is<Key>, or _<key>, 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 _<key>, _is<Key>, <key>, or is<Key>, 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<Key>, <key>, is<Key> 或 _<key> 的访问器(即方法)。
2.如果没有找到访问器并且类方法 accessInstanceVariablesDirectly 返回 YES,则按顺序搜索名为 _<key>, _is<key>,<key>,或 is<Key> 的实例变量,如果找到,则直接获取实例变量的值并将值转换成相应类型返回。
3.如果方法和实例变量都没有找到,则调用 valueForUndefinedKey: 方法。

3.1 getKey: 方法验证

@implementation Person
//MARK: - valueForKey 流程分析 - get<Key>, <key>, is<Key>, or _<key>,
- (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,则会报 '[<Person 0x600001ce28b0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key xxx.' 崩溃。

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<Key>:、_set<Key>、setIs<Key>:(虽然setIs<Key>:这个方法官方文档上没有写,但确实是调用了的) 方法,有的话就调用进行赋值。
2.如果没有找到,并且类方法 accessInstanceVariablesDirectly 返回 YES,则会按顺序去查找名称为 _<key>、_is< key>、<key> 或 <key> 的实例变量,如果找到直接设置变量并完成。
3.如果又没找到方法和实例变量,则会调用 setValue:forUndefinedKey: 方法,如果对象没有实现 setValue:forUndefinedKey: 则会报 '[<Person 0x60000346a760> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key xxx.' 的崩溃。
4.在使用 valueForKey: 的时候,先去按顺序查找对象是否有 get<Key>, <key>, is<Key> 或 _<key> 的方法,有的话就返回。
5.如果没有找到,并且类方法 accessInstanceVariablesDirectly 返回 YES,则会按顺序搜索名为 _<key>, _is<key>,<key>,或 is<Key> 的实例变量,如果找到,则直接获取实例变量的值并将值转换成相应类型返回。
6.如果又没找到方法和实例变量,则调用 valueForUndefinedKey: 方法。如果对象没有实现 valueForUndefinedKey: 则会报 '[<Person 0x600001ce28b0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key xxx.' 的崩溃。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349