1. 基本介绍

  • KVC全称Key-Value Coding(键值编码),是一种由NSKeyValueCoding非正式协议启用的机制,对象采用它来提供对其属性的间接访问

  • 当一个对象符合键值编码时,它的属性可以通过字符串Key来寻址。这种间接访问机制补充了实例变量及其关联的访问方法所提供的直接访问

  • 官方文档:Key-Value Coding Programming Guide

2. API介绍

  • KVCAPIFoundation框架中,暂未开源

  • KVC本质上是对NSObjectNSArrayNSDictionaryNSMutableDictionaryNSOrderedSetNSSet等对象,实现NSKeyValueCoding分类,赋予它们Key-Value Coding的能力

通过Key读取和存储

  1. - (nullable id)valueForKey:(NSString *)key;
  2. - (void)setValue:(nullable id)value forKey:(NSString *)key;

通过keyPath读取和存储

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

其他API

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

3. API使用

3.1 对象属性

一个对象通常在其接口声明中指定属性,这些属性属于以下几个类别之一:

  • 属性:这些是简单的值,例如标量、字符串或布尔值。值对象NSNumber和其他不可变类型NSColor也被视为属性

  • 一对一的关系:这些是具有自己属性的可变对象。一个对象的属性可以在不改变对象本身的情况下改变

    • 例如:一个银行账户对象可能有一个owner属性,它是一个Person对象的实例,它本身有一个address属性。所有者的地址可能会更改,而不会更改银行帐户持有的所有者参考。银行账户的所有者没有改变。只有他们的地址
  • 对多关系:这些是集合对象。通常使用NSArray或的实例NSSet来保存此类集合,也可能是自定义集合类型

3.1.1 使用Key寻址

  1. LGPerson *person = [[LGPerson alloc] init];
  2. // 一般setter方法
  3. person.name = @"LG_Cooci";
  4. person.age = 18;
  5. person->myName = @"cooci";
  6. // 非正式协议 - 间接访问
  7. [person setValue:@"KC" forKey:@"name"];

3.1.2 使用路径寻址

  1. LGStudent *student = [LGStudent alloc];
  2. student.subject = @"大师班";
  3. person.student = student;
  4. [person setValue:@"Swift" forKeyPath:@"student.subject"];
  5. NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);
  6. -------------------------
  7. Swift

3.2 集合属性

该协议为集合对象访问定义了三种不同的代理方法,每种方法都有一个键和一个键路径变体:

  • mutableArrayValueForKey:mutableArrayValueForKeyPath:

    • 它们返回一个行为类似于NSMutableArray对象的代理对象
  • mutableSetValueForKey:mutableSetValueForKeyPath:

    • 它们返回一个行为类似于NSMutableSet对象的代理对象
  • mutableOrderedSetValueForKey:mutableOrderedSetValueForKeyPath:

    • 它们返回一个行为类似于NSMutableOrderedSet对象的代理对象

3.2.1 修改数组元素

  1. person.array = @[@"1",@"2",@"3"];
  2. // 第一种:创建新的数组,KVC赋值就
  3. NSArray *array = [person valueForKey:@"array"];
  4. array = @[@"100",@"2",@"3"];
  5. [person setValue:array forKey:@"array"];
  6. // 第二种:使用mutableArrayValueForKey:
  7. NSMutableArray *mArray = [person mutableArrayValueForKey:@"array"];
  8. mArray[0] = @"200";

3.2.2 数组取值

  1. LGStudent *p = [LGStudent new];
  2. p.penArr = [NSMutableArray arrayWithObjects:@"pen0", @"pen1", @"pen2", @"pen3", nil];
  3. NSArray *arr = [p valueForKey:@"penArr"];
  4. NSLog(@"pens = %@", arr);
  5. NSEnumerator *enumerator = [arr objectEnumerator];
  6. NSString* str = nil;
  7. while (str = [enumerator nextObject]) {
  8. NSLog(@"%@", str);
  9. }
  10. -------------------------
  11. pens = (
  12. pen0,
  13. pen1,
  14. pen2,
  15. pen3
  16. )
  17. 遍历:pen0
  18. 遍历:pen1
  19. 遍历:pen2
  20. 遍历:pen3

