iOS-底层原理20:KVC底层原理

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

API

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

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

//通过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;

使用

@class LBHStudent;
@interface LBHPerson : NSObject
@property (nonatomic, copy)   NSString          *age;
@property (nonatomic, strong) LBHStudent        *student;
@end

@implementation LBHPerson

@end

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

@implementation LBHStudent

@end

通过 setValue: forKey:valueForKey:来设值和取值

LBHPerson *person = [[LBHPerson alloc] init];
        
person.age = @"6";
        
NSLog(@"= %@",[person valueForKey:@"age"]);
        
[person setValue:@"8" forKey:@"age"];
        
NSLog(@"= %@",[person valueForKey:@"age"]);

打印

= 6
= 8

通过setValue: forKeyPath:valueForKeyPath: 来设值和取值

LBHStudent *student = [[LBHStudent alloc] init];
student.name    = @"liu";
person.student     = student;
        
NSLog(@"= %@",[person valueForKeyPath:@"student.name"]);
        
[person setValue:@"嘻嘻" forKeyPath:@"student.name"];
        
NSLog(@"= %@",[person valueForKeyPath:@"student.name"]);

打印

= liu
= 嘻嘻

KVC 设值 底层原理

进入setValue:forKey的声明,发现是在Foundation框架中,而Foundation框架是不开源的,有以下几种方式可以去探索底层:

  • 通过Hopper反汇编,查看伪代码
  • 通过苹果官方文档
  • Github搜索是否有相关的demo

我们通过苹果官方文档来研究。

通过文档获取流程

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

step1: 首先查找是否有这三种setter方法,按照查找顺序为set<Key>:--> _set<Key> --> setIs<Key>

step2: 如果没有第一步中的三个简单的setter方法,如果accessInstanceVariablesDirectly是否返回YES,则查找间接访问的实例变量进行赋值,查找顺序为:_<key> --> _is<Key> --> <key> --> is<Key>

step3: 如果没有找到 setter 或实例变量,则调用 setValue:forUndefinedKey: 方法,并默认抛出一个异常

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

测试

step1: 新建一个LBHPerson类,添加文档中那些成员变量和方法

@interface LBHPerson : NSObject
{
    @public
    NSString *_isName;
    NSString *name;
    NSString *isName;
    NSString *_name;
}

@end

@implementation LBHPerson

//开启或关闭实例变量赋值
+ (BOOL)accessInstanceVariablesDirectly
{
    return YES;
}

- (void)setName:(NSString *)name{
    NSLog(@"%s - %@",__func__, name);
}

- (void)_setName:(NSString *)name{
    NSLog(@"%s - %@",__func__, name);
}

- (void)setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__, name);
}

@end

step2: 调用

LBHPerson *person = [[LBHPerson alloc] init];   
[person setValue:@"liu" forKey:@"name"];

step3: 运行

输出结果

-[LBHPerson setName:] - liu

step4:setName方法注释掉继续运行

-[LBHPerson _setName:] - liu

step5:_setName方法注释掉继续运行

-[LBHPerson setIsName:] - liu

step6:setIsName方法注释掉

啥都没有

step7:accessInstanceVariablesDirectly返回改为NO,继续运行

崩溃了

step8:accessInstanceVariablesDirectly返回改为YES,并在调用处添加如下代码

NSLog(@"==%@",person->_name);
NSLog(@"==%@",person->_isName);
NSLog(@"==%@",person->name);
NSLog(@"==%@",person->isName);

step9: 运行

==liu
==(null)
==(null)
==(null)

step10: 注释掉变量_name,继续运行

==liu
==(null)
==(null)

step11: 注释掉变量_isName,继续运行

==liu
==(null)

step12: 注释掉变量name,继续运行

==liu

以上流程可以反复运行验证

KVC 取值 底层原理

我们同样可以根据官方文档分析KVC取值的底层原理

当调用valueForKey:时,其底层的执行流程如下:

step1: 首先按 get<Key> --> <key> --> is<Key> --> _<key>的顺序查找 getter 方法,如果找到,则进入step5,如果没有找到,则进入step2

step2: 若上面的 getter没有找到,则查找 countOf<Key>objectIn<Key>AtIndex:<Key>AtIndexes 格式的方法。如果找到其中一个,则会创建一个响应所有NSArray方法的集合代理对象,并返回该对象,即NSKeyValueArray,是NSArray的子类。代理对象随后将接收到的所有NSArray消息转换为countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes:消息的某种组合,用来创建键值编码对象。如果原始对象还实现了一个名为get<Key>:range:之类的可选方法,则代理对象也将在适当时使用该方法。如果没有找到这三个访问数组的,请继续进入step3

step3: 还没查到,那么查找 countOf<Key>enumeratorOf<Key>memberOf<Key>: 格式的方法。如果这3个方法都找到,那么返回一个可以相应NSSet所有方法的集合代理。发送给这个代理集合的NSSet消息方法,就会以countOf<Key>、enumeratorOf<Key>、memberOf<Key>: 组合的形式调用。如果还是没有找到,则进入step4

step4: 还是没查到,那么如果类方法 accessInstanceVariablesDirectly返回YES,那么按_<key> --> _is<Key> --> <key> --> is<Key> 的顺序直接搜索实例变量。如果搜到,直接获取实例变量的值,进入step5,否则进入step6

