美文网首页
第二十一节—KVC(二)原理探索

第二十一节—KVC(二)原理探索

作者: L_Ares | 来源:发表于2020-11-08 23:14 被阅读0次

    本文为L_Ares个人写作,以任何形式转载请表明原文出处。

    资料准备 : AppleDevelopment - KVC文档

    代码准备 : 创建一个Project--->App。创建JDPerson类。添加NSString类型的成员变量_name_isNamenameisName

    上一节主要介绍了一些KVC的基本信息,包括知道了KVC本身是一种间接访问机制,提供的是直接访问实例变量的settergetter,还有一些常见或者常用的API

    本节依然从文档入手,探索KVC的访问模式是怎样的。

    首先从最最常见的setValueForKey来看,在我们对一个类的某个属性利用KVC进行赋值的时候,比如说JDPerson有个name属性,访问的一般是namesetter或者getter,那么这就会影响对KVC本身的探索,所以为了纯净环境下探索KVC的访问模式,我就选择相对更纯净一点的成员变量方式,而不是通过属性的方式。

    一、KVC的访问模式——设值过程

    先看一下官方文档里面对于KVC访问模式中的Setter的一些知识。

    图1.0.0.png

    根据自己的理解,加上翻译,可以将官方文档给出的三个知识点总结 :

    【前言】 : setValue:forKey:这个方法的默认实现,给定keyvalue作为输入参数,尝试将名为key的变量的值设置为value(对于非对象属性,也就是上一节中见到过的结构体,是需要进行一步包装的),设置的流程如下所述 :

    • 第一,先寻找set_set方法,如果找到了就调用set或者_set方法进行赋值。
    • 第二,如果没有找到set或者_set方法,并且调用KVC的对象的类的accessInstanceVariablesDirectly方法返回值是YES,那么就按照顺序查找具有如下名称的实例变量(拿name举例),_name_isNamenameisName。如果找到这四个之一,直接对其进行赋值为value,并且完成赋值。
    • 第三,如果第一,第二条全都不满足,那就调用setValue:forUndefinedKey:。本来默认是会抛出异常的,但是NSObject的一个子类提供了这个方法来处理异常的key

    按照步骤,举个例子 :

    (1). 验证上面的第1点 :

    JDPerson.h

    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface JDPerosn : NSObject
    
    {
        @public
        NSString *_name;
        NSString *_isName;
        NSString *name;
        NSString *isName;
    }
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    JDPerson.m

    #import "JDPerosn.h"
    
    @implementation JDPerosn
    
    #pragma mark - 开启或关闭实例变量的赋值
    //默认就是YES,设置为NO的话,如果没有set或_set的方法,那么就无法对实例变量进行赋值
    +(BOOL)accessInstanceVariablesDirectly
    {
        return YES;
    }
    
    #pragma mark - KVC - setKey流程
    //这就是文档中说的set<key>
    - (void)setName:(NSString *)name
    {
        NSLog(@"%s---%@",__func__,name);
    }
    //_set<key>
    - (void)_setName:(NSString *)name
    {
        NSLog(@"%s---%@",__func__,name);
    }
    
    - (void)setIsName:(NSString *)name
    {
        NSLog(@"%s---%@",__func__,name);
    }
    @end
    
    

    ViewController.m

    - (void)viewDidLoad {
        
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        JDPerosn *person = [[JDPerosn alloc] init];
        
        //1. KVC - 设置值的过程
        [person setValue:@"LJD" forKey:@"name"];
        NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,
                             person->name,person->isName);
        NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
        NSLog(@"%@-%@",person->name,person->isName);
        NSLog(@"%@",person->isName);
        
    }
    

    执行结果 :


    图1.0.1.png

    可以再把- (void)setName:(NSString *)name方法注释掉,会发现调用- (void)_setName:(NSString *)name,如果把它也注释掉,则会调用- (void)setIsName:(NSString *)name。4个实例变量的值依然都是null

    这就验证了上面说的第一点,在利用KVC设置值的时候,会先找到set或者_set方法对其进行赋值。

    (2). 验证上面的第二点 :

    JDPerson.m中的有关set的3个方法全部注释掉,然后让accessInstanceVariablesDirectly变成return NO。其余代码不变,再执行。结果如下图 :

    图1.0.2.png

    所以验证了第二点中,没有set_set方法的时候,accessInstanceVariablesDirectly要设置成YES才可以。

    然后将accessInstanceVariablesDirectly重新设置成YES。再执行。结果如下图 :

    图1.0.2.png

    验证了第二点中会先给_name赋值。
    然后依次的注释掉JDPerson.h中的成员变量_name_isNamename
    就会验证第二点中,KVC赋值同名变量的顺序是_name_isNamenameisName

    第三点我就不验证了,因为经常会用到,比如字典转模型的时候,大家应该常用。

    另外,KVC为什么有第二点这种设计呢?因为在编译期的时候,底层也会生成一些这样的变量,比如之前我们在前面的章节进行clang.m文件编译成.cpp文件的时候,有一些属性就会变成了_xxx的形式的成员变量,或者BOOL类型的值会变成isXXX的形式,这也是因为运行时的特性造成的。

    总结 :

    KVC设值流程.png

    二、KVC的访问模式——取值过程

    一样先来查阅官方文档,然后根据自己的理解加上翻译,再做总结。

    图2.0.0.png

    明显getter的过程比较多。还是按照官方的分成六点。

    【前言】 : valueForKey:的默认实现,给定key作为输入参数,执行下列程序,从接收valueForKey:调用的类实例的内部操作。

    • 第一,按照顺序查找,是否有get<Key>, <key>, is<Key>,_<key>这些方法。如果找到了就调用它,并且在下面的第五点中处理结果。如果找不到就进入第二点
    • 第二,如果第一点中的4个get方法都没实现,在实例方法中查找countOf<Key>objectIn<Key>AtIndex:还有<key>AtIndexes:方法。
      • 如果countOf<Key>存在,并且objectIn<Key>AtIndex:<key>AtIndexes:中至少一个方法存在,那么创建一个响应所有的NSArray方法的集合代理对象,并且返回这个集合代理对象
      • 否则进入第三点
      • 集合代理对象随后会将所有接收到的NSArray的消息都转换为countOfobjectInAtIndex:AtIndexes:的一些组合,这些组合将消息发送给创建它的符合KVC机制的对象。
      • 如果原始对象还实现了一个名为get:range:的可选方法,代理对象也会在适当的时候使用它。
      • 实际上,代理对象与和KVC兼容的对象一起工作,允许底层属性像NSArray一样工作,即使它不是NSArray
    • 第三,如果没有找到第二点中的3个方法,则同时查找countOf <Key>,enumeratorOf<Key>和memberOf<Key>这三个方法,
      • 如果这三个方法都找到了,创建一个响应所有NSSet方法的集合代理对象,并将集合代理对象返还。
      • 如果这三个方法也没找到,那么就直接进入第四点
      • 集合代理对象随后将它接收到的任何NSSet消息转换为countOfenumeratorOfmemberOf:消息的组合,并发送给创建它的对象。
      • 实际上,代理对象与遵循KVC的对象一起工作,允许底层属性像NSSet一样运行,即使它不是NSSet
    • 第四,如果get方法和集合方法都没找到,并且接收者的类方法accessinstancevariables返回YES,则按照顺序查找_<key>, _is<Key>, <key>, is<Key>变量。如果找到,直接获取实例变量的值并进入第五点。否则,进入第六点
    • 第五,根据搜索到的属性值的类型,返回不同的结果
      • 如果是对象指针,则直接返回结果。
      • 如果值是NSNumber支持的标量类型,将其存储在NSNumber实例中并返回。
      • 如果结果是NSNumber不支持的标量类型,转换为NSValue对象并返回它。
    • 第六,如果上面的所有方法都没有找到,调用setValue:forUndefinedKey:。默认情况下会抛出异常,可以执行这个方法来处理。

    因为这里明显的能看出来,简单类型取值和集合类型是不一样的,先看简单类型,按照步骤,举个例子 :

    (1). 验证上面的第1点和第4点 :

    JDPerson.h不发生改变。

    JDPerson.m

    #pragma mark - 开启或关闭实例变量的赋值
    //默认就是YES,设置为NO的话,如果没有set或_set的方法,那么就无法对实例变量进行赋值
    +(BOOL)accessInstanceVariablesDirectly
    {
        return YES;
    }
    
    #pragma mark - KVC - getKey流程
    //文档中说的get<key>
    - (NSString *)getName
    {
        return NSStringFromSelector(_cmd);
    }
    //\<key>
    - (NSString *)name
    {
        return NSStringFromSelector(_cmd);
    }
    //is<key>
    - (NSString *)isName
    {
        return NSStringFromSelector(_cmd);
    }
    //_<key>
    - (NSString *)_name
    {
        return NSStringFromSelector(_cmd);
    }
    
    

    ViewController.m

    - (void)viewDidLoad {
        
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        [self jd_kvc_getter];
        
        
    }
    
    - (void)jd_kvc_getter
    {
        JDPerosn *person = [[JDPerosn alloc] init];
        person->_name   = @"_name";
        person->_isName = @"_isName";
        person->name    = @"name";
        person->isName  = @"isName";
        NSLog(@"KVC取值---%@",[person valueForKey:@"name"]);
    }
    
    - (void)jd_kvc_setter
    {
        JDPerosn *person = [[JDPerosn alloc] init];
        
        //1. KVC - 设置值的过程
        [person setValue:@"LJD" forKey:@"name"];
        NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,
                             person->name,person->isName);
        NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
        NSLog(@"%@-%@",person->name,person->isName);
        NSLog(@"%@",person->isName);
    }
    
    

    执行结果 :

    图2.0.1.png

    然后依次注释掉4个get方法,就可以验证第一点,并且全部注释掉以后会走到第四点,顺便把第四点也验证了。

    从验证可以看出,valueForKey先找的不是我们对变量的赋值,而是先找的getter方法,只有getter方法没有实现的情况下,才会找到变量直接拿值。简单类型也就是非集合类型的只会找到第一点第四点如果有错误会直接到第六点

    (1). 验证上面的第2点和第3点 :

    因为第二,第三点的原理是一样的,这里就拿更常见的数组来说明,NSSet的例子会放出来,但是就不说明了。

    JDPerson.h

    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface JDPerosn : NSObject
    
    {
        @public
        NSString *_name;
        NSString *_isName;
        NSString *name;
        NSString *isName;
        NSArray  *arr;
        NSSet    *set;
    }
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    JDPerson.m

    #pragma mark - KVC - 正常的数组取值
    - (NSUInteger)countOfArr
    {
        NSLog(@"%s",__func__);
        return [arr count];
    }
    
    - (id)objectInArrAtIndex:(NSUInteger)index
    {
        NSLog(@"%s",__func__);
        return [NSString stringWithFormat:@"objectInArrAtIndex : %lu",index];
    }
    
    #pragma mark - KVC - 正常的集合取值
    - (NSUInteger)countOfSet
    {
        NSLog(@"%s",__func__);
        return [set count];
    }
    
    - (id)memberOfSet:(id)object
    {
        NSLog(@"%s",__func__);
        return [set containsObject:object] ? object : nil;
    }
    
    - (id)enumeratorOfSet
    {
        NSLog(@"%s",__func__);
        return [set objectEnumerator];
    }
    
    
    
    #pragma mark - KVC - 没有Pens,但是我有方法,一样可以取值数组
    - (NSUInteger)countOfPens
    {
        NSLog(@"%s",__func__);
        return [arr count];
    }
    
    - (id)objectInPensAtIndex:(NSUInteger)index
    {
        NSLog(@"%s",__func__);
        return [NSString stringWithFormat:@"objectInPensAtIndex %lu", index];
    }
    
    - (id)pensAtIndexes:(NSUInteger)index
    {
        NSLog(@"%s",__func__);
        return [NSString stringWithFormat:@"pensAtIndexes %lu", index];
    }
    
    #pragma mark - KVC - 没有books,但是我有方法,一样可以取值集合
    // 个数
    - (NSUInteger)countOfBooks{
        NSLog(@"%s",__func__);
        return [arr count];
    }
    
    // 是否包含这个成员对象
    - (id)memberOfBooks:(id)object {
        NSLog(@"%s",__func__);
        return [set containsObject:object] ? object : nil;
    }
    
    // 迭代器
    - (id)enumeratorOfBooks {
        // objectEnumerator
        NSLog(@"来了 迭代编译");
        return [arr reverseObjectEnumerator];
    }
    

    ViewController.m

    - (void)jd_kvc_array_and_set
    {
        JDPerosn *person = [[JDPerosn alloc] init];
        person->arr = @[@"pen0", @"pen1", @"pen2", @"pen3"];
        NSLog(@"正常的数组取值 : %@",[person valueForKey:@"arr"]);
        NSArray *array = [person valueForKey:@"pens"];
        NSLog(@"就算没有pens,只要在类中实现了方法也可以objectAtIndex : %@",[array objectAtIndex:1]);
        NSLog(@"就算没有pens,只要在类中实现了方法也可以containsObject : %d",[array containsObject:@"pen1"]);
    
        person->set = [NSSet setWithArray:person->arr];
        NSLog(@"正常的集合取值 : %@",[person valueForKey:@"set"]);
        NSSet *set = [person valueForKey:@"books"];
        [set enumerateObjectsUsingBlock:^(id  _Nonnull obj, BOOL * _Nonnull stop) {
            NSLog(@"set遍历 %@",obj);
        }];
        
    }
    

    执行结果就不贴图了,太长了,可以自己运行一下,会发现count的方法都会打印两次,也证明了的确是存在着代理集合的。所以验证也是符合的。

    总结 :

    KVCvalueForKey的流程主要还是分了简单对象和集合对象,简单对象的取值和其setValueForKey是一样的。而集合对象则多需要几步骤,但是可以防止取到不认识的key,也可以修改取值的结果。

    kvc-getter简单类型流程图.png

    三、KVC的一些特殊功能

    1. KVC有自动转换类型的功能。

    JDPerson.h :

    typedef struct {
        float x, y, z;
    } ThreeFloats;
    
    @interface JDPerosn : NSObject
    
    {
        @public
        NSString *_name;
        NSString *_isName;
        NSString *name;
        NSString *isName;
        NSArray  *arr;
        NSSet    *set;
        int      age;
        ThreeFloats  threeFloats;
    }
    
    @end
    
    1.1 NSNumber支持的标量类型
        //1. NSNumber支持的标量类型
        JDPerosn *person = [[JDPerosn alloc] init];
        [person setValue:@18 forKey:@"age"];
        NSLog(@"%@-%@",[person valueForKey:@"age"],[[person valueForKey:@"age"] class]);//__NSCFNumber
        [person setValue:@"20" forKey:@"age"];
        NSLog(@"%@-%@",[person valueForKey:@"age"],[[person valueForKey:@"age"] class]);//__NSCFNumber
    
    图3.1.0.png

    证明了 : KVC在对已知类型的value设置的时候,如果类型是NSNumber支持的标量类型,则会将value存储到NSNumber的实例中,在取值的时候返回NSNumber实例。

    1.2 NSNumber不支持的标量类型
        //2. NSNumber不支持的标量类型
        ThreeFloats floats = {1.f, 2.f, 3.f};
        NSValue *value  = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
        [person setValue:value forKey:@"threeFloats"];
        NSLog(@"%@-%@",[person valueForKey:@"threeFloats"],[[person valueForKey:@"threeFloats"] class]);//NSConcreteValue
    
    图3.1.1.png

    证明了 : KVC存储的类型如果是NSNumber不支持的标量类型,那么就要转换为NSValue存储并且返回。

    2. 空值

    JDPerson.m中添加KVC监控 :

    - (void)setNilValueForKey:(NSString *)key{
        NSLog(@"设置 %@ 是空值",key);
    }
    
    - (void)setValue:(id)value forUndefinedKey:(NSString *)key{
        NSLog(@"没有这个key : %@",key);
    }
    
    - (id)valueForUndefinedKey:(NSString *)key{
        NSLog(@"没有这个key : %@ - 给你一个其他的吧,别奔溃了!",key);
        return @"LJD";
    }
    
    
    2.1 key存在,value设置为空

    -(void)viewDidLoad中 :

        JDPerosn *person = [[JDPerosn alloc] init];
        [person setValue:nil forKey:@"age"]; // subject不会走 - 官方注释里面说只对 NSNumber - NSValue
        [person setValue:nil forKey:@"name"];
    
    图3.2.0.png 图3.2.1.png

    证明了 : KVC中的setNilValueForKey只监控NSNumberNSValue的结构体,不会监控到其他类型。

    2.2 key不存在,value设置为空
    [person setValue:nil forKey:@"LJD"];  //LJD不是已知的key
    

    可以实现这个 :

    - (void)setValue:(id)value forUndefinedKey:(NSString *)key
    

    进行监控。

    图3.2.2.png
    2.3 key为空
    NSLog(@"%@",[person valueForKey:@"KC"]);  //KC是不存在的key
    

    这个可以使用 :

    - (id)valueForUndefinedKey:(NSString *)key
    

    进行监控。

    图3.2.3.png

    3. 键值验证

    JDPerson.m中添加 :

    - (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing  _Nullable *)outError{
        if([inKey isEqualToString:@"name"]){
            [self setValue:[NSString stringWithFormat:@"可以修改一下: %@",*ioValue] forKey:inKey];
            return YES;
        }
        *outError = [[NSError alloc]initWithDomain:[NSString stringWithFormat:@"%@ 不是 %@ 的属性",inKey,self] code:16688 userInfo:nil];
        return NO;
    }
    

    viewDidLoad中 :

        NSError *error;
        NSString *name = @"LJD";
        JDPerosn *person = [[JDPerosn alloc] init];
        if (![person validateValue:&name forKey:@"names" error:&error]) {
            NSLog(@"%@",error);
        }else{
            NSLog(@"%@",[person valueForKey:@"name"]);
        }
    

    结果 :

    图3.3.0.png

    相关文章

      网友评论

          本文标题:第二十一节—KVC(二)原理探索

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