美文网首页iOS
iOS-底层原理20:KVC底层原理

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

作者: AcmenL | 来源:发表于2020-12-16 20:04 被阅读0次

    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方法和打印方法打开,可以按照上面的取值流程一步步测试

    相关文章

      网友评论

        本文标题:iOS-底层原理20:KVC底层原理

        本文链接:https://www.haomeiwen.com/subject/qxxogktx.html