1、定义

KVC(全称key-value coding)键值编码。在iOS开发中,允许开发者通过key直接访问对象的属性,或者给对象的属性进行赋值,而不需要调用明确的存取方法。这样就可以在运行时动态的访问和修改对象的属性,而不是在编译时确定。

KVC的定义是通过对NSObject的扩展来实现的,定义在 NSKeyValueCoding.h 文件中,是一个非正式协议。

2、KVC 相关方法

NSKeyValueCoding 中,KVC最为重要的方法如下:

  1. // 通过key来取值
  2. - (id)valueForKey:(NSString *)key;
  3. // 通过keyPath来取值
  4. - (id)valueForKeyPath:(NSString *)keyPath;
  5. // 通过key来设值
  6. - (void)setValue:(id)value forKey:(NSString *)key;
  7. // 通过keyPath来设值
  8. - (void)setValue:(id)value forKeyPath:(NSString *)keyPath;

NSKeyValueCoding 中还有其他的相关方法,例如:

  1. // KVC提供属性值确认的API,它可以用来检查set的值是否正确,为不正确的值做一个替换值或者拒绝设值新值并返回错误原因
  2. - (BOOL)validateValue:(inout id _Nullable *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable *)outError;
  3. // 如果key不存在,且没有KVC无法搜索到任何和key有关的字段或者属性,则会调用这个方法,默认是抛出异常
  4. - (void)setValue:(id)value forUndefinedKey:(NSString *)key;
  5. // 和上一个方法一样,上一个方法为设值,该方法为取值
  6. - (id)valueForUndefinedKey:(NSString *)key;
  7. // 如果在setValue方法时给value传nil,则会调用该方法
  8. - (void)setNilValueForKey:(NSString *)key;
  9. // 输入一组key,返回该组key对应的value,再转成字典返回,用于将model转字典
  10. - (NSDictionary<NSString *,id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

3、寻找key的策略

3.1 setValue:forKey:方法赋值的原理

设值会调用 setValue:forKey: 方法,其大致步骤如下流程图所示:

image.png

  1. 查找 set<Key>:_set<Key>: 命名的 setter,按照这个顺序,如果找到,则调用这个方法并将值传进去。
  2. 如果没有发现一个简单的 setter ,但是 accessInstanceVariablesDirectly 类属性返回YES,则查找一个命名规则为 _key、_isKey、key、isKey的实例变量。按照这个顺序,如果查找到则将value赋值给实例变量。
  3. 如果没有找到 setter 或实例变量,则调用 setValue:forUndefinedKey: 方法,并默认抛出一个异常。

3.2、valueForKey:方法取值的原理

当调用 valueForKey: 方法时,KVC对key的搜索顺序有点不同于 setValue:forKey: 方法,大致步骤如下:

image.png

  1. 首先按 get<Key><key>is<Key> 的顺序查找 getter 方法,找到直接调用。
    • 若方法的返回结果类型是一个对象指针,则直接返回结果。
    • 若类型为能够转化为 NSNumber 的基本数据类型,转换为 NSNumber 后返回;否则转换为 NSValue 返回。
  2. 若上面的 getter没有找到,则查找 countOf<Key>objectIn<Key>AtIndex:<Key>AtIndexes 格式的方法。如果 countOf<Key> 和另外两个方法中的一个找到,那么就会返回一个可以响应 NSArray 所有方法的集合代理。发送给这个代理集合的 NSArray 消息方法,就会以 countOf<Key>objectIn<Key>AtIndex:<Key>AtIndexes 这几个方法组合的形式调用。如果 receiver 的类实现了 get<Key>:range: 方法,该方法也会用于性能优化。
  3. 还没查到,那么查找 countOf<Key>enumeratorOf<Key>memberOf<Key>: 格式的方法。如果这3个方法都找到,那么久返回一个可以相应NSSet所有方法的集合代理。发送给这个代理集合的NSSet消息方法,就会以countOf<Key>enumeratorOf<Key>memberOf<Key>: 组合的形式调用。
  4. 还是没查到,那么如果类方法 accessInstanceVariablesDirectly返回YES,那么按_<key>_is<Key><key>is<Key> 的顺序直接搜索实例变量。如果搜索到了,则返回receiver相应实例变量的值。
  5. 再没有查到,调用 valueForUndefinedKey: 方法,抛出异常。

4、使用keyPath

在实际开发过程中,一个类的成员变量有可能是自定义类或者其他的复杂数据类型,我们可以先用KVC获取该属性,然后再用KVC来获取这个自定义类的属性。但这样比较繁琐,因此KVC提供了一个解决方案,keyPath。

  1. // 通过KeyPath来取值
  2. - (nullable id)valueForKeyPath:(NSString *)keyPath;
  3. // 通过KeyPath来设值
  4. - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

5、处理异常

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

  • KVC处理nil异常,如果在设值过程中,不小心传了nil值,KVC会调用方法 setNilValueForKey:,这个默认方法是抛出 NSInvalidArgumentException 异常,所以一般而言最好重写这个方法,对异常进行处理。
  • KVC处理UndefinedKey异常,如果在设值取值传的key不存在时,程序就会crash,设值会调用到 setValue:forUndefinedKey: 方法,而取值会调用 valueForUndefinedKey: 方法,这两个方法默认都是抛出 NSUndefinedKeyException 异常,因此如果要避免程序crash,可以重写这两个方法。

6、集合类运算

6.1 集合运算符格式

KVC提供的 valueForKeyPath: 方法非常强大,可以通过该方法对集合对象进行“深入”操作,在其keyPath中嵌套集合运算符,例如求一个数组中对象某个属性的count。集合运算符的格式如下:

  1. keyPathToCollection.@collentionOperator.keyPathToproperty
  • keyPathToCollection:Left key path,要操作的集合对象,若调用 valueForKeyPath: 方法的对象本来就是集合对象,则可以为空。
  • collectionOperator:Collection operator,集合操作符,一般以@开头。
  • keyPathToproperty:Right key path,要运算的属性。

6.2 集合运算符格式

集合运算符主要分为以下三类:

  • 集合操作符:处理集合包含的对象,并根据操作符的不同返回不同的类型,返回值以NSNumber为主。
  • 数组操作符:根据操作符的条件,将符合条件的对象包含在数组中返回。
  • 嵌套操作符:处理集合对象中嵌套其他集合对象的情况,返回结果也是一个集合对象。

6.2.1 集合操作符

为了演示集合操作符,我们新建一个项目,定义一个Book类,有bookName和bookPrice属性,然后在main函数中,新建一个Book数组,再对数组进行集合操作。详细操作如下:

  • @avg 用来计算集合中 right keyPath 指定的属性的平均值。
  1. NSNumber *avgNum = [bookrack valueForKeyPath:@"@avg.bookPrice"];
  2. NSLog(@"avg: %f", [avgNum floatValue]);
  • @count 用来计算集合中对象的数量。注意:@count 操作符不需要写 rightKeyPath,如果写了也会被忽略。
  1. NSNumber *count = [bookrack valueForKeyPath:@"@count"];
  2. NSLog(@"count: %f", [count floatValue]);
  • @sum 用来计算集合中 right keyPath 指定的属性的总和。
  1. NSNumber *sum = [bookrack valueForKeyPath:@"@sum.bookPrice"];
  2. NSLog(@"sum: %f", [sum floatValue]);
  • @max 用来查找集合中 right keyPath 指定属性的最大值。
  1. NSNumber *max = [bookrack valueForKeyPath:@"@max.bookPrice"];
  2. NSLog(@"max: %f", [max floatValue]);
  • @min 用来查找集合中 right keyPath 指定属性的最小值。
  1. NSNumber *min = [bookrack valueForKeyPath:@"@min.bookPrice"];
  2. NSLog(@"min: %f", [min floatValue]);

6.2.2 数组操作符

  • @unionOfObjects 将集合中的所有对象的同一个属性放在数组中返回。
  1. NSArray *priceArray = [bookrack valueForKeyPath:@"@unionOfObjects.bookPrice"];
  2. NSLog(@"unionOfObjects: %@", priceArray);
  • @distinctUnionOfObjects 将集合中对象的属性进行去重后并返回。
  1. NSArray *nameArray = [bookrack valueForKeyPath:@"@distinctUnionOfObjects.bookName"];
  2. NSLog(@"distinctUnionOfObjects: %@", nameArray);

需要注意:以上两个方法,如果操作的属性为nil,则在添加到数组中时会导致crash。

6.2.3 嵌套操作符

由于嵌套操作符是需要对嵌套的集合对象进行操作,所以新建了一个racks数组,其中包含了两个Book类型对象的数组。

  • @unionOfArrays 是用来操作集合内部的集合对象,将所有 right keyPath 对应的对象放在一个数组中返回。
  1. NSArray *unionArray = [racks valueForKeyPath:@"@unionOfArrays.bookName"];
  2. NSLog(@"unionOfArrays: %@", unionArray);
  • @distinctUnionOfArrays 是用来操作集合内部的集合对象,将所有 right keyPath 对应的对象放在一个数组中,并进行去重后返回。
  1. NSArray *distinctArray = [racks valueForKeyPath:@"@distinctUnionOfArrays.bookPrice"];
  2. NSLog(@"distinctUnionOfArrays: %@", distinctArray);

7、KVC 安全性检查

在使用KVC时,由于传入的key或者keyPath是一个字符串,因此很容易写错或者属性本身修改后忘记修改对应的字符串,导致crash。

解决的方案为,利用反射机制,通过 @selector() 获取到方法的SEL,然后通过 NSStringFromSelector() 将SEL反射为字符串。这样在 @selector() 中传入方法名的过程中,编译器会有合法性检查,如果方法不存在或者未实现时,会报对应的警告。

  1. [self valueForKey:NSStringFromSelector(@selector(object))];

8、参考