iOS底层--KVC实现原理

KVC 是 Key-Value Coding的简称。是由NSKeyValueCoding非正式协议启用的一种机制,对象采用该协议提供对其属性的间接访问。

请注意:这里说的很明确,是访问属性的

KVC 缺点:
1、KVC是使用过程中 ,是直接使用字符串进行操作,编译器不会进行检查,不易发现错误。后期维护也可能出现问题。
2、有时候取值可能也不是我们想要的类型。(可以拉到最后看注意点里面的 1、2)。

结论(具体步骤)

KVC虽然是NSObject的方法(来自 NSObject的分类@interface NSObject(NSKeyValueCoding))。但是其实现在Foundation.framework里,这里面是看不到源码的实现。
我们借助官方文档的介绍,先看结论

set(赋值)
1、找set<key>_set<Key>setIsName(setIsName文档上没有,但是确实是调用了)的方法,如果有,调用找到的方法 并结束。
2、如果没找到,就去找accessInstanceVariablesDirectly这个方法,如果这个方法返回YES,就可以进行下一步,如果返回NO,就抛出异常(步骤4)。
3、在2满足的情况下 ,按照这个顺序_<key>_is<Key><key>is<Key>找成员变量,并将value赋值给这个成员变量。
4、如果上面的步骤都失败了,调用setValue:forUndefinedKey:抛出异常。

get(取值) Object对象的流程
1、按顺序找get<Key><key>is<Key>_<key>的方法,如果有,调用找到的方法 并结束。
2、尝试获取一个NSArray:
2.1 、如果找到countOf<Key>objectIn<Key>AtIndex:这2个方法,会根据这2个方法创建一个数组并返回
2.2、如果找到countOf<Key><key>AtIndexes:这2个方法,也同样会返回一个数组
3、尝试获取一个NSSet:
同时找到3个方法:countOf<Key>enumeratorOf<Key>memberOf<Key>:,如果找到,会返回一个NSSet
4、如果没找到,就去找accessInstanceVariablesDirectly这个方法,如果这个方法返回YES,就可以进行下一步,如果返回NO,就抛出异常
4、按照这个顺序_<key>_is<Key><key>is<Key>找成员变量,并取出该成员变量的值。
5、进行细节处理:
5.1、如果是object 指针,直接返回结果
5.2、如果是NSNumber支持的标量类型,存储NSNumber 并返回
5.3、如果结果是NSNumber不支持的标量类型,转换成NSValue对象并返回
6、如果上面的步骤都失败了,调用valueForUndefinedKey:抛出异常。

详细流程如下图:
KVC设值、取值流程

验证步骤

官方文档有很详细的讲解,这里做记录和自己的补充理解。(官方文档是英文版,如果引文不太好的同学,可以借助Chrome自带的翻译功能,准确率。。。。。至少比看不懂英文要强一点。。。)。

set(赋值)

先看以下代码:

//定义一个NSObject类  Person
//添加一个公开的成员变量(@public  方便打印)
//添加一个NSString属性
//添加一个NSInteger属性
@interface Person : NSObject
{
    @public
    NSString    *jeName;
}
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age; 
@end

// 对其内容进行赋值
Person *p = [Person new];
[p setValue:@"nameValue" forKey:@"name"];
//[p setValue:@"nameValue" forKey:@"_name"];
[p setValue:@"jeNameValue" forKey:@"jeName"];
[p setValue:@4 forKey:@"age"];
NSLog(@"name = %@,  jeName = %@  age = %@",p.name,p->jeName,@(p.age));

// 查看打印信息
name = nameValue,  jeName = jeNameValue  age = 4

从上面的代码中,有几个疑问:

  • 明明是对属性进行访问,为何在KVC之后,成员变量jeName也有值了?
  • NSInteger通过KVC赋值的是一个NSNumber对象

为了搞清问题,先了解KVC原理。

在文档的Search Pattern for the Basic Setter部分中看到这样的介绍。

1、找set<key>_set<Key>的方法,如果有,调用找到的方法 并结束。
2、如果没找到,就去找accessInstanceVariablesDirectly这个方法,如果这个方法返回YES,就可以进行下一步,如果返回NO,就抛出异常(步骤4)。
3、在2满足的情况下 ,按照这个顺序_<key>_is<Key><key>is<Key>找成员变量,并将value赋值给这个成员变量。
4、如果上面的步骤都失败了,调用setValue:forUndefinedKey:抛出异常。