3.2.3 模型字典转换

  1. NSDictionary* dict = @{
  2. @"name":@"Cooci",
  3. @"nick":@"KC",
  4. @"subject":@"iOS",
  5. @"age":@18,
  6. @"length":@180
  7. };
  8. LGStudent *s = [[LGStudent alloc] init];
  9. [s setValuesForKeysWithDictionary:dict];
  10. NSLog(@"模型:%@",s);
  11. NSArray *array = @[@"name",@"age"];
  12. NSDictionary *dic = [s dictionaryWithValuesForKeys:array];
  13. NSLog(@"字典:%@",dic);
  14. -------------------------
  15. 模型:<LGStudent: 0x2835b8990>
  16. 字典:{
  17. age = 18;
  18. name = Cooci;
  19. }

3.2.4 消息传递

  1. NSArray *array = @[@"Hank",@"Cooci",@"Kody",@"CC"];
  2. NSArray *lenStr= [array valueForKeyPath:@"length"];
  3. NSLog(@"长度:%@",lenStr);
  4. NSArray *lowStr= [array valueForKeyPath:@"lowercaseString"];
  5. NSLog(@"小写:%@",lowStr);
  6. -------------------------
  7. 长度:(
  8. 4,
  9. 5,
  10. 4,
  11. 2
  12. )
  13. 小写:(
  14. hank,
  15. cooci,
  16. kody,
  17. cc
  18. )

3.3 集合运算符

集合操作符表现出三种基本的行为类型:

  • 聚合操作符:以某种方式合并集合的对象,并返回一个通常与在正确键路径中命名的属性的数据类型匹配的对象。@count操作符是个例外,它不接受正确的键路径,并且总是返回一个NSNumber实例

  • 数组操作符:返回一个NSArray实例,该实例包含命名集合中持有的对象的一些子集

  • 嵌套操作符:处理包含其他集合的集合,并根据操作符返回一个NSArrayNSSet实例,以某种方式组合嵌套集合的对象

3.3.1 聚合操作符

  1. NSMutableArray *personArray = [NSMutableArray array];
  2. for (int i = 0; i < 6; i++) {
  3. LGStudent *p = [LGStudent new];
  4. NSDictionary* dict = @{
  5. @"name":@"Tom",
  6. @"age":@(18+i),
  7. @"nick":@"Cat",
  8. @"length":@(175 + 2*arc4random_uniform(6)),
  9. };
  10. [p setValuesForKeysWithDictionary:dict];
  11. [personArray addObject:p];
  12. }
  13. NSLog(@"lengthArray:%@", [personArray valueForKey:@"length"]);
  14. float avg = [[personArray valueForKeyPath:@"@avg.length"] floatValue];
  15. NSLog(@"@avg:%f", avg);
  16. int count = [[personArray valueForKeyPath:@"@count.length"] intValue];
  17. NSLog(@"@count:%d", count);
  18. int sum = [[personArray valueForKeyPath:@"@sum.length"] intValue];
  19. NSLog(@"@sum:%d", sum);
  20. int max = [[personArray valueForKeyPath:@"@max.length"] intValue];
  21. NSLog(@"@max:%d", max);
  22. int min = [[personArray valueForKeyPath:@"@min.length"] intValue];
  23. NSLog(@"@min:%d", min);
  24. -------------------------
  25. lengthArray:(
  26. 177,
  27. 179,
  28. 175,
  29. 181,
  30. 175,
  31. 175
  32. )
  33. @avg177.000000
  34. @count6
  35. @sum1062
  36. @max181
  37. @min175

