KVC原理详解

作者: xianminxiao | 来源:发表于2018-11-29 20:03 被阅读0次

    导语:

    KVC(Key-value coding)键值编码,允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值,不需要调用明确的存取方法。通过这个技术就可以在运行时动态地访问和修改对象的属性,而不是在编译时确定,这也是iOS开发中的黑魔法之一。很多iOS开发技巧都是基于KVC实现的。
    Demo源码见Github上的工程KVCDemo,主要从以下几个方面来展开KVC:

    1. KVC定义
    2. KVC寻找key策略
    3. KVC使用keyPath
    4. KVC处理异常
    5. KVC键值验证
    6. KVC处理容器类属性

    KVC定义

    KVC的定义是对NSObject的扩展来实现的,Objective-C中有个显式的NSKeyValueCoding类别名,在文件NSKeyValueCoding.h中

    @interface NSObject(NSKeyValueCoding)
    

    所以对于所有继承了NSObject的类型,都能使用KVC(一些纯Swift类和结构体是不支持KVC的,因为没有继承NSObject),以下是KVC最为重要的四个方法:

    - (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; 
    

    NSKeyValueCoding类别中还有其他的一些比较重要方法,如下

    // 默认返回YES,表示如果没有找到set(get)<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索。
    + (BOOL)accessInstanceVariablesDirectly;
    // 如果Key不存在,且无法搜索到任何和Key有关的字段或者属性,则最后才会调用这个方法,默认是抛出异常。
    - (nullable id)valueForUndefinedKey:(NSString *)key;
    // 和方法valueForUndefinedKey:一样,但这个方法是设值的时候才调用
    - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
    // 如果调用setValue方法时给Value传nil,则会调用这个方法。
    - (void)setNilValueForKey:(NSString *)key;
    // KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
    - (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
    // 集合操作的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回,这个用于KVC监听属性为NSMutableArray*类型。
    - (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
    // 输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
    - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
    

    KVC寻找key策略

    一般开发过程中,怎么用KVC大部分开发都比较清楚,就那么几个常用方法调用就可以,但是不是很清楚key的寻找策略,通过Demo来验证下key的寻找顺序。

    设值

    设值会调用方法setValue:forKey:,设置方法代码底层的执行机制大致步骤如下流程图: KVC设值原理.png
    1. 程序优先调用set<Key>:或_set<Key>方法,代码通过setter方法完成设置。注意,这里的<key>是指成员变量名,首字母大小写要符合KVC的命名规则,下同
    2. 如果没有找到set<Key>:方法,KVC机制会搜索该类里面有没有名为<key>的成员变量,无论该变量是在类接口处定义,还是在类实现处定义,也无论用了什么样的访问修饰符,只在存在以<key>命名的变量,KVC都可以对该成员变量赋值。
    3. 如果该类即没有set<Key>:方法,也没有_<key>成员变量,KVC机制会搜索_is<Key>的成员变量。
    4. 和上面一样,如果该类即没有set<Key>:方法,也没有_<key>和_is<Key>成员变量,KVC机制再会继续搜索<key>和is<Key>的成员变量。再给它们赋值。
    5. 如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的setValue:forUndefinedKey:方法,默认是抛出异常。

    简单说KVC机制在设值的时候会按照set<Key>: 》_set<Key> 》_<key> 》_is<Key> 》<key> 》 is<Key>顺序搜索成员并进行赋值操作,但是如果开发者重写了类方法+ (BOOL)accessInstanceVarialbesDirectly并且让其返回NO,这样在搜索的时候会直接从步骤1跳转到步骤5。
    KVC机制设值顺序效果可以看KVCDemo中对KVCModel的各个name设值,然后查看下KVCmodel的对象信息:

    @interface KVCModel ()
    {
        NSString* _name;
        
        BaseModel* _name2;
        NSString* _isName2;
        
        NSString* _isName3;
        NSString* name3;
        
        NSString* name4;
        NSString* isName4;
        
        NSString* isName5;
    }
    
    @end
    
    @implementation KVCModel
    
    - (void)setName:(NSString*)name
    {
        NSLog(@"%s name=%@", __FUNCTION__, name);
        _name = name;
    }
    
    - (void)setValue:(id)value forUndefinedKey:(NSString *)key
    {
        NSLog(@"%s value=%@, 该key=%@不存在!", __FUNCTION__, value, key);
    }
    
    - (id)valueForUndefinedKey:(NSString *)key
    {
        NSLog(@"%s,该key不存在%@", __FUNCTION__, key);
        return nil;
    }
    

    调用设值如下:

    KVCModel* model = [KVCModel new];
    [model setValue:@"newName" forKey:@"name"];
    [model setValue:@"newName2" forKey:@"name2"];
    [model setValue:@"newName3" forKey:@"name3"];
    [model setValue:@"newName4" forKey:@"name4"];
    [model setValue:@"newName5" forKey:@"name5"];
    [model setValue:@"newName6" forKey:@"name6"];
    
    打断点查看model的信息,如下:

    同时日志输出如下:

    2018-11-28 19:32:36.482351+0800 KVCDemo[46228:1428290] -[KVCModel setName:] name=newName
    2018-11-28 19:32:36.482508+0800 KVCDemo[46228:1428290] -[KVCModel setValue:forUndefinedKey:] value=newName6, 该key=name6不存在!
    
    重写类方法accessInstanceVariablesDirectly并返会NO,效果可以查看KVCNoAccessInstanceModel的逻辑处理: 1543405114_51_w1368_h412.png

    对应日志输出如下:

    2018-11-28 19:38:03.325658+0800 KVCDemo[47299:1452530] -[KVCNoAccessInstanceModel setName:] name=newName
    2018-11-28 19:38:03.325795+0800 KVCDemo[47299:1452530] -[KVCNoAccessInstanceModel setValue:forUndefinedKey:] value=newKey, 该key=key不存在!
    2018-11-28 19:38:03.325924+0800 KVCDemo[47299:1452530] -[KVCNoAccessInstanceModel valueForUndefinedKey:],该key不存在name
    2018-11-28 19:38:03.326002+0800 KVCDemo[47299:1452530] -[KVCNoAccessInstanceModel getKey]
    

    注意:在KVCModel中_name2定义成自定义类BaseModel,但是通过方法setValue:forKey:设置NSString*,打断点会发现_name2类型已经变为NSString*,本来BaseModel有个方法获取属性num值,在设值后去调用会出现crash。还有一种情况是如果name类型为基本数据类型(BOOL,int等),类型是不会改变。

    取值

    当调用valueForKey:方法时,KVC对key的搜索顺序有点不同于setValue:forKey:方法,大致步骤如下: KVC取值原理.png
    1. 按顺序查找方法get<Key>, <key>, is<Key>,如果其中一种方法找到直接调用,如果是BOOL或者int等值类型,会将包装成NSNumber对象。
    2. 如果步骤1的几个getter方法都没有找到,KVC机制会查找是否实现了方法countOf<Key>,同时还实现了两个方法(objectIn<Key>AtIndex<Key>AtIndexes)中的一个即可。如果都实现就会返回一个可以响应NSArray所有方法的代理集合(它是 NSKeyValueArray,是NSArray的子类),调用这个代理集合的方法,或者说给这个代理集合发送属于NSArray的方法,就会以countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes这几个方法组合的形式调用,在这几个函数打断点查看会发现还有一个可选方法get<Key>:range:。所以想重新定义KVC的一些功能,可以添加这些方法,添加的时候注意方法名要符合KVC标准命名方法。
    3. 步骤2没有找到,就会同时查找countOf<Key>,enumeratorOf<Key>,memberOf<Key> 格式的方法。如果这三个方法都找到,就会返回一个可以响应NSSet所有方法的代理集合,同步骤2一样的形式调用。
    4. 步骤3也没找到就会检查类方法+ (BOOL)accessInstanceVarialbesDirectly,如果返回是YES的,和设值一样的顺序,按_<key> 》_is<Key> 》<key> 》is<Key>搜索成员变量名,当然这种方法不推荐这么做,这样直接访问实例变量破坏了封装性,使代码更脆弱。这样还没找到就会调用valueForUndefinedKey:方法,默认抛出异常。

    步骤2的效果可以看KVCModel中实现的方法:

    #pragma mark - 当key使用name6时,KVC会找到这两个方法。
    - (NSUInteger)countOfName7
    {
        NSLog(@"%s enter", __FUNCTION__);
        return 4;
    }
    -(id)objectInName7AtIndex:(NSUInteger)index
    {
        NSLog(@"%s enter", __FUNCTION__);
        return @(index * 2);
    }
    

    其他情况搜索逻辑可以见KVCDemo对KVCModel数据获取,输出日志如下:

    2018-11-28 20:09:28.786934+0800 KVCDemo[51488:1543594] key=name value=newName
    2018-11-28 20:09:28.787015+0800 KVCDemo[51488:1543594] -[KVCModel name2] enter
    2018-11-28 20:09:28.787074+0800 KVCDemo[51488:1543594] key=name2 value=(null)
    2018-11-28 20:09:28.787175+0800 KVCDemo[51488:1543594] key=name3 value=newName3
    2018-11-28 20:09:28.787301+0800 KVCDemo[51488:1543594] key=name4 value=newName4
    2018-11-28 20:09:28.787419+0800 KVCDemo[51488:1543594] key=name5 value=newName5
    2018-11-28 20:09:28.787519+0800 KVCDemo[51488:1543594] -[KVCModel valueForUndefinedKey:],该key不存在name6
    2018-11-28 20:09:28.787580+0800 KVCDemo[51488:1543594] key=name6 value=(null)
    2018-11-28 20:09:28.787719+0800 KVCDemo[51488:1543594] -[KVCModel countOfName7] enter
    2018-11-28 20:09:28.787778+0800 KVCDemo[51488:1543594] -[KVCModel countOfName7] enter
    2018-11-28 20:09:28.787836+0800 KVCDemo[51488:1543594] -[KVCModel objectInName7AtIndex:] enter
    2018-11-28 20:09:28.787882+0800 KVCDemo[51488:1543594] -[KVCModel objectInName7AtIndex:] enter
    2018-11-28 20:09:28.787959+0800 KVCDemo[51488:1543594] -[KVCModel objectInName7AtIndex:] enter
    2018-11-28 20:09:28.788027+0800 KVCDemo[51488:1543594] -[KVCModel objectInName7AtIndex:] enter
    2018-11-28 20:09:28.788120+0800 KVCDemo[51488:1543594] key=name7 value=(
        0,
        2,
        4,
        6
    )
    

    KVC使用keyPath

    类的成员变量有可能是自定义类或其他复杂数据类型,对这种成员变量可以先用KVC获取该属性,然后再用KVC来获取这个自定义类的属性,这样一层层去获取,但这样比较繁琐。对此KVC提供一个解决方案,就是键路径keyPath,顾名思义就是按照路径寻找key。主要有两个以下两个方法:

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

    代码实现见对KVCModel对象逻辑处理:

    NSLog(@"keyPath=baseModel.num value=%@", [model valueForKeyPath:@"baseModel.num"]);
    [model setValue:@"newNumValue" forKeyPath:@"baseModel.num"];
    NSLog(@"keyPath=baseModel.num value=%@", [model valueForKeyPath:@"baseModel.num"]);
    

    KVC对于keyPath的搜索机制第一步就是分离key,用小数点.来分割key,然后再像普通key一样按照上面介绍的顺序搜索。

    KVC处理异常

    使用KVC过程中最常见的异常就是不小心使用了错误的key,或者在设值中不小心传了nil的值,KVC有专门的方法处理这些异常。

    • KVC处理nil异常,如果在设值过程中,不小心传了nil,KVC会调用方法setNilValueForKey:,这个默认方法是抛出异常,所以一般而言最好重写这个方法。
    • KVC处理UndefinedKey异常,如果在设值取值传的key不存在,这样的操作就会crash,设值会调用到setValue:forUndefinedKey:方法,而取值会调用valueForUndefinedKey:方法,这两个方法默认都是抛出异常,所以最好重写这两个方法来规避crash。

    KVC键值验证

    KVC提供了验证key对应的value是否可用的方法:

    - (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
    

    方法实现会取探索类是否实现了方法

    -(BOOL)validateName8:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError
    

    如果有实现这个方法,就用实现的方法返回,没有就直接返回YES,这是默认返回结果,效果可以看KVCDemo中对属性name8的验证。
    注意:键值验证不会主动去做验证,需要开发者手动去验证,所以即使类实现了验证方法,但是KVC也不会主动验证,还是会设值成功。当然也有一些技术,比如CoreData会自动调用。

    KVC处理容器类属性

    对象的属性可以是一对一的,也可以是一对多的。一对多的属性要么是有序的数组,要么就是无序的集合。
    不可变的有序数组属性(NSArray)和无序集合(NSSet)属性可使用方法valueForKey:来获取。而当对象的属性是可变的容器时,对于有序的容器,可以用以下方法:

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

    该方法返回一个可变有序数组。对于无序的容器,可以用以下方法:

    - (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
    

    该方法返回一个可变的无序集合。同时他们也有对应的keyPath版本:

    - (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
    - (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;
    

    可变的容器获取方法,一般在KVO对容器监听的时候会用到,具体使用可见《iOS KVO原理探究》
    当NSDictionary对象使用KVC时,valueForKey:的表现行为和objectForKey:一样,使用valueForKeyPath:可访问多层嵌套的字典会方便点,在KVC中有两个关于NSDictionary的方法:

    // 输入一组key,返回这组key对应的属性,再组成一个字典。
    - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
    // 用来修改Model中对应key的属性
    - (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
    

    结尾

    KVC作为在iOS开发中的利器,基于运行时的编程方式极大提高了灵活性,简化了代码,可以实现一些难以想象的功能,是许多iOS开发黑魔法的基础,一般有以下几个场景:

    • 动态的取值和设值
    • 访问和修改私有变量
    • 修改一些控件的内部属性
    • 操作容器类(NSArray和NSSet等)
    • 实现高阶消息传递
      当对容器类使用KVC时,valueForKey:将会被传递给容器中的每一个对象,而不是容器本身进行操作,结果会添加进返回的容器中,这样开发就很方便操作集合来返回另一个集合,如以下代码:
    NSArray* languageArrStr = @[@"english", @"franch", @"chinese"];
    NSArray* languageArrCapStr = [languageArrStr valueForKey:@"capitalizedString"];
    for (NSString* languageStr  in languageArrCapStr)
    {
        NSLog(@"%@", languageStr);
    }
    

    打印结果:

    2018-11-29 14:13:07.940471+0800 KVCDemo[17618:2982517] English
    2018-11-29 14:13:07.940554+0800 KVCDemo[17618:2982517] Franch
    2018-11-29 14:13:07.940694+0800 KVCDemo[17618:2982517] Chinese
    

    方法 capitalizedString被传递到NSArray的每一项,这样NSArray的每一员都会执行capitalizedString并返回一个包含结果的 新NSArray,可从打印结果看出成功的把首字符都转成大写。

    相关文章

      网友评论

        本文标题:KVC原理详解

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