accessInstanceVariablesDirectly:
方法含义解释:--- 是否直接访问成员变量---- 如果有类似的成员变量,可不可以让我给它赋值

什么意思呢? 用代码来看具体是怎样调用的。

  • 步骤1的验证:
//定义一个NSObject类 Person   implementation 中不做任何操作
// 添加一个Person的分类 Person+JE
// 在分类中添加一个 jeName属性 (在person中添加属性,会被编译器自动生成set、get方法,不便与研究set、get流程)

@interface Person (JE)
@property (nonatomic, strong) NSString *jeName; 
@end

@implementation
#pragma mark - 关闭或开启实例变量赋值 (不实现的情况下,默认返回YES)
+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}

//set方法1
- (void)setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}
//set方法2
- (void)_setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}
//set方法3  ---------  这个方法官方文档上面没有提,但是确实是调用了
- (void)setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}
@end


//对jeName 用KVC进行赋值
Person *p = [Person new];
[p setValue:@"jeNameValue" forKey:@"jeName"];
NSLog(@"jeName = %@",p->jeName);

步骤1的验证 做以下操作

操作 3个set方法都保留 屏蔽set1 屏蔽set1、2 3个set都屏蔽
打印方法 打印方法1 打印方法2 打印方法3 -
jeName 值 nil nil nil jeNameValue
jeName 值的原因 对set进行覆盖,但是set内未赋值 同左 同左 未实现set方法,进行了步骤3

这样可以验证系统是按照set<key>_set<key>setIs<key>的步骤去走set方法

  • 步骤3的验证:
    用以下代码:
// 只有Person类,不创建分类
@interface Person : NSObject
{
    @public //方便外部打印
    NSString    *_jeName;      //成员变量1
    NSString    *_isJeName;    //成员变量2
    NSString    *jeName;      //成员变量3
    NSString    *isJeName;    //成员变量4
}
@end

.m 文件不做任何操作

//外部KVC赋值 并打印
Person *p = [Person new];
    [p setValue:@"jeNameValue" forKey:@"jeName"];
    //[p setValue:@4 forKey:@"age"];
    NSLog(@"_jeName = %@  _isJeName = %@  jeName = %@  isJeName = %@",p->_jeName,p->_isJeName,p->jeName,p->isJeName);

打印结果
_jeName = jeNameValue  _isJeName = (null)  jeName = (null)  isJeName = (null)

步骤3操作

操作 4个成员变量都存在 屏蔽成员变量1 屏蔽成员变量1、2 屏蔽成员变量1、2、3 屏蔽成员变量1、2、3 、4
分别打印成员变量值 变量1有值 变量2 有值 变量3 有值 变量4 有值 抛出异常

走到这里就知道为何可以对成员变量进行赋值了。其实是因为 系统在查找3个set方法没找到情况下,走了步骤3,对类似的成员变量进行了赋值

get(取值)

还是先看文档在文档的
Search Pattern for the Basic Getter部分中看到这样的介绍。

1、按顺序找get<Key><key>is<Key>_<key>的方法,如果有,调用找到的方法 并结束。
2、如果没找到,就去找accessInstanceVariablesDirectly这个方法,如果这个方法返回YES,就可以进行下一步,如果返回NO,就抛出异常4。
3、在2满足的情况下 ,按照这个顺序_<key>_is<Key><key>is<Key>找成员变量,并取出该成员变量的值。
4、如果上面的步骤都失败了,调用valueForUndefinedKey:抛出异常。

用下面代码验证

  • 验证步骤1
//  创建一个Person类并什么代码都不写
// 创建一个Person的分类  Person+JE
// 在Person+JE.m 中添加以下4个方法
@implementation Person (JE)
//方法1
- (NSString *)getJeName {
    NSLog(@"%s ",__func__);
    return @"";
}
//方法2
- (NSString *)jeName {
    NSLog(@"%s ",__func__);
    return @"";
}
//方法3
- (NSString *)isJeName {
    NSLog(@"%s ",__func__);
    return @"";
}
//方法4
- (NSString *)_jeName {
    NSLog(@"%s ",__func__);
    return @"";
}
@end