step5: 根据搜索到的属性值的类型,返回不同的结果。如果是对象指针,则直接返回结果;如果是NSNumber支持的标量类型,则将其存储在NSNumber实例中并返回它;如果是是NSNumber不支持的标量类型,请转换为NSValue对象并返回该对象。

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

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

2251862-fa926517a617b1aa.png

测试

step1:LBHPerson中添加以下方法

- (NSString *)getName{
    return NSStringFromSelector(_cmd);
}

- (NSString *)name{
    return NSStringFromSelector(_cmd);
}

- (NSString *)isName{
    return NSStringFromSelector(_cmd);
}

- (NSString *)_name{
    return NSStringFromSelector(_cmd);
}

step2: 如果是用上面的demo,注释掉调用方法,并输出取值

NSLog(@"取值:%@",[person valueForKey:@"name"]);

step3: 运行

取值:getName

step4: 注释掉getName方法实现,继续运行

取值:name

step5: 注释掉name方法实现,继续运行

 取值:isName

step6: 注释掉isName方法实现,继续运行

取值:_name

step7: 注释掉_name方法实现,在打印方法前加上如下代码并继续运行

person->_name = @"_name";
person->_isName = @"_isName";
person->name = @"name";
person->isName = @"isName";

运行结果

取值:_name

step8: 注释掉person->_name = @"_name";这行代码,继续运行

取值:(null)

为什么取值是空的?

因为变量_name还在,没有给它赋值所以为null,我们需要将对应的成员变量也注释掉

step9: 注释掉成员变量_name,继续运行

取值:_isName

step10: 注释掉成员变量_isName和赋值,继续运行

取值:name

step11: 注释掉成员变量name和赋值,继续运行

取值:isName

如果将accessInstanceVariablesDirectly返回值改为NO,运行

如果没有getter方法,将accessInstanceVariablesDirectly改为NO,程序将会崩溃。

可以反复运行验证流程。

自定义 KVC 设值

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

step1: 判断key非空

step2: 查找setter方法,顺序是:set<Key> --> _set<Key> --> setIs<Key>

step3: 判断是否响应accessInstanceVariablesDirectly方法,即间接访问实例变量,返回YES继续下一步设值;如果是NO,则崩溃

step4: 间接访问变量赋值,顺序是:_<Key> --> _is<Key> --> <Key> --> is<Key>

4.1: 定义一个收集实例变量的可变数组
4.2: 通过class_getInstanceVariable方法,获取相应的 ivar
4.3: 通过object_setIvar方法,对相应的 ivar 设置值

step5: 如果找不到相关实例变量,则抛出异常

相关代码

新建一个NSObject分类,#import <objc/runtime.h>

//.h
@interface NSObject (KVC)

// KVC 自定义入口
- (void)lbh_setValue:(nullable id)value forKey:(NSString *)key;

@end

//.m
@implementation NSObject (KVC)

- (void)lbh_setValue:(nullable id)value forKey:(NSString *)key
{
    // KVC 自定义
    // 1: 判断什么 key
    if (key == nil || key.length == 0) {
        return;
    }
    
    // 2: setter set<Key>: or _set<Key>,
    // key 首字母大写
    NSString *Key = key.capitalizedString;
    // 拼接方法
    NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
    
    if ([self lbh_performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"*********%@**********",setKey);
        return;
    }else if ([self lbh_performSelectorWithMethodName:_setKey value:value]) {
        NSLog(@"*********%@**********",_setKey);
        return;
    }else if ([self lbh_performSelectorWithMethodName:setIsKey value:value]) {
        NSLog(@"*********%@**********",setIsKey);
        return;
    }
    
    // 3: 判断是否响应 accessInstanceVariablesDirectly 返回YES NO 奔溃
    // 3:判断是否能够直接赋值实例变量
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
    // 4: 间接变量
    // 获取 ivar -> 遍历 containsObjct -
    // 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:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
    
}


#pragma mark - 相关方法
- (BOOL)lbh_performSelectorWithMethodName:(NSString *)methodName value:(id)value{
 
    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
        
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic pop
        return YES;
    }
    return NO;
}

- (NSMutableArray *)getIvarListName{
    
    NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
    unsigned int count = 0;
    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];
        NSLog(@"ivarName == %@",ivarName);
        [mArray addObject:ivarName];
    }
    free(ivars);
    return mArray;
}

@end

LBHPerson类中注释掉的setter方法和打印方法打开,可以按照上面的设值流程一步步测试

自定义 KVC 取值

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

step1: 判断key非空

step2: 查找相应方法,顺序是:get<Key> --> <key> --> countOf<Key> --> objectIn<Key>AtIndex

step3: 判断是否能够直接赋值实例变量,即判断是否响应accessInstanceVariablesDirectly方法,间接访问实例变量,返回YES继续下一步取值,如果是NO,则崩溃

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

4.1 定义一个收集实例变量的可变数组
4.2 通过class_getInstanceVariable方法,获取相应的 ivar
4.3 通过object_getIvar方法,返回相应的 ivar 的值

相关代码

- (nullable id)lbh_valueForKey:(NSString *)key{
    
    // 1:刷选key 判断非空
    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:判断是否能够直接赋值实例变量
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
    // 4.找相关实例变量进行赋值
    // 4.1 定义一个收集实例变量的可变数组
    NSMutableArray *mArray = [self getIvarListName];
    // _<key> _is<Key> <key> is<Key>
    // _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 @"";
}

LBHPerson类中注释掉的getter方法和打印方法打开,可以按照上面的取值流程一步步测试

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容