美文网首页iOS面试&笔试iOS基础
iOS底层原理探究之----KVC

iOS底层原理探究之----KVC

作者: coolLee | 来源:发表于2019-01-14 18:20 被阅读5次

    不管是平常开发还是找工作面试中,KVC、KVO的原理都是面试官比较喜欢问的问题。最近抽时间研究了一下KVC和KVO的实现原理,本想着一篇文章就可以说完,等研究完才发现不看不知道,一看吓一跳。KVC和KVO都有很多内容可以研究,所以分为两篇分享,第一篇分享KVC的底层原理。
    本次分享准备从这几个方面入手:
    1、概念定义
    2、原理介绍
    3、自己实现
    4、使用场景

    一、概念定义

    KVC:Key-value coding (键-值编码)
    苹果开发者文档中有这么一句话:


    苹果文档

    大意就是要想理解KVO必须首先理解KVC!足可见KVC的重要性。

    概念:允许开发者通过key直接访问对象的属性方法或者成员变量,而不需要调用明确的存取方法。

    实际上,KVC是对NSObject的扩展:NSKeyValueCoding,当然其中对NSArray、NSDictionary、NSMutableDictionary、NSOrderedSet、NSSet也添加了扩展,更方便使用。
    其中主要提供了以下四个方法,当然还有很多其他方法,可以在苹果文档中查看:

    - (nullable id)valueForKey:(NSString *)key;
    - (void)setValue:(nullable id)value forKey:(NSString *)key;
    - (nullable id)valueForKeyPath:(NSString *)keyPath;
    - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
    

    这些也是我们平常使用KVC使用最多的方法。

    二、原理介绍

    不知道大家在平常使用KVC的时候有没有思考这些问题:
    Q1:不管是set还是get,只需要传入一个key字符串就可以存取,为什么?
    Q2:如果类中没有相应的属性是否一定不能存取?
    带着这样的问题我们研究一下KVC的实现原理。通过查阅苹果的官方文档和介绍,我们就可以了解到,在我们看似简单的API调用下,苹果其实是有一套完整的搜索规则,我们分set和get分别说明如下:

    setValue:(id)value forKey:(NSString *)key
    
    • set的搜索规则如下

    1.查找set<Key>:或_set<Key>命名的setter,按照这个顺序,如果找到的话,调用这个方法并将值传进去(根据需要进行对象转换)。
    2.如果没有发现一个简单的setter,但是accessInstanceVariablesDirectly类属性返回YES,则查找一个命名规则为_<key>、_is<Key>、<key>、is<Key>的实例变量。根据这个顺序,如果发现则将value赋值给实例变量。
    3.如果没有发现setter或实例变量,则调用setValue:forUndefinedKey:方法,并默认提出一个异常,但是一个NSObject的子类可以提出合适的行为。

    上面提到了一个类属性accessInstanceVariablesDirectly

     + (BOOL)accessInstanceVariablesDirectly
    

    它表示是否允许读取实例变量的值,如果为YES则在KVC查找的过程中,从内存中读取属性、实例变量的值。如果不允许外界通过KVC对我们的私有属性和成员变量进行操作,则可以设置此值为NO。
    set的规则相对比较简单,相信大家都能看懂。我们也可以按照这个搜索顺序自己验证是否符合。(实现两个setter方法以及添加四种成员变量,然后依次注释掉后检查是不是按照上面的顺序进行赋值操作。)

    • get的搜索规则
      get的搜索规则相对于set就有点复杂了

    1.通过getter方法搜索实例,例如get<Key>, <key>, is<Key>, _<key>的拼接方案。按照这个顺序,如果发现符合的方法,就调用对应的方法并拿着结果跳转到第五步。否则,就继续到下一步。
    2.如果没有找到简单的getter方法,则搜索其匹配模式的方法countOf<Key>、objectIn<Key>AtIndex:、<key>AtIndexes:。如果找到其中的第一个和其他两个中的一个,则创建一个集合代理对象NSKeyValueArray,该对象响应所有NSArray的方法并返回该对象。否则,继续到第三步。代理对象随后将NSArray接收到的countOf<Key>、objectIn<Key>AtIndex:、<key>AtIndexes:的消息给符合KVC规则的调用方。当代理对象和KVC调用方通过上面方法一起工作时,就会允许其行为类似于NSArray一样。
    3.如果没有找到NSArray简单存取方法,或者NSArray存取方法组。则查找有没有countOf<Key>、enumeratorOf<Key>、memberOf<Key>:命名的方法。如果找到三个方法,则创建一个集合代理对象,该对象响应所有NSSet方法并返回。否则,继续执行第四步。此代理对象随后转换countOf<Key>、enumeratorOf<Key>、memberOf<Key>:方法调用到创建它的对象上。实际上,这个代理对象和NSSet一起工作,使得其表象上看起来是NSSet。
    4.如果没有发现简单getter方法,或集合存取方法组,以及接收类方法accessInstanceVariablesDirectly是返回YES的。搜索一个名为_<key>、_is<Key>、<key>、is<Key>的实例,根据他们的顺序。如果发现对应的实例,则立刻获得实例可用的值并跳转到第五步,否则,跳转到第六步。
    5.如果取回的是一个对象指针,则直接返回这个结果。如果取回的是一个基础数据类型,但是这个基础数据类型是被NSNumber支持的,则存储为NSNumber并返回。如果取回的是一个不支持NSNumber的基础数据类型,则通过NSValue进行存储并返回。
    6.如果所有情况都失败,则调用valueForUndefinedKey:方法并抛出异常,这是默认行为。但是子类可以重写此方法。

    其中第二步搜索的意思是:没有找到第一步中的简单getter方法,但是实现了countOf<Key>以及objectIn<Key>AtIndex:、<key>AtIndexes:两个中的其中一个,此时意味着当前对象拥有一个属性名为<key>的NSKeyValueArray类型的属性,它可以响应NSArray的所有方法。到这里其实就可以回答上面提到的第二个问题,一个对象不一定需要显式的写出自己的属性也可以进行存取操作!
    第三步搜索的意思和第二步相似,只是条件更苛刻,且最终返回的是NSSet对象,响应NSSet的所有方法。

    了解了上面的set、get的搜索规则,上面的第一个问题也就回答了,苹果底层会根据你传入的key字符串按照搜索规则进行搜索,并进行存取操作。

    • NSMutableArray 、NSMutableSet和NSMutableOrderedSet对应的搜索规则

    这几种可变集合的搜索规则基本一致,只是搜索时调用的方法不同。详细的搜索方法都可以在KVC官方文档中找到。

    以NSMutableArray为例说明:

    - (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
    

    搜索规则如下:

    1.搜索insertObject:in<Key>AtIndex: , removeObjectFrom<Key>AtIndex: 或者 insert<Key>AtIndexes , remove<Key>AtIndexes 格式的方法。如果至少找到一个insert方法和一个remove方法,那么同样返回一个可以响应NSMutableArray所有方法代理集合(类名是NSKeyValueFastMutableArray2),那么给这个代理集合发送NSMutableArray的方法,以insertObject:in<Key>AtIndex: , removeObjectFrom<Key>AtIndex: 或者 insert<Key>AtIndexes , remove<Key>AtIndexes组合的形式调用。还有两个可选实现的接口:replaceObjectAtIndex:withObject:,replace<Key>AtIndexes:with<Key>:。
    2.如果上面的方法没有找到,则搜索set<Key>: 格式的方法,如果找到,那么发送给代理集合的NSMutableArray最终都会调用set<Key>:方法。 也就是说,mutableArrayValueForKey:取出的代理集合修改后,用set<Key>: 重新赋值回去去。这样做效率会低很多。所以推荐实现上面的方法。
    3.如果上一步的方法还还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),会按_<key>,<key>,的顺序搜索成员变量名,如果找到,那么发送的NSMutableArray消息方法直接交给这个成员变量处理。
    4.如果还是找不到,则调用valueForUndefinedKey:。


    上面调用的方法- (NSMutableArray )mutableArrayValueForKey:(NSString )key;其实有一个很重要的使用场景:KVO监听可变集合的变化**,当然这里的可变集合包括NSMutableArray,NSMutableSet,NSMutableOrderedSet,但不包括NSDictionary。
    通常使用KVO监听某个对象的可变集合属性,当可变集合发生如add、remove等元素操作时,对象并不能收到通知,具体原因可以留到下次分享KVO原理中说明。但是如果使用上面的这种方法获取的可变集合,当内部元素发生变化时也可以收到通知。


    以上就是KVC的内部原理,依据这样的原理,我们下面尝试自己实现系统的KVC机制

    三、自己实现

    原理:给NSObject添加分类,实现自己的set、get方法,在方法中根据苹果定义的搜索规则进行实现。
    这里就以最简单的set和get方法来说明自己实现的思路,权当抛砖引玉
    为NSObject添加自己的KVC分类NSObject (NewKVC)

    NSObject+NewKVC.h
    
    @interface NSObject (NewKVC)
    -(void)setMyValue:(id)value forKey:(NSString*)key;
    -(id)myValueforKey:(NSString*)key;
    @end
    
    NSObject+NewKVC.m
    
    @implementation NSObject (NewKVC)
    - (void)setMyValue:(id)value forKey:(NSString *)key{
        if (key == nil || key.length == 0) {  //验证key
            return;
        }
        if ([value isKindOfClass:[NSNull class]]) {
            [self setNilValueForKey:key]; //完全自定义需要自定义setMyNilValueForKey
            return;
        }
        if (![value isKindOfClass:[NSObject class]]) {
            @throw @"must be s NSObject type";
            return;
        }
      
        NSString* funcName = [NSString stringWithFormat:@"set%@:",key.capitalizedString];
        if ([self respondsToSelector:NSSelectorFromString(funcName)]) {  //默认优先调用set方法
            [self performSelector:NSSelectorFromString(funcName) withObject:value];
            return;
        }
        unsigned int count;
        BOOL flag = false;
        Ivar* vars = class_copyIvarList([self class], &count);
        for (NSInteger i = 0; i<count; i++) {
            Ivar var = vars[i];
            NSString* keyName = [[NSString stringWithCString:ivar_getName(var) encoding:NSUTF8StringEncoding] substringFromIndex:1];
            
            if ([keyName isEqualToString:[NSString stringWithFormat:@"_%@",key]]) {
                flag = true;
                object_setIvar(self, var, value);
                break;
            }
            
            if ([keyName isEqualToString:key]) {
                flag = true;
                object_setIvar(self, var, value);
                break;
            }
        }
        if (!flag) {
            [self setValue:value forUndefinedKey:key];//如果完全自定义,那么需要写一个setMyValue: forUndefinedKey:方法,这里必要性不是很大,就省略了
        }
    }
    
    - (id)myValueforKey:(NSString *)key{
        if (key == nil || key.length == 0) {
            return [NSNull new];
        }
        //这里没有做相关集合的方法查询
        NSString* funcName = [NSString stringWithFormat:@"get%@",key.capitalizedString];
        if ([self respondsToSelector:NSSelectorFromString(funcName)]) {
            return [self performSelector:NSSelectorFromString(funcName)];
        }
        
        unsigned int count;
        BOOL flag = false;
        Ivar* vars = class_copyIvarList([self class], &count);
        for (NSInteger i = 0; i<count; i++) {
            Ivar var = vars[i];
            NSString* keyName = [[NSString stringWithCString:ivar_getName(var) encoding:NSUTF8StringEncoding] substringFromIndex:1];
            if ([keyName isEqualToString:[NSString stringWithFormat:@"_%@",key]]) {
                flag = true;
                return     object_getIvar(self, var);
                break;
            }
            if ([keyName isEqualToString:key]) {
                flag = true;
                return     object_getIvar(self, var);
                break;
            }
        }
        if (!flag) {
            [self valueForUndefinedKey:key];//需要自己实现myValueForUndefinedKey
        }
        return [NSNull new];
    }
    @end
    

    注意.m中需要引入#import <objc/runtime.h>,因为我们需要动态获取当前对象的成员变量,以便存取操作。上面简化版的KVC,只考虑了最简单的情况,如果大家感兴趣,完全可以实现一整套自己的KVC哦。

    四、使用场景

    讲了这么多原理,这么写实现,那KVC到底能用来干啥呢?如果你还不了解KVC的使用,那你就OUT啦
    1.动态地设值和取值
    这个应用就不多说了,最基本的应用
    2.用KVC来访问和修改私有变量
    对于KVC来说,一个对象没有自己的隐私,只要它愿意,就可以修改任何私有的东西。不信可以试试在.m文件中声明私有属性或者成员变量,KVC一样可以获取到。
    3.多值操作(model和字典互转)

    - (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
    - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
    

    主要通过这两个API来实现,也很简单,不多介绍。
    4.修改一些系统控件的内部属性
    使用runtime来获取Apple不想开放的成员变量,利用KVC进行修改。比如自定义tabbar,textfield等,这个的应用也是比较常见。
    5.用KVC实现高阶消息传递
    这个应用场景就比较少了,它的意思是在对容器类使用KVC时,valueForKey:将会被传递给容器中的每一个对象,而不是对容器本身进行操作。比如下面的代码:

        NSArray* arrStr = @[@"english",@"franch",@"chinese"];
        NSArray* arrCapStr = [arrStr valueForKey:@"capitalizedString"];
        for (NSString* str  in arrCapStr) {
            NSLog(@"%@",str);
        }
        NSArray* arrCapStrLength = [arrStr valueForKeyPath:@"capitalizedString.length"];
        for (NSNumber* length  in arrCapStrLength) {
            NSLog(@"%ld",(long)length.integerValue);
        }
    

    打印结果如下:

    在这里插入图片描述
    6.用KVC中的函数来操作集合(集合主要指NSArray和NSSet,不包括NSDictionary)
    集合运算符格式
    上面的图是集合运算符的格式,主要是对象调用valueForKeyPath:方法进行操作。运算符有三种:
    1)简单集合运算符共有@avg, @count , @max , @min ,@sum5种
        NSArray* arrBooks = @[book1,book2,book3,book4];
        NSNumber* sum = [arrBooks valueForKeyPath:@"@sum.price"];
    

    如上,arrBooks种存放的是4个Book对象,[arrBooks valueForKeyPath:@"@sum.price"]的意思就是计算arrBooks中的每个Book对象的price的和。当然还会有:

        NSNumber* avg = [arrBooks valueForKeyPath:@"@avg.price"];
        NSNumber* count = [arrBooks valueForKeyPath:@"@count"];
        NSNumber* min = [arrBooks valueForKeyPath:@"@min.price"];
        NSNumber* max = [arrBooks valueForKeyPath:@"@max.price"];
    

    2)对象运算符

    @distinctUnionOfObjects
    @unionOfObjects
    

    它们的返回值都是NSArray,区别是前者返回的元素都是唯一的,是去重以后的结果;后者返回的元素是全集。
    注意:以上两个方法中,如果操作的属性为nil,在添加到数组中时会导致Crash。
    比如:[arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
    3)Array和Set嵌套操作符

    @distinctUnionOfArrays
    @unionOfArrays
    @distinctUnionOfSets
    

    这种操作是指数组嵌套数组或者Set嵌套Set等的操作,比如:

         NSArray *bookArrs = @[arrBooks,arrBooks];
        [bookArrs valueForKeyPath:@"@distinctUnionOfArrays.price"];
    

    这样就可以取出来多个Book数组中price不同的对象,是不是很赞?

    以上就是我总结出来的一点内容,希望对你有帮助!

    相关文章

      网友评论

        本文标题:iOS底层原理探究之----KVC

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