美文网首页iOS开发技术博客
iOS KVC底层原理分析

iOS KVC底层原理分析

作者: 冼同学 | 来源:发表于2021-09-15 10:47 被阅读0次

    准备工作

    KVC协议定义

    KVCNSKeyValueCoding的简写,键值编码是由NSKeyValueCoding非正式协议启用的一种机制,对象采用该机制来提供对其属性的间接访问。当对象符合键值编码时,其属性可通过字符串参数通过简洁、统一的消息传递接口进行寻址。这种间接访问机制补充了实例变量及其相关访问器方法提供的直接访问。

    KVC在Objective-C中的定义

    KVC的定义都是对NSObject的扩展来实现的,查看setValueForKey方法,发现其在Foundation里面,而Foundation框架是不开源的,只能在苹果官方文档查找。见下图:

    Foundation框架

    KVC提供的API方法

    • 我们可以通过官方提供的文档进行查看(文章开头有链接)
    • 苹果对一些容器类比如NSArray或者NSSet等,KVC有着特殊的实现。

    常用方法

    对于所有继承了NSObject的类型,也就是几乎所有的Objective-C对象都能使用KVC,下面是KVC最为重要的四个方法:

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

    特殊方法

    NSKeyValueCoding类别中还有其他的方法,当我们遇到适合的需求时,就能够派上用场了。方法如下:

    // 默认返回YES,表示如果没有找到Set方法的话,会按照_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;
    

    KVC常用案例

    • 结构体的处理
      KVC在进行结构体处理时,需要用到NSValue,设值时,将结构体封装成NSValue,进行键值设值;取值同样返回NSValue,然后按照结构体格式进行解析,见下面代码:
        // 结构体
        ThreeFloats floats = {1.,2.,3.};
        // 封装成NSValue
        NSValue *value     = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
        // 设值
        [person setValue:value forKey:@"threeFloats"];
    
        // 取值
        NSValue *value1    = [person valueForKey:@"threeFloats"];
        // 结构体解析
        ThreeFloats th;
        [value1 getValue:&th];
        NSLog(@"%f-%f-%f",th.x,th.y,th.z);
    
    • 字典处理(模型转换)
      字典可以实现与模型进行装换,也可以通过键值数组从模型中获取字典数据。实现代码如下:
    - (void)dictionaryTest{
        // 字典
        NSDictionary* dict = @{
                               @"name":@"Cooci",
                               @"nick":@"KC",
                               @"subject":@"iOS",
                               @"age":@18,
                               @"length":@180
                               };
        // 模型
        LGStudent *p = [[LGStudent alloc] init];
        // 字典转模型
        [p setValuesForKeysWithDictionary:dict];
    
        // 键值数组
        NSArray *array = @[@"name",@"age"];
        // 从模型中获取响应的字典数据
        NSDictionary *dic = [p dictionaryWithValuesForKeys:array];
        NSLog(@"%@",dic);
    }
    

    KVC设值取值顺序

    KVC的使用相信是没什么难度的,但是它寻找key的过程是怎么样子的呢?以下就进行分析。

    设值

    当调用setValue:forKey:代码时,会有什么的内部操作呢?我在其官方文章中找到下图:

    官方解释
    上图的意思是:
    setValue:forKey:的默认实现,给定keyvalue参数作为输入,尝试将名为key的属性设置为value,在接收调用的对象内部,使用以下过程:按顺序查找名为 set<Key>:_set<Key> 的第一个访问器。 如果找到,则使用输入值(或根据需要展开的值)调用它并完成。如果未找到简单访问器,并且类方法 accessInstanceVariablesDirectly返回 YES,则按顺序查找名称类似于 _<key>_is<Key><key>is<Key> 的实例变量。 如果找到,直接使用输入值(或解包值)设置变量并完成。
    在未找到访问器或实例变量时,调用 setValue:forUndefinedKey:。 默认情况下,这会引发异常,但 NSObject的子类可能会提供特定于键的行为。

    根据上面的解析可以总结为以下的几点:

    • 按顺序查找名为set<Key>_set<Key>或者setIs<Key>setter访问器顺序查找,如果找到就调用。只要实现任意一个,那么就会将调用这个方法,将属性的值设为传进来的值
    • 如果没有找到这些setter方法,KVC机制会检查+ (BOOL)accessInstanceVariablesDirectly方法有没有返回YES,默认该方法会返回YES,如果重写了该方法让其返回NO的话,那么在这一步KVC会执行setValue:forUndefinedKey:方法。
    • 如果返回YESKVC机制会优先搜索该类里面有没有名为_<Key>的成员变量,无论该变量是在类接口处定义,还是在类实现处定义,也无论用了什么样的访问修饰符,只在存在以_<Key>命名的变量,KVC都可以对该成员变量赋值。
    • KVC机制再会继续搜索_is<Key><key>is<key>的成员变量,再给它们赋值。
    • 如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的setValue:forUndefinedKey:方法,默认是抛出异常。
    [person setValue:@"newName" forKey:@"name"];为例,得出结论:
    • 优先通过setter方法,进行属性设置,调用顺序是:
      • setName
      • _setName
      • setIsName
    • 果以上方法均未找到,并且accessInstanceVariablesDirectly返回YES,则通过成员变量进行设置,顺序是:
      • _name
      • _isName
      • name
      • isName

    注意:以上可以通过案例进行演示,我就不在这里演示了。

    补充说明accessInstanceVariablesDirectly

    尝试重写+ (BOOL)accessInstanceVariablesDirectly方法让其返回NO,如果KVC没有找到set<Key>_set<Key>setIs<Key>相关方法时,会直接用setValue:forUndefinedKey:方法。我们用代码来测试一下上面的KVC机制:

    @interface LGPerson : NSObject
    {
        @public
            NSString *_isName;
            NSString *name;
            NSString *isName;
            NSString *_name;
    }
    @end
    @implementation LGPerson
    
    +(BOOL)accessInstanceVariablesDirectly{
        return NO;
    }
    
    -(id)valueForUndefinedKey:(NSString *)key{
        NSLog(@"出现异常,该key不存在%@",key);
        return nil;
    }
    -(void)setValue:(id)value forUndefinedKey:(NSString *)key{
         NSLog(@"出现异常,该key不存在%@",key);
    }
    
    // 设置方法全部注释掉
    // -(void)setName:(NSString*)name{
    //     toSetName = name;
    // }
    // - (void)_setName:(NSString *)name{
    //     NSLog(@"%s - %@",__func__,name);
    // }
    // - (void)setIsName:(NSString *)name{
    //     NSLog(@"%s - %@",__func__,name);
    // }
    
    
    @end
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            LGPerson* person = [LGPerson new];
            [person setValue:@"NewName" forKey:@"name"];
            NSString* name = [person valueForKey:@"name"];
            NSLog(@"value for key : %@",name);
    
            NSLog(@"取值_name:%@",person->_name);
            NSLog(@"取值_isName:%@",person->_isName);
            NSLog(@"取值name:%@",person->name);
            NSLog(@"取值isName:%@",person->isName);
        }
        return 0;
    }
    

    运行结果:


    运行结果

    这说明了重写+(BOOL)accessInstanceVariablesDirectly方法让其返回NO后,KVC找不到set<Key>等方法后,不再去找<Key>系列成员变量,而是直接调用setValue:forUndefinedKey:,如果我们自身的类不需要KVC机制的话可以这样子写。

    KVC设值流程图
    KVC设值流程图

    取值

    同理,在调用valueForKey:时候会发生什么呢?根据官方的文档得出:

    valueForKey官方文档描述
    根据官方文档得出valueForKey:的机制如下:
    • 首先按get<Key><Key>is<Key>_<Key>的顺序方法查找getter方法,找到的话会直接调用,如果是BOOL或者Int等值类型, 会将其包装成一个NSNumber对象。
    • 如果上面的getter没有找到,KVC则会查找countOf<Key>objectIn<Key>AtIndex<Key>AtIndexes格式的方法。如果countOf<Key>方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子类),调用这个代理集合的方法,或者说给这个代理集合发送属于NSArray的方法,就会以countOf<Key>objectIn<Key>AtIndexAt<Key>Indexes这几个方法组合的形式调用。还有一个可选的get<Key>:range:方法。所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名
    • 如果上面的方法没有找到,那么会同时查找countOf<Key>enumeratorOf<Key>memberOf<Key>格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合,和上面一样,给这个代理集合发NSSet的消息,就会以countOf<Key>enumeratorOf<Key>memberOf<Key>组合的形式调用。
    • 如果还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),那么和先前的设值一样,会按_<Key>_is<Key><Key>is<Key>的顺序搜索成员变量名,这里不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱。如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly返回NO的话,那么会直接调用valueForUndefinedKey:
    • 还没有找到的话,调用valueForUndefinedKey:
    [person valueForKey:@"name"];为例
    • getter方法的调用顺序是:
      • getName
      • name
      • isName
      • _name
    • 如果以上方法没有找到,accessInstanceVariablesDirectly返回YES,则直接返回成员变量,获取顺序依然是:
      • _name
      • _isName
      • name
      • isName

    注意:以上可以通过案例进行演示,我就不在这里演示了。

    KVC取值流程图
    KVC取值流程
    代码验证KVC取值(需要的话就拷贝运行即可)
        @interface LGPerson : NSObject
        {
            @public
                NSString *_isName;
                NSString *name;
                NSString *isName;
                NSString *_name;
        }
        @end
        @implementation LGPerson
    
        +(BOOL)accessInstanceVariablesDirectly{
            return NO;
        }
    
        -(id)valueForUndefinedKey:(NSString *)key{
            NSLog(@"出现异常,该key不存在%@",key);
            return nil;
        }
        -(void)setValue:(id)value forUndefinedKey:(NSString *)key{
             NSLog(@"出现异常,该key不存在%@",key);
        }
    
        // 设置方法全部注释掉
        // -(void)setName:(NSString*)name{
        //     toSetName = name;
        // }
        // - (void)_setName:(NSString *)name{
        //     NSLog(@"%s - %@",__func__,name);
        // }
        // - (void)setIsName:(NSString *)name{
        //     NSLog(@"%s - %@",__func__,name);
        // }
    
        // 取值方法
        //- (NSString *)getName{
        //    return NSStringFromSelector(_cmd);
        //}
        //- (NSString *)name{
        //    return NSStringFromSelector(_cmd);
        //}
        //- (NSString *)isName{
        //    return NSStringFromSelector(_cmd);
        //}
        //- (NSString *)_name{
        //    return NSStringFromSelector(_cmd);
        //}
        @end
    
        int main(int argc, const char * argv[]) {
            @autoreleasepool {
                // insert code here...
                LGPerson* person = [LGPerson new];
                [person setValue:@"NewName" forKey:@"name"];
                NSString* name = [person valueForKey:@"name"];
                NSLog(@"value for key : %@",name);
    
                NSLog(@"取值_name:%@",person->_name);
                NSLog(@"取值_isName:%@",person->_isName);
                NSLog(@"取值name:%@",person->name);
                NSLog(@"取值isName:%@",person->isName);
            }
            return 0;
        }
    

    在KVC中使用keyPath

    除了对当前对象的属性进行赋值外,还可以对其更深层的对象进行赋值。例如,对当前对象的location属性的country属性进行赋值。KVC进行多级访问时,直接类似于属性调用一样用点语法进行访问即可。

        [person setValue:@"" forKeyPath:@"location.country"];
    

    通过keyPath对数组进行取值时,并且数组中存储的对象类型都相同,可以通过valueForKeyPath:方法指定取出数组中所有对象的某个字段。例如下面例子中,通过valueForKeyPath:将数组中所有对象的name属性值取出,并放入一个数组中返回。

        NSArray *names = [array valueForKeyPath:@"name"];
    
    例子展示以及运行结果

    异常处理

    当根据KVC搜索规则,没有搜索到对应的key或者keyPath,则会调用对应的异常方法。异常方法的默认实现,在异常发生时会抛出一个异常,并且应用程序Crash。见下图:

    crash现象
    重写以下两个方法,防止crash发生:
        -(id)valueForUndefinedKey:(NSString *)key{
            NSLog(@"出现异常,该key不存在%@",key);
            return nil;
        }
        
        -(void)setValue:(id)value forUndefinedKey:(NSString *)key{
             NSLog(@"出现异常,该key不存在%@",key);
        }
    

    再次运行程序,发现不再崩溃:

    再次运行程序
    为了合理处理KVC发出的异常,我们还可以这样子处理:
    - (void)setNilValueForKey:(NSString *)key {
        if ([key isEqualToString:@"name"]) {
            [self setValue:@"" forKey:@”age”];
        } else {
            [super setNilValueForKey:key];
        }
    }
    

    自定义KVC的实现

    根据苹果官方文档提供的设值、取值规则,我们可以自己进行KVC的自定义实现。见下面实现代码:

    // KVC 自定义
    @implementation NSObject (LGKVC)
    
    // 设置
    - (void)lg_setValue:(nullable id)value forKey:(NSString *)key{
        // 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 lg_performSelectorWithMethodName:setKey value:value]) {
            NSLog(@"*********%@**********",setKey);
            return;
        }else if ([self lg_performSelectorWithMethodName:_setKey value:value]) {
            NSLog(@"*********%@**********",_setKey);
            return;
        }else if ([self lg_performSelectorWithMethodName:setIsKey value:value]) {
            NSLog(@"*********%@**********",setIsKey);
            return;
        }
    
        // 3: 判断是否响应 accessInstanceVariablesDirectly 返回YES NO 奔溃
        // 3:判断是否能够直接赋值实例变量——NO
        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];
    }
    
    // 取值
    - (nullable id)lg_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:判断是否能够直接赋值实例变量-YES、NO
        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 @"";
    }
    
    #pragma mark **- 相关方法**
    
    - (BOOL)lg_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;
    }
    
    - (id)performSelectorWithMethodName:(NSString *)methodName{
    
        if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            return [self performSelector:NSSelectorFromString(methodName) ];
    #pragma clang diagnostic pop
        }
        return nil;
    }
    
    - (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
    

    相关文章

      网友评论

        本文标题:iOS KVC底层原理分析

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