3.3.2 数组操作符

  1. NSMutableArray *personArray = [NSMutableArray array];
  2. for (int i = 0; i < 6; i++) {
  3. LGStudent *p = [LGStudent new];
  4. NSDictionary* dict = @{
  5. @"name":@"Tom",
  6. @"age":@(18+i),
  7. @"nick":@"Cat",
  8. @"length":@(175 + 2*arc4random_uniform(6)),
  9. };
  10. [p setValuesForKeysWithDictionary:dict];
  11. [personArray addObject:p];
  12. }
  13. NSLog(@"lengthArray:%@", [personArray valueForKey:@"length"]);
  14. NSArray* arr1 = [personArray valueForKeyPath:@"@unionOfObjects.length"];
  15. NSLog(@"不排重:%@", arr1);
  16. NSArray* arr2 = [personArray valueForKeyPath:@"@distinctUnionOfObjects.length"];
  17. NSLog(@"排重:%@", arr2);
  18. -------------------------
  19. lengthArray:(
  20. 177,
  21. 183,
  22. 177,
  23. 175,
  24. 181,
  25. 183
  26. )
  27. 不排重:(
  28. 177,
  29. 183,
  30. 177,
  31. 175,
  32. 181,
  33. 183
  34. )
  35. 排重:(
  36. 175,
  37. 181,
  38. 177,
  39. 183
  40. )

3.3.3 嵌套操作符

  1. NSMutableArray *personArray1 = [NSMutableArray array];
  2. for (int i = 0; i < 6; i++) {
  3. LGStudent *student = [LGStudent new];
  4. NSDictionary* dict = @{
  5. @"name":@"Tom",
  6. @"age":@(18+i),
  7. @"nick":@"Cat",
  8. @"length":@(175 + 2*arc4random_uniform(6)),
  9. };
  10. [student setValuesForKeysWithDictionary:dict];
  11. [personArray1 addObject:student];
  12. }
  13. NSMutableArray *personArray2 = [NSMutableArray array];
  14. for (int i = 0; i < 6; i++) {
  15. LGPerson *person = [LGPerson new];
  16. NSDictionary* dict = @{
  17. @"name":@"Tom",
  18. @"age":@(18+i),
  19. @"nick":@"Cat",
  20. @"length":@(175 + 2*arc4random_uniform(6)),
  21. };
  22. [person setValuesForKeysWithDictionary:dict];
  23. [personArray2 addObject:person];
  24. }
  25. // 嵌套数组
  26. NSArray* nestArr = @[personArray1, personArray2];
  27. NSArray* arr = [nestArr valueForKeyPath:@"@distinctUnionOfArrays.length"];
  28. NSLog(@"排重:%@", arr);
  29. NSArray* arr1 = [nestArr valueForKeyPath:@"@unionOfArrays.length"];
  30. NSLog(@"不排重:%@", arr1);
  31. -------------------------
  32. 排重:(
  33. 185,
  34. 183,
  35. 179,
  36. 177
  37. )
  38. 不排重:(
  39. 177,
  40. 177,
  41. 177,
  42. 183,
  43. 185,
  44. 177,
  45. 183,
  46. 179,
  47. 179,
  48. 179,
  49. 179,
  50. 179
  51. )

3.4 访问非对象属性

  • 默认键值编码实现使用NSNumber实例包装的标量类型

  • 默认存取用于包装和展开常见NSPointNSRangeNSRect、和NSSize结构

  • 结构类型,可以包装在一个NSValue对象中

3.4.1 包装在NSNumber对象中的标量类型

数据类型 创建方法 存取方法
BOOL numberWithBool: boolValue (在 iOS 中)
charValue (在 macOS 中)*
char numberWithChar: charValue
double numberWithDouble: doubleValue
float numberWithFloat: floatValue
int numberWithInt: intValue
long numberWithLong: longValue
long long numberWithLongLong: longLongValue
short numberWithShort: shortValue
unsigned char numberWithUnsignedChar: unsignedChar
unsigned int numberWithUnsignedInt: unsignedInt
unsigned long numberWithUnsignedLong: unsignedLong
unsigned long long numberWithUnsignedLongLong: unsignedLongLong
unsigned short numberWithUnsignedShort: unsignedShort

3.4.2 包装和展开常见结构