都不屏蔽 //打印方法1
屏蔽方法1 // 打印方法2
方法1、2 // 打印方法3
方法1、2、3 // 打印方法4
方法1、2、3、4 // 抛出异常 valueForUndefinedKey

  • 步骤3的验证
    创建Person 并添加4个成员变量
@interface Person : NSObject
{
    @public //方便外部打印
    NSString    *_jeName;      //成员变量1
    NSString    *_isJeName;    //成员变量2
    NSString    *jeName;      //成员变量3
    NSString    *isJeName;    //成员变量4
}
@end

外部对4个变量赋值 并通过KVC对jeName 进行取值
Person *p = [Person new];
p->_jeName = @"_jeName";
p->_isJeName = @"_isJeName";
p->jeName = @"isJeName";
p->isJeName = @"isJeName";

NSString *kvcValue = [p valueForKey:@"jeName"];

NSLog(@"jeName = %@",kvcValue);

可以分别屏蔽 成员变量1、2、3、4 查看打印结果,来验证步骤3。

KVC补充

1、keyPath

- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
是KVC 一级/多级 赋值/取值的一种方式。
比如以下代码:Person里面有一个 model 属性

// Person : NSObject     
//TestModel : NSObject
@interface Person : NSObject
@property (nonatomic, strong) TestModel *model;
@end

@interface TestModel : NSObject
@property (nonatomic, strong) NSString *modelName;
@end

可以直接通过keyPath的方式对model属性里面的 modelName进行赋值

Person *p = [Person new];
[p setValue:[TestModel new] forKey:@"model"];  
// 等同   p.model = [TestModel new];
// 等同 [p setValue:[TestModel new] forKeyPath:@"model"];  

[p setValue:@"modelName" forKeyPath:@"model.modelName"];

NSLog(@"modelName = %@",p.model.modelName);

keyPath 注意点:
1、比必须保证赋值的上一级是有值的(内存不为空)否则赋值无效。
2、可以对一级、多级赋值(上面代码中,初始化model属性的时候,forKey: 或者 forKeyPath:都是可行的)
3、forKey 只能对一级进行赋值(比如上面代码中model中的modelName进行初始化的时候,用forKey的方式的话 会报错)

  • keyPath 还有一个用法:
    将数组中的所有元素的某一个属性拿出来放在一个数组中
// 上面代码中的 TestModel

NSMutableArray *tempArr = [NSMutableArray new];
    for (NSInteger i = 0; i < 3; i ++) {
        TestModel *model = [TestModel new];
        [tempArr addObject:model];
    }
//    NSArray *arr = [tempArr valueForKey:@"modelName"];  和下面的 valueForKeyPath 效果相同
    NSArray *arr = [tempArr valueForKeyPath:@"modelName"];

//Arr 的打印结果
<__NSArrayI 0x6000020d0cf0>(
<null>,
<null>,
<null>
)

2、集合操作符

集合操作符是在调用[valueForKeyPath:]根据keypath中的特定符号,返回数据之前以某种方式对数据操作,官方文档戳这里

集合操作符的标准写法:
[ obj valueForKeyPath:@"leftKeypath.@collectionOperator.rightKeyPath"]

官方图

当操作对象本身是NSArray类型的时候,leftKeyPath可以省略

举例:

//创建一个Model,有一个NSArray类型的属性
@interface TestModel : NSObject
@property (nonatomic, strong) NSMutableArray<TestChildModel *> *childArr;
@end

@interface TestChildModel : NSObject
@property (nonatomic, assign) NSInteger childAge; 
@end

// 创建一个TestModel 并对其进行内容赋值 
//下面2种写法都是一样的
id obj1 = [model valueForKeyPath:@"childArr.@sum.childAge"];  //操作对象是NSObject,那么leftKeyPath 就不能省略
id obj2 = [model.childArr valueForKeyPath:@"@sum.childAge"];  //操作对象是model.childArr 数组类型,那么leftKeyPath 就能省略,而且必须要省略

