[iOS] KVC底层原理

1. 简介

KVC 即键值编码,KVC 是由 NSKeyValueCoding 非正式协议启用的一种机制,对象采用该协议来间接访问其属性。即可以通过一个字符串key来访问某个属性,这种间接访问机制补充了实例变量及其相关的访问器方法所提供的直接访问。

2. 相关 API

主要有下面 4 个常用的方法:

  • 通过key 设值/取值
//直接通过Key来取值
- (nullable id)valueForKey:(NSString *)key;

//通过Key来设值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
  • 通过keyPath 设值/取值
//通过KeyPath来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath; 

//通过KeyPath来设值                 
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  

还有其他的一些方法:

//默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
+ (BOOL)accessInstanceVariablesDirectly;

//KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

//这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

//如果Key不存在,且KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
- (nullable id)valueForUndefinedKey:(NSString *)key;

//和上一个方法一样,但这个方法是设值。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

//如果你在SetValue方法时面给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;

//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

3. 底层原理

3.1 KVC 赋值底层原理

在日常开发中,针对对象属性的赋值,一般有以下两种方式:

  • 通过 setter方法赋值
  • 通过KVC 键值编码的相关API 赋值
Person *person = [[Person alloc] init];
// 1、一般setter 方法
person.name = @"哈哈";
// 2、KVC方式
[person setValue:@"嘻嘻" forKey:@"name"]; 

下面针对使用最多的KVC设值方法:setValue:forKey,来进行其底层原理的探索。
在这里,我们通过Key-Value Coding Programming Guide苹果官方文档来研究,针对设值流程,有以下说明:

当调用 setValue:forKey:设值属性 value 时,其底层的执行流程为:

[第一步]
首先查找是否有这三种setter 方法,并且查找顺序为set<Key>: -> _set<Key> -> setIs<Key>

  • 如果实现了其中任意一个 setter方法,则直接设置成员变量的value( 注意:key 是指成员变量名,首字母大小写需要符合KVC 的命名规范)
  • 如果没有,则进入下一步

[第二步]
如果没有第一步中的三个简单的 setter 方法,则查找accessInstanceVariablesDirectly是否返回YES

  • 如果返回YES,则查找间接访问的实例变量进行赋值,查找顺序为: _<key> -> _is<Key> -> <key> -> is<Key>,如果找到其中任意一个实例变量,则赋值,如果都没有,则进入下一步
  • 如果返回NO,则进入下一步

[第三步]
如果 setter 方法或者实例变量都没有找到,系统会执行该对象的setValue:forUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常。

注意:我们实现setValue:forUndefinedKey:这个方法之后,哪怕没有任何操作,也不会引发崩溃了。

综上所述,KVC通过setValue:forKey:方法设值的流程以设置LGPerson的对象person的属性name为例,如下图所示:

image.png

3.2 KVC取值底层原理

同样的,我们可以通过官方文档分析KVC取值的底层原理
当调用valueForKey:时,其底层的执行流程如下:
[第一步]
首先查找getter方法,按照get<Key> -> <key> -> is<Key> -> _<key>的方法顺序查找

  • 如果找到,则进入 [第五步]
  • 如果没找到,则进入下一步

[第二步]
如果第一步中的getter方法没有找到,KVC会查找countOf <Key>objectIn <Key> AtIndex :<key> AtIndexes :

  • 如果找到 countOf<Key> 和其他两个方法中的任意一个,则会创建一个响应所有 NSArray 方法的集合代理对象,并返回该对象,即 NSKeyValueArray,是NSArray的子类。代理对象随后将接收到的所有 NSArray 消息转换为countOf<Key>objectIn<Key> AtIndex:<key>AtIndexes:消息的某种组合,用来创建键值编码对象。如果原始对象还实现了一个名为get<Key>:range:之类的可选方法,则代理对象也将在适当时使用该方法(注意:方法名的命名规则要符合KVC的标准命名方法,包括方法签名。)
  • 如果没有找到这三个访问数组的方法,则进入下一步

[第三步]
如果没有找到上面的几种方法,则会同时查找countOf <Key>enumeratorOf<Key>memberOf<Key>这三个方法

  • 如果这三个方法都找到,则会创建一个响应所有NSSet方法的集合代理对象,并返回该对象,此代理对象随后将其收到的所有NSSet消息转换为countOf<Key>enumeratorOf<Key>memberOf<Key>:消息的某种组合,用于创建它的对象
  • 如果还是没有找到,则进入下一步