数据类型 创建方法 存取方法
NSPoint valueWithPoint: pointValue
NSRange valueWithRange: rangeValue
NSRect valueWithRect: (仅限 macOS) rectValue
NSSize valueWithSize: sizeValue

3.4.3 自定义结构体

  1. typedef struct {
  2. float x, y, z;
  3. } ThreeFloats;
  4. ThreeFloats floats = {1.,2.,3.};
  5. NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
  6. [person setValue:value forKey:@"threeFloats"];
  7. NSValue *value1 = [person valueForKey:@"threeFloats"];
  8. NSLog(@"%@",value1);
  9. ThreeFloats th;
  10. [value1 getValue:&th];
  11. NSLog(@"%f-%f-%f",th.x,th.y,th.z);
  12. -------------------------
  13. {length = 12, bytes = 0x0000803f0000004000004040}
  14. 1.000000-2.000000-3.000000

3.5 验证属性

由于特定于属性的验证方法通过引用接收值和错误参数,因此验证具有三种可能的结果:

  • 验证方法认为值对象有效并在YES不改变值或错误的情况下返回

  • 验证方法认为值对象无效,但选择不更改它。在这种情况下,该方法返回NO并将错误引用(如果由调用者提供)设置为NSError指示失败原因的对象

  • 验证方法认为值对象无效,但会创建一个新的有效对象作为替换。在这种情况下,该方法返回YES同时保持错误对象不变。在返回之前,该方法修改值引用以指向新的值对象。当它进行修改时,该方法总是创建一个新对象,而不是修改旧对象,即使值对象是可变的

name属性的验证

  1. LGPerson *per = [LGPerson new];
  2. NSError* error;
  3. NSString* name = @"John";
  4. if (![per validateValue:&name forKey:@"name" error:&error]) {
  5. NSLog(@"%@",error);
  6. }

4. KVC的存储过程

查找setter方法

  1. - (void)setName:(NSString *)name{
  2. NSLog(@"%s - %@",__func__,name);
  3. }
  4. - (void)_setName:(NSString *)name{
  5. NSLog(@"%s - %@",__func__,name);
  6. }
  7. - (void)setIsName:(NSString *)name{
  8. NSLog(@"%s - %@",__func__,name);
  9. }
  10. // 没有调用
  11. - (void)_setIsName:(NSString *)name{
  12. NSLog(@"%s - %@",__func__,name);
  13. }

查找实例变量

  1. #import <Foundation/Foundation.h>
  2. @interface LGPerson : NSObject{
  3. @public
  4. NSString *_name;
  5. NSString *_isName;
  6. NSString *name;
  7. NSString *isName;
  8. }
  9. @end
  10. @implementation LGPerson
  11. #pragma mark - 关闭/开启实例变量
  12. + (BOOL)accessInstanceVariablesDirectly{
  13. return YES;
  14. }
  15. @end

image.png

调用setValue:forKey:的流程:

  • 【第一步】查找是否存在以下三种setter方法,查找顺序:set<Key>:_set<Key>setIs<Key>

    • key是指成员变量名,首字母大小写需要符合KVC的命名规范

    • 存在任意一种setter方法,直接设置属性的value

    • 如果都不存在,进入【第二步】

  • 【第二步】:查找accessInstanceVariablesDirectly方法的返回值

    • 返回YES,查找间接访问的实例变量进行赋值,查找顺序:_<key>_is<Key><key>is<Key>

      • 如果找到其中任意一个实例变量,可对其赋值

      • 如果都未找到,进入【第三步】

    • 返回NO,进入【第三步】

  • 【第三步】如果setter方法或实例变量都没有找到,系统会调用该对象的setValue:forUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常

5. KVC的读取过程

查找getter方法

  1. - (NSString *)getName{
  2. return NSStringFromSelector(_cmd);
  3. }
  4. - (NSString *)name{
  5. return NSStringFromSelector(_cmd);
  6. }
  7. - (NSString *)isName{
  8. return NSStringFromSelector(_cmd);
  9. }
  10. - (NSString *)_name{
  11. return NSStringFromSelector(_cmd);
  12. }

