日志5 KVC

217 阅读5分钟

定义 KVC(Key-Value Coding)是利用NSKeyValueCoding 非正式协议实现的一种机制,
对象采用这种机制来提供对其属性的间接访问.

1、KVC的全称是Key-Value Coding、俗称“键值编码”、可以通过一个key来访问某一个属性。

2、常见的API有四种
setValue: forKey
setValue: forKeyPath
valueForKey:
valueForKeyPath
前两种是赋值 后两种是取值   forKey 和 forKeyPath 主要区别是路径区别如下

例:有2个类(Person和Dog)   
Person类有2个属性 name 和 dog 
Dog类 中也有一个name属性 如下:
        @interface FCPerson : NSObject
        @property(nonatomic,strong)NSString *name;
        @property(nonatomic,strong)Dog *dog;
        @end

        @interface Dog : NSObject
        @property(nonatomic,strong)NSString *name;
        @end

我们使用 setValue: forKey 对 Person name赋值 
使用 和 setValue: forKeyPath对 Dog  name赋值  如下所示:

    FCPerson * person = [[FCPerson alloc]init];
    person.dog = [[Dog alloc]init];
    [person setValue:@"张三" forKey:@"name"];
    [person setValue:@"小狗狗" forKeyPath:@"dog.name"];
    NSLog(@"人名:%@---狗名:%@",person.name,person.dog.name);

最后打印的结果就是 人名:张三---狗名:小狗狗
如果直接使用 setValue: forKey 的方法对狗狗name赋值就会报错


!! forKeyPath支持路径查找,forKey不支持

使用范围

  • KVC是通过对NSObject的扩展来实现的 —— 只要继承了NSObject的类都可以使用KVC

  • NSArray、NSDictionary、NSMutableDictionary、NSOrderedSet、NSSet等也遵守KVC协议

  • 除少数类型(结构体)以外都可以使用KVC

        TCJPerson *person = [TCJPerson alloc];
        [person setValue:@"TCJ" forKey:@"name"];
        [person setValue:@"CJ" forKey:@"nickName"];
    

KVC常用方法,这些也是我们在日常开发中经常用到的

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

NSKeyValueCoding类别的其它方法

// 默认为YES。 如果返回为YES,如果没有找到 set<Key> 方法的话, 
// 会按照_key, _isKey, key, isKey的顺序搜索成员变量, 返回NO则不会搜索
+ (BOOL)accessInstanceVariablesDirectly;

// 键值验证, 可以通过该方法检验键值的正确性, 然后做出相应的处理
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue 
          forKey:(NSString *)inKey 
           error:(out NSError **)outError;

// 如果key不存在, 并且没有搜索到和key有关的字段, 会调用此方法, 默认抛出异常。
// 两个方法分别对应 get 和 set 的情况
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

// setValue方法传 nil 时调用的方法
// 注意文档说明: 当且仅当 NSNumber 和 NSValue 类型时才会调用此方法 
- (void)setNilValueForKey:(NSString *)key;

// 一组 key对应的value, 将其转成字典返回, 可用于将 Model 转成字典
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

三、KVC底层原理

由于NSKeyValueCoding的实现在Foundation框架中,但它又不开源,我们只能通过KVC官方文档来了解它

  • ①.按set<Key>:_set<Key>:顺序查找对象中是否有对应的方法
    • 找到了直接调用设值
    • 没有找到跳转第2步
  • ②.判断accessInstanceVariablesDirectly结果
    • 为YES时按照_<key>_is<Key><key>is<Key>的顺序查找成员变量,找到了就赋值;找不到就跳转第3步
    • 为NO时跳转第3步
  • ③.调用setValue:forUndefinedKey:。默认情况下会引发一个异常,但是继承于NSObject的子类可以重写该方法就可以避免崩溃并做出相应措施

取值过程:

  • ①.按照get<Key><key>is<Key>_<key>顺序查找对象中是否有对应的方法

    • 如果有则调用getter,执行第5步
    • 如果没有找到,跳转到第2步
  • ②.查找是否有countOf<Key>objectIn<Key>AtIndex: 方法(对应于NSArray类定义的原始方法)以及<key>AtIndexes: 方法(对应于NSArray方法objectsAtIndexes:)

    • 如果找到其中的第一个(countOf<Key>),再找到其他两个中的至少一个,则创建一个响应所有 NSArray方法的代理集合对象,并返回该对象(即要么是countOf<Key> + objectIn<Key>AtIndex:,要么是countOf<Key> + <key>AtIndexes:,要么是countOf<Key> + objectIn<Key>AtIndex: + <key>AtIndexes:)
    • 如果没有找到,跳转到第3步
  • ③.查找名为countOf<Key>enumeratorOf<Key>memberOf<Key>这三个方法(对应于NSSet类定义的原始方法)

    • 如果找到这三个方法,则创建一个响应所有NSSet方法的代理集合对象,并返回该对象
    • 如果没有找到,跳转到第4步
  • ④.判断accessInstanceVariablesDirectly

    • 为YES时按照_<key>_is<Key><key>is<Key>的顺序查找成员变量,找到了就取值
    • 为NO时跳转第6步
  • ⑤.判断取出的属性值

    • 属性值是对象,直接返回
    • 属性值不是对象,但是可以转化为NSNumber类型,则将属性值转化为NSNumber 类型返回
    • 属性值不是对象,也不能转化为NSNumber类型,则将属性值转化为NSValue类型返回
  • ⑥.调用valueForUndefinedKey:.默认情况下会引发一个异常,但是继承于NSObject的子类可以重写该方法就可以避免崩溃并做出相应措施

四、自定义KVC

根据KVC的设值过程、取值过程,我们可以自定义KVCsetter方法和getter方法,但是这一切都是根据官方文档做出的规则,自定义KVC只能在一定程度上取代系统KVC,大致流程几乎一致:实现了 setValue:forUndefinedKey:valueForUndefinedKey: 的调用,且 accessInstanceVariablesDirectly 无论为truefalse,都能保持两次调用

①.自定义setter方法

1. key 非空判断
2. 2.找到相关方法set<Key>_set<Key>setIs<Key>,若存在就直接调用

3.判断是否能够直接赋值实例变量,不能的情况下就调用setValue:forUndefinedKey:或抛出异常

4.找相关实例变量进行赋值

5.调用setValue:forUndefinedKey:或抛出异常