[第四步]
如果还没有找到,检查类方法accessInstanceVariablesDirectly是否YES,依次搜索_<key>_is<Key><key>is<Key>的实例变量

  • 如果找到了,直接获取实例变量的值,进入下一步
  • 如果还没找到,进入[第六步]

[第五步]
根据搜索到的成员变量的值的类型,返回不同的结果:

  • 如果是对象指针,则直接返回结果
  • 如果是NSNumber支持的标量类型,则将其存储在NSNumber实例中并返回它
  • 如果是是NSNumber不支持的标量类型,转换为NSValue对象并返回该对象

[第六步]
如果上面5步的方法均失败,系统会执行该对象的valueForUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常

综上所述,KVC通过 valueForKey:方法取值的流程以设置LGPerson的对象person的属性name为例,如下图所示:

image.png

4.自定义 KVC

原理:通过给 NSObject 添加分类CJLKVC,实现自定义的cjl_setValue:forKey:cjl_valueForKey:方法,根据苹果官方文档提供的查找规则进行实现:

@interface NSObject (KVC)

//设值
- (void)customSetValue:(nullable id)value forKey:(NSString *)key;
//取值
- (nullable id)customValueForKey:(NSString *)key;

@end
4.1 自定义 KVC 设值

自定义KVC设置流程,主要分为以下几个步骤:

  • 1、判断key非空
  • 2、查找setter方法,顺序是:setXXX、_setXXX、 setIsXXX
  • 3、判断是否响应accessInstanceVariablesDirectly方法,即是否访问实例变量,
    返回YES,继续下一步设值,
    如果是NO,则崩溃
  • 4、间接访问变量赋值(只会走一次),顺序是:_key、_isKey、key、isKey
    • 4.1 定义一个收集实例变量的可变数组
    • 4.2 通过class_getInstanceVariable方法,获取相应的 ivar
    • 4.3 通过object_setIvar方法,对相应的ivar设置值
  • 5 、如果找不到相关实例变量,则抛出异常
//设值
- (void)customSetValue:(nullable id)value forKey:(NSString *)key{
    
//    1、判断key 是否存在
    if (key == nil || key.length == 0) return;
    
//    2、找setter方法,顺序是:setXXX、_setXXX、 setIsXXX
    // key 要大写
    NSString *Key = key.capitalizedString;
    // key 要大写
    NSString *setKey = [NSString stringWithFormat:@"set%@:", Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:", Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:", Key];
    
    if ([self performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"*************%@*************", setKey);
        return;
    }else if([self performSelectorWithMethodName:_setKey value:value]){
        NSLog(@"*************%@*************", _setKey);
        return;
    }else if([self performSelectorWithMethodName:setIsKey value:value]){
        NSLog(@"*************%@*************", setIsKey);
        return;
    }
    
    
//    3、判断是否响应`accessInstanceVariablesDirectly`方法,即间接访问实例变量,返回YES,继续下一步设值,如果是NO,则崩溃
    if (![self.class accessInstanceVariablesDirectly]) {
        @throw [NSException exceptionWithName:@"CJLUnKnownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
//    4、间接访问变量赋值,顺序为:_key、_isKey、key、isKey
    // 4.1 定义一个收集实例变量的可变数组
    NSMutableArray *mArray = [self getIvarListName];
    // _<key> _is<Key> <key> is<Key>
    NSString *_key = [NSString stringWithFormat:@"_%@", key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@", key];
    NSString *isKey = [NSString stringWithFormat:@"is%@", key];
    if ([mArray containsObject:_key]) {
        // 4.2 获取相应的 ivar
        Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        // 4.3 对相应的 ivar 设置值
        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;
    }
    
//    5、如果找不到则抛出异常
    @throw [NSException exceptionWithName:@"CJLUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
    
}
4.2 自定义 KVC 取值

取值的自定义代码如下,分为以下几步

  • 1、判断key非空

  • 2、查找相应方法,顺序是:get<Key>、 <key>、 countOf<Key>、 objectIn<Key>AtIndex

  • 3、判断是否能够直接赋值实例变量,即判断是否响应accessInstanceVariablesDirectly方法,间接访问成员变量,

    返回YES,继续下一步取值

    如果是NO,则崩溃

  • 4、间接访问实例变量,顺序是:_<key> _is<Key> <key> is<Key>

    • 4.1 定义一个收集实例变量的可变数组

    • 4.2 通过class_getInstanceVariable方法,获取相应的 ivar

    • 4.3 通过object_getIvar方法,返回相应的ivar 的值

//取值
- (nullable id)cjl_valueForKey:(NSString *)key{
    
//    1、判断非空
    if (key == nil || key.length == 0) {
        return nil;
    }
    
//    2、找到相关方法:get<Key> <key> countOf<Key>  objectIn<Key>AtIndex
    // key 要大写
    NSString *Key = key.capitalizedString;
    // 拼接方法
    NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
    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(getKey)]) {
        return [self performSelector:NSSelectorFromString(getKey)];
    }else if ([self respondsToSelector:NSSelectorFromString(key)]){
        return [self performSelector:NSSelectorFromString(key)];
    }
    //集合类型
    else 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
    
//    3、判断是否响应`accessInstanceVariablesDirectly`方法,即间接访问实例变量,返回YES,继续下一步设值,如果是NO,则崩溃
    if (![self.class accessInstanceVariablesDirectly]) {
        @throw [NSException exceptionWithName:@"CJLUnKnownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
//    4.找相关实例变量进行赋值,顺序为:_<key>、 _is<Key>、 <key>、 is<Key>
    // 4.1 定义一个收集实例变量的可变数组
    NSMutableArray *mArray = [self getIvarListName];
    // 例如:_name -> _isName -> name -> isName
    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);;
    }

    return @"";
    
    return @"";
}

5. 使用 keyPath

在日常开发中,一个类的成员变量有可能是自定义类或者其他的复杂数据类型,一般的操作是,我们可以先通过KVC获取该属性,然后再通过KVC获取自定义类的属性,就是比较麻烦,还有另一种比较简便的方法,就是使用KeyPath,涉及以下两个方法:setValue:forKeyPath:valueForKeyPath:

//通过KeyPath来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;                  

//通过KeyPath来设值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  

比如以下的案例:

//Person类
@interface Person : NSObject
@property (nonatomic, copy)   NSString          *age;
@property (nonatomic, strong) Student         *student;
@end

//Student类
@interface Student : NSObject
@property (nonatomic, copy)   NSString          *name;
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        Student *student = [Student alloc];
        student.name    = @"CJL";
        person.student     = student;
        //根据kvc - keyPath路由修改student的subject属性的值
        [person setValue:@"嘻嘻" forKeyPath:@"student.name"];
        NSLog(@"%@",[person valueForKeyPath:@"student.name"]);
    }
    return 0;
}