查找集合类型

  1. // 个数
  2. - (NSUInteger)countOfPens{
  3. NSLog(@"%s",__func__);
  4. return [self.arr count];
  5. }
  6. // 获取值
  7. - (id)objectInPensAtIndex:(NSUInteger)index {
  8. NSLog(@"%s",__func__);
  9. return [NSString stringWithFormat:@"pens %lu", index];
  10. }

查找NSSet类型

  1. // 个数
  2. - (NSUInteger)countOfBooks{
  3. NSLog(@"%s",__func__);
  4. return [self.set count];
  5. }
  6. // 是否包含这个成员对象
  7. - (id)memberOfBooks:(id)object {
  8. NSLog(@"%s",__func__);
  9. return [self.set containsObject:object] ? object : nil;
  10. }
  11. // 迭代器
  12. - (id)enumeratorOfBooks {
  13. // objectEnumerator
  14. NSLog(@"来了 迭代编译");
  15. return [self.arr reverseObjectEnumerator];
  16. }

查找实例变量

  1. #import <Foundation/Foundation.h>
  2. @interface LGPerson : NSObject{
  3. @public
  4. NSString *_name;
  5. NSString *_isName;
  6. NSString *name;
  7. NSString *isName;
  8. }
  9. @end
  10. @implementation LGPerson
  11. #pragma mark - 关闭/开启实例变量
  12. + (BOOL)accessInstanceVariablesDirectly{
  13. return YES;
  14. }
  15. @end

image.png

调用valueForKey:的流程:

  • 【第一步】查找getter方法,查找顺序:get<Key><key>is<Key>_<key>

    • 如果找到,进入【第五步】

    • 如果未找到,进入【第二步】

  • 【第二步】查找countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes:

    • 如果找到第一个或其他两个中的至少一个,则会创建一个响应所有NSArray方法的集合代理对象,并返回该对象,即:NSKeyValueArray,属于NSArray的子类。代理对象随后将接收到任何NSArray消息转换为countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes:消息的某种组合,用来创建键值编码对象

      • 如果原始对象还实现了一个名为get<Key>:range:之类的可选方法,则代理对象也将在适当时使用该方法

      • 方法名的命名规则,要符合KVC的标准命名方法,包括方法签名

    • 如果未找到,进入【第三步】

  • 【第三步】查找以下几种方法,countOf<Key>enumeratorOf<Key>memberOf<Key>:

    • 如果三个方法都找到,则会创建一个响应所有NSSet方法的集合代理对象,并返回该对象,此代理对象随后将其收到的所有NSSet消息转换为countOf<Key>enumeratorOf<Key>memberOf<Key>:消息的某种组合,用于创建它的对象

    • 如果未找到,进入【第四步】

  • 【第四步】查找accessInstanceVariablesDirectly方法的返回值

    • 返回YES,依次搜索_<key>_is<Key><key>is<Key>的实例变量

      • 找到实例变量,直接获取实例变量的值,进入【第五步】

      • 未找到,进入【第六步】

    • 返回NO,进入【第六步】

  • 【第五步】根据搜索到的属性值的类型,返回不同的结果

    • 如果是对象指针,则直接返回结果

    • 如果是NSNumber支持的标量类型,则将其存储在NSNumber实例中并返回它

    • 如果是NSNumber不支持的标量类型,转换为NSValue对象并返回该对象

  • 【第六步】如果以上所有方法都失败,系统调用该对象的valueForUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常

6、自定义KVC