集合操作符包括:

  • Aggregation Operators聚合操作符
    @avg 平均数,@count" 计数,@max 最大值,@min 最小值,@sum 求和

  • Array Operators数组操作符
    1、@unionOfObjects 操作对象指定属性的集合,类似上面说的NSArray *arr = [tempArr valueForKeyPath:@"modelName"];
    2、@distinctUnionOfObjects 操作对象指定属性去重后的集合 类似于unionOfObjects,不过多了一步去重的操作

  • Nesting Operators嵌套操作符
    1、unionOfArrays 操作嵌套对象指定属性的集合,操作对象是指定嵌套对象,比如二位数组(数组里面的对象也是数组),将2个数组中指定的属性全部组合在一起
    2、distinctUnionOfArrays 把 unionOfArrays 的结果进行去重操作、
    3、distinctUnionOfSets 操作的对象是嵌套的 NSSet, 得到的结果是一个去重的 NSSet(NSSet本身就有去重的特性)
    ⚠️:对NSSet 进行 valueForKey: 操作时,得到的结果是一个NSSet,而且NSSet的count 并不一定和原来的操作对象的count相同,因为NSSet 有去重特性

KVC 细节(注意点)

  • 1、在上面取值过程中,说到如果没找到相关的get方法,那么会直接取值相似的成员变量并返回,以下代码中:取值的类型是什么? 值是什么?
//创建一个Person类 并添加一个公开成员变量
@interface Person : NSObject {
    @public //方便外部赋值、取值
    NSString    *_age;
}

//创建一个Person分类 Person+JE 并添加一个属性age
@interface Person (JE)
@property (nonatomic, assign) NSInteger age; 
@end

//在外部进行赋值并打印valueForKey:
Person *p = [Person new];
p->_age = @"1";
id age = [p valueForKey:@"age"];
NSLog(@"age = %@",age);

打印结果是 age = "1"; age是NSString类型。
因为是直接取到_age的值并返回,所以,即使定义的是NSInteger 类型,最后运行时,得到的依然是NSString

  • 2、依然是上面的代码,在外部进行通过KVC对age进行赋值
Person *p = [Person new];
[p setValue:@12 forKey:@"age"];
id age = [p valueForKey:@"age"];
NSString *_age = p->_age;
NSLog(@"age = %@  cls = %@ \
      _age = %@ cls = %@",age,[age class],_age,[_age class]);

可以先猜想打印结果。
其实这里的age 和 _age 最终的取值 都是来自Person中的公开成员变量 NSString *_age
age = 12 cls = __NSCFNumber _age = 12 cls = __NSCFNumber

  • 3、创建一个Person类并添加一个age属性和name(和上面的1、2已经没关系了)
@interface Person : NSObject
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) NSString *name;
@end

//外部进行赋值操作
Person *p = [Person new];
//    [p setValue:@"a" forKey:@"age"];
[p setValue:@"12" forKey:@"age"];
[p setValue:@(13) forKey:@"name"];

猜测能否正常运行,如果正常运行,得到的age值是什么类型?
能正常运行
1、最后得到的age 还是 NSInteger(本身定一个属性的类型),值为12
2、最后的name 是 NSnumber 类型的13
因为在get取值步骤中,只说道,对NSnunber类型的属性有自动转换类型的功能,但是并没有对其他类型的属性自动转换,所有age被自动转换为NSInteger,而name由于运行时机制,最后得到的是NSInteger

如果赋值为@"a",结果得到的age = 0; 因为@"a"转成integer类型就是0
也就是说。KVC赋值 具有自动转换类型的功能。符合get流程中的5.2

  • 4、 NSMutableDictionary 中使用 setValue: forKey:valueForKey:

系统对NSMutableDictionary的 setValue: forKey:NSDictionary 的valueForKey: 进行了重写

@interface NSDictionary<KeyType, ObjectType>(NSKeyValueCoding)
/* Return the result of sending -objectForKey: to the receiver.
*/
- (nullable ObjectType)valueForKey:(NSString *)key;
@end

@interface NSMutableDictionary<KeyType, ObjectType>(NSKeyValueCoding)
/* Send -setObject:forKey: to the receiver, unless the value is nil, in which case send -removeObjectForKey:.
如果赋值为nil, 将会调用 removeObjectForKey 方法
*/
- (void)setValue:(nullable ObjectType)value forKey:(NSString *)key;
@end

所以 :在发送网络请求的时候,组装一个NSMutableDictionary 时,会有这样的写法:[dic setValue:@"value" forKey:@"key"]; 来达到为dictionary添加一个key的目的。

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