//*************打印结果*************
2020-10-27 09:55:08.512833+0800 001-KVC简介[58089:6301894] 改变前:CJL
2020-10-27 09:55:08.512929+0800 001-KVC简介[58089:6301894] 改变后:嘻嘻

6.使用场景

6.1 动态设值和取值
  • 常用的可以通过setValue:forKey:valueForKey:
  • 也可以通过路由的方式setValue:forKeyPath:valueForKeyPath:
6.2 通过KVC访问和修改私有变量

在日常开发中,对于类的私有属性,在外部定义的对象,是无法直接访问私有属性的,但是对于KVC而言,一个对象没有自己的隐私,所以可以通过KVC修改和访问任何私有属性,一般用于修改系统类的私有属性。

6.3 多值操作(model和字典互转)

model和字典的转换可以通过下面两个KVCAPI实现:

//字典转模型
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

//模型转字典
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
6.4 修改一些系统空间的内部属性

在日常开发中,我们知道,很多UI控件都是在其内部由多个UI控件组合而成,这些内部控件苹果并没有提供访问的API,但是使用KVC可以解决这个问题,常用的就是自定义tabbar、个性化UITextField中的placeHolderText

6.5 用KVC实现高阶消息传递

在对容器类使用KVC时,valueForKey:将会被传递给容器中的每一个对象,而不是对容器本身进行操作,结果会被添加到返回的容器中,这样,可以很方便的操作集合 来返回 另一个集合。

//KVC实现高阶消息传递
- (void)transmitMsg{
    NSArray *arrStr = @[@"english", @"franch", @"chinese"];
    NSArray *arrCapStr = [arrStr valueForKey:@"capitalizedString"];
    
    for (NSString *str in arrCapStr) {
        NSLog(@"%@", str);
    }
    
    NSArray *arrCapStrLength = [arrCapStr valueForKeyPath:@"capitalizedString.length"];
    for (NSNumber *length in arrCapStrLength) {
        NSLog(@"%ld", (long)length.integerValue);
    }
}

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

推荐阅读更多精彩内容