6.1 相关方法

  1. - (BOOL)lg_performSelectorWithMethodName:(NSString *)methodName value:(id)value{
  2. if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
  3. #pragma clang diagnostic push
  4. #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
  5. [self performSelector:NSSelectorFromString(methodName) withObject:value];
  6. #pragma clang diagnostic pop
  7. return YES;
  8. }
  9. return NO;
  10. }
  11. - (id)performSelectorWithMethodName:(NSString *)methodName{
  12. if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
  13. #pragma clang diagnostic push
  14. #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
  15. return [self performSelector:NSSelectorFromString(methodName) ];
  16. #pragma clang diagnostic pop
  17. }
  18. return nil;
  19. }
  20. - (NSMutableArray *)getIvarListName{
  21. NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
  22. unsigned int count = 0;
  23. Ivar *ivars = class_copyIvarList([self class], &count);
  24. for (int i = 0; i<count; i++) {
  25. Ivar ivar = ivars[i];
  26. const char *ivarNameChar = ivar_getName(ivar);
  27. NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
  28. NSLog(@"ivarName == %@",ivarName);
  29. [mArray addObject:ivarName];
  30. }
  31. free(ivars);
  32. return mArray;
  33. }

6.2 KVC存储

  1. // 自定义KVC-存储
  2. - (void)lg_setValue:(nullable id)value forKey:(NSString *)key{
  3. // 1: 判断key
  4. if (key == nil || key.length == 0) {
  5. return;
  6. }
  7. // 2: setter:set<Key>:→_set<Key>→setIs<Key>
  8. // key要大写
  9. NSString *Key = key.capitalizedString;
  10. // 拼接方法
  11. NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
  12. NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
  13. NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
  14. if ([self lg_performSelectorWithMethodName:setKey value:value]) {
  15. NSLog(@"*********%@**********",setKey);
  16. return;
  17. }else if ([self lg_performSelectorWithMethodName:_setKey value:value]) {
  18. NSLog(@"*********%@**********",_setKey);
  19. return;
  20. }else if ([self lg_performSelectorWithMethodName:setIsKey value:value]) {
  21. NSLog(@"*********%@**********",setIsKey);
  22. return;
  23. }
  24. // 3: 判断能否直接赋值实例变量,如果accessInstanceVariablesDirectly返回NO,奔溃
  25. if (![self.class accessInstanceVariablesDirectly] ) {
  26. @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
  27. }
  28. // 4: 间接变量
  29. // 获取 ivar -> 遍历 containsObjct
  30. // 4.1: 定义一个收集实例变量的可变数组
  31. NSMutableArray *mArray = [self getIvarListName];
  32. // _<key>→_is<Key>→<key>→is<Key>
  33. NSString *_key = [NSString stringWithFormat:@"_%@",key];
  34. NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
  35. NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
  36. if ([mArray containsObject:_key]) {
  37. // 4.2: 获取相应的 ivar
  38. Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
  39. // 4.3: 对相应的 ivar 设置值
  40. object_setIvar(self , ivar, value);
  41. return;
  42. }else if ([mArray containsObject:_isKey]) {
  43. Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
  44. object_setIvar(self , ivar, value);
  45. return;
  46. }else if ([mArray containsObject:key]) {
  47. Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
  48. object_setIvar(self , ivar, value);
  49. return;
  50. }else if ([mArray containsObject:isKey]) {
  51. Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
  52. object_setIvar(self , ivar, value);
  53. return;
  54. }
  55. // 5: 找不到,奔溃
  56. @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
  57. }

6.3 KVC读取

  1. - (nullable id)lg_valueForKey:(NSString *)key{
  2. // 1: 判断key
  3. if (key == nil || key.length == 0) {
  4. return nil;
  5. }
  6. // 2: 找到相关方法get<Key>→<key> countOf<Key> objectIn<Key>AtIndex
  7. // key要大写
  8. NSString *Key = key.capitalizedString;
  9. // 拼接方法
  10. NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
  11. NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
  12. NSString *_key = [NSString stringWithFormat:@"_%@",key];
  13. NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
  14. #pragma clang diagnostic push
  15. #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
  16. if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
  17. return [self performSelector:NSSelectorFromString(getKey)];
  18. }
  19. else if ([self respondsToSelector:NSSelectorFromString(key)]){
  20. return [self performSelector:NSSelectorFromString(key)];
  21. }
  22. else if ([self respondsToSelector:NSSelectorFromString(isKey)]){
  23. return [self performSelector:NSSelectorFromString(isKey)];
  24. }
  25. else if ([self respondsToSelector:NSSelectorFromString(_key)]){
  26. return [self performSelector:NSSelectorFromString(_key)];
  27. }
  28. else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]){
  29. NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];
  30. if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
  31. int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
  32. NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
  33. for (int i = 0; i<num-1; i++) {
  34. num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
  35. }
  36. for (int j = 0; j<num; j++) {
  37. id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
  38. [mArray addObject:objc];
  39. }
  40. return mArray;
  41. }
  42. }
  43. #pragma clang diagnostic pop
  44. // 3: 判断是否能够直接赋值实例变量
  45. if (![self.class accessInstanceVariablesDirectly] ) {
  46. @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
  47. }
  48. // 4: 找相关实例变量进行赋值
  49. // 4.1: 定义一个收集实例变量的可变数组
  50. NSMutableArray *mArray = [self getIvarListName];
  51. // _<key>→_is<Key>→<key>→is<Key>
  52. // _name→_isName→name→isName
  53. NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
  54. if ([mArray containsObject:_key]) {
  55. Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
  56. return object_getIvar(self, ivar);;
  57. }else if ([mArray containsObject:_isKey]) {
  58. Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
  59. return object_getIvar(self, ivar);;
  60. }else if ([mArray containsObject:key]) {
  61. Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
  62. return object_getIvar(self, ivar);;
  63. }else if ([mArray containsObject:isKey]) {
  64. Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
  65. return object_getIvar(self, ivar);;
  66. }
  67. return @"";
  68. }

7. 使用场景

7.1 动态设值和取值

  • 通过Key读取和存储:setValue:forKey:valueForKey:

  • 通过keyPath读取和存储:setValue:forKeyPath:valueForKeyPath:

7.2 通过KVC访问和修改私有变量

在日常开发中,对于类的私有属性,在外部定义的对象,是无法直接访问私有属性的。但是对于KVC而言,一个对象没有自己的隐私,所以可以通过KVC修改和访问任何私有属性

7.3 多值操作

模型字典转换

  1. //字典转模型
  2. - (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
  3. //模型转字典
  4. - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

7.4 修改一些系统空间的内部属性

在日常开发中,很多UI控件都是在其内部由多控件组合而成,这些内部控件苹果并没有提供访问的API,但是使用KVC可以解决这个问题,例如:自定义tabbar、修改UITextField中的placeHolderText

7.5 用KVC实现高阶消息传递

在对容器类使用KVC时,valueForKey:将会被传递给容器中的每一个对象,而不是对容器本身进行操作,结果会被添加到返回的容器中,这样,可以很方便的操作集合,然后返回另一个集合

总结

基本介绍:

  • KVC全称Key-Value Coding(键值编码),是一种由NSKeyValueCoding非正式协议启用的机制,对象采用它来提供对其属性的间接访问
  • 当一个对象符合键值编码时,它的属性可以通过字符串Key来寻址。这种间接访问机制补充了实例变量及其关联的访问方法所提供的直接访问
  • 官方文档:Key-Value Coding Programming Guide

API介绍:

  • KVCAPIFoundation框架中,暂未开源
  • KVC本质上是对NSObjectNSArrayNSDictionaryNSMutableDictionaryNSOrderedSetNSSet等对象,实现NSKeyValueCoding分类,赋予它们Key-Value Coding的能力
  • 通过Key读取和存储
    • - (nullable id)valueForKey:(NSString *)key;
    • - (void)setValue:(nullable id)value forKey:(NSString *)key;
  • 通过keyPath读取和存储
    • - (nullable id)valueForKeyPath:(NSString *)keyPath;
    • - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

API使用:

  • 对象属性
    • 属性:这些是简单的值,例如标量、字符串或布尔值。值对象NSNumber和其他不可变类型NSColor也被视为属性
    • 一对一的关系:这些是具有自己属性的可变对象。一个对象的属性可以在不改变对象本身的情况下改变
    • 对多关系:这些是集合对象。通常使用NSArray或的实例NSSet来保存此类集合,也可能是自定义集合类型
  • 集合属性
    • mutableArrayValueForKey:mutableArrayValueForKeyPath:
      • 它们返回一个行为类似于NSMutableArray对象的代理对象
    • mutableSetValueForKey:mutableSetValueForKeyPath:
      • 它们返回一个行为类似于NSMutableSet对象的代理对象
    • mutableOrderedSetValueForKey:mutableOrderedSetValueForKeyPath:
      • 它们返回一个行为类似于NSMutableOrderedSet对象的代理对象
  • 访问非对象属性
    • 默认键值编码实现使用NSNumber实例包装的标量类型
    • 默认存取用于包装和展开常见NSPointNSRangeNSRect、和NSSize结构
    • 结构类型,可以包装在一个NSValue对象中
  • 验证属性
    • 验证方法认为值对象有效并在YES不改变值或错误的情况下返回
    • 验证方法认为值对象无效,但选择不更改它。在这种情况下,该方法返回NO并将错误引用(如果由调用者提供)设置为NSError指示失败原因的对象
    • 验证方法认为值对象无效,但会创建一个新的有效对象作为替换。在这种情况下,该方法返回YES同时保持错误对象不变。在返回之前,该方法修改值引用以指向新的值对象。当它进行修改时,该方法总是创建一个新对象,而不是修改旧对象,即使值对象是可变的

KVC的存储过程:

  • 【第一步】查找是否存在以下三种setter方法,查找顺序:set<Key>:_set<Key>setIs<Key>

    • key是指成员变量名,首字母大小写需要符合KVC的命名规范

    • 存在任意一种setter方法,直接设置属性的value

    • 如果都不存在,进入【第二步】

  • 【第二步】:查找accessInstanceVariablesDirectly方法的返回值

    • 返回YES,查找间接访问的实例变量进行赋值,查找顺序:_<key>_is<Key><key>is<Key>

      • 如果找到其中任意一个实例变量,可对其赋值

      • 如果都未找到,进入【第三步】

    • 返回NO,进入【第三步】

  • 【第三步】如果setter方法或实例变量都没有找到,系统会调用该对象的setValue:forUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常

KVC的读取过程:

  • 【第一步】查找getter方法,查找顺序:get<Key><key>is<Key>_<key>

    • 如果找到,进入【第五步】

    • 如果未找到,进入【第二步】

  • 【第二步】查找countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes:

    • 如果找到第一个或其他两个中的至少一个,则会创建一个响应所有NSArray方法的集合代理对象,并返回该对象,即:NSKeyValueArray,属于NSArray的子类。代理对象随后将接收到任何NSArray消息转换为countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes:消息的某种组合,用来创建键值编码对象

      • 如果原始对象还实现了一个名为get<Key>:range:之类的可选方法,则代理对象也将在适当时使用该方法

      • 方法名的命名规则,要符合KVC的标准命名方法,包括方法签名

    • 如果未找到,进入【第三步】

  • 【第三步】查找以下几种方法,countOf<Key>enumeratorOf<Key>memberOf<Key>:

    • 如果三个方法都找到,则会创建一个响应所有NSSet方法的集合代理对象,并返回该对象,此代理对象随后将其收到的所有NSSet消息转换为countOf<Key>enumeratorOf<Key>memberOf<Key>:消息的某种组合,用于创建它的对象

    • 如果未找到,进入【第四步】

  • 【第四步】查找accessInstanceVariablesDirectly方法的返回值

    • 返回YES,依次搜索_<key>_is<Key><key>is<Key>的实例变量

      • 找到实例变量,直接获取实例变量的值,进入【第五步】

      • 未找到,进入【第六步】

    • 返回NO,进入【第六步】

  • 【第五步】根据搜索到的属性值的类型,返回不同的结果

    • 如果是对象指针,则直接返回结果

    • 如果是NSNumber支持的标量类型,则将其存储在NSNumber实例中并返回它

    • 如果是NSNumber不支持的标量类型,转换为NSValue对象并返回该对象

  • 【第六步】如果以上所有方法都失败,系统调用该对象的valueForUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常