iOS之Key-Value Coding(KVC)

什么是KVC?

Key-Value Coding即键值编码,简称KVC.
通过阅读Apple官方文档可以得知,KVC是由NSKeyValueCoding非正式协议启用的一种机制,这种机制为对象提供了对其属性进行间接访问的方式.当对象符合KVC的要求时,其属性可以通过字符串参数和简洁统一的消息接口进行访问.这种间接访问方式是对实例变量及其相关访问器方法提供的直接访问方式的一种补充.

KVC的定义

  • KVC具体实现在Foundation框架的NSKeyValueCoding中:
    • kvc.png
  • NSObject(NSKeyValueCoding)的写法可以看出是NSObject的一个分类,提供了如下接口:
    • NSKeyValueCoding.png

KVC的使用

基础使用

KVC主要对基础数据类型,集合类型对象类型进行操作,首先定义对象如下:

typedef struct {
    float x, y, z;
} ThreeFloats;

@interface SomeObj : NSObject

@property (nonatomic, copy) NSString *sName;

@end

@interface KVCObj : NSObject

// 结构体类型
@property (nonatomic) ThreeFloats threeFloats;

// 基础数据类型
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;

// 集合类型
@property (nonatomic, strong) NSMutableArray *mArr;

// 对象类型
@property (nonatomic, strong) SomeObj *someObj;

@end
复制代码
  • KVC的几种基本使用方式:
    • setValue:(nullable id)value forKey:(NSString *)key
    • valueForKey:(NSString *)key
    • setValue:(nullable id)value forKeyPath:(NSString *)keyPath
    • valueForKeyPath:(NSString *)keyPath
    • 可以参考如下代码:
    KVCObj *obj = [KVCObj.alloc init];

    // 以setter直接方式赋值
    obj.age = 18;

    // 以KVC间接方式赋值
    [obj setValue:@"KVC" forKey:@"name"];

    // KVC集合类型赋值
    // 第一种
    NSMutableArray *mArr = [@[@"1",@"2",@"3"] mutableCopy];
    [obj setValue:mArr forKey:@"mArr"];
    // 第二种
    NSMutableArray *mArray = [obj mutableArrayValueForKey:@"mArr"];
    mArray[0] = @"9";

    // KVC对结构体的赋值与取值
    ThreeFloats floats = {1.,2.,3.};
    NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
    [obj setValue:value forKey:@"threeFloats"];

    NSValue *value1 = [obj valueForKey:@"threeFloats"];
    ThreeFloats th;
    [value1 getValue:&th];
    NSLog(@"%f-%f-%f",th.x,th.y,th.z);

    // KVC跨层访问,注意使用的是setValue:forKeyPath:
    SomeObj *someObj = [SomeObj.alloc init];
    someObj.sName = @"";
    obj.someObj = someObj;
    [obj setValue:@"some obj sName" forKeyPath:@"someObj.sName"];
    
    NSLog(@"\n%@\n%d\n%@\n%@", obj.name, obj.age, obj.mArr, obj.someObj.sName);
复制代码

进阶使用

  • 字典操作
    • setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues
    • dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys
    • 可以参考如下代码:
        NSDictionary* dic = @{
            @"name":@"kvc",
            @"age":@18,
            @"mArr":[@[@"1",@"2"] mutableCopy]
        };
        KVCObj *obj = [KVCObj.alloc init];
        // 字典转模型
        [obj setValuesForKeysWithDictionary:dic];
        NSLog(@"%@",obj);
        // 键数组转模型到字典
        NSArray *keys = @[@"name",@"age"];
        NSDictionary *dict = [obj dictionaryWithValuesForKeys:keys];
        NSLog(@"%@",dict);
    复制代码
  • 消息传递
    • valueForKeyPath:(NSString *)keyPath
    • 可以参考如下代码:
        NSArray *arr = @[@"One",@"Two",@"Three",@"Four"];
        // length消息实际上从arr传递给了每一个string对象,和跨层访问有些相似
        NSArray *lenArr= [arr valueForKeyPath:@"length"];
        NSLog(@"%@",lenArr);
        // lowercaseString消息将被数组内的字符串对象接收并处理
        NSArray *lowArr= [arr valueForKeyPath:@"lowercaseString"];
        NSLog(@"%@",lowArr);
    复制代码
  • 聚合操作符
    • 首先生成一个包含多个对象的数组:
        NSMutableArray *objArr = [@[] mutableCopy];
        for (int i = 0; i < 5; i++) {
          KVCObj *obj = [KVCObj new];
          NSDictionary *dict = @{
            @"name":@"Tom",
            @"age":@(18 + 5*i),
          };
          [obj setValuesForKeysWithDictionary:dict];
          [objArr addObject:obj];
        }
    复制代码
    • @avg
      • 同于计算数组中通过keypath指定的的属性的平均值
      • 参考代码:
      // 获取age的平均值
      float avg = [[objArr valueForKeyPath:@"@avg.age"] floatValue];
      NSLog(@"%f", avg);      //打印结果: 28.000000
      复制代码
    • @count
      • 同于计算数组中通过keypath指定的的属性的总数
      • 参考代码:
      // 获取age的个数
      int count = [[objArr valueForKeyPath:@"@count.age"] intValue];
      NSLog(@"%d", count);    //打印结果: 5
      复制代码
    • @sum
      • 同于计算数组中通过keypath指定的的属性的总和
      • 参考代码:
      // 获取age的总和
      int sum = [[objArr valueForKeyPath:@"@sum.age"] intValue];
      NSLog(@"%d", sum);      //打印结果: 140
      复制代码
    • @max
      • 同于计算数组中通过keypath指定的的属性的最大值
      • 参考代码:
      // 获取age的最大值
      int max = [[objArr valueForKeyPath:@"@max.age"] intValue];
      NSLog(@"%d", max);      //打印结果: 38
      复制代码
    • @min
      • 同于计算数组中通过keypath指定的的属性的最小值
      • 参考代码:
      // 获取age的最小值
      int min = [[objArr valueForKeyPath:@"@min.age"] intValue];
      NSLog(@"%d", min);      //打印结果: 18
      复制代码
  • 数组操作符/嵌套数组操作符
    • 首先生成一个包含多个对象的数组和一个嵌套数组:
        NSMutableArray *objArr1 = [@[] mutableCopy];
        NSMutableArray *objArr2 = [@[] mutableCopy];
        for (int i = 0; i < 5; i++) {
            KVCObj *obj = [KVCObj new];
            NSDictionary *dict = @{
                @"name":@"Tom",
                @"age":@(18 + (arc4random() % 5)),
            };
            [obj setValuesForKeysWithDictionary:dict];
            [objArr1 addObject:obj];
            [objArr2 addObject:obj];
        }
        // 嵌套数组
        NSArray *arr = @[objArr1, objArr2];
    复制代码
    • @unionOfObjects
      • 返回操作对象指定属性的集合
      • 参考代码:
      NSArray *arr1 = [objArr1 valueForKeyPath:@"@unionOfObjects.age"];
      NSLog(@"arr1 = %@", arr1);  //打印结果: arr1 = (18,22,22,18,22)
      复制代码
    • @distinctUnionOfObjects
      • 返回操作对象指定属性的集合 — 去重
      • 参考代码:
      NSArray *arr2 = [objArr1 valueForKeyPath:@"@distinctUnionOfObjects.age"];
      NSLog(@"arr2 = %@", arr2);  //打印结果:arr2 = (22,18)
      复制代码
    • @unionOfArrays
      • 返回嵌套数组内对象指定属性的集合
      • 参考代码:
      NSArray* arr3 = [arr valueForKeyPath:@"@unionOfArrays.age"];
      NSLog(@"arr3 = %@", arr3);  //打印结果:arr3 = (20,18,20,19,22,20,18,20,19,22)
      复制代码
    • @distinctUnionOfArrays
      • 返回嵌套数组内对象指定属性的集合 — 去重
      • 参考代码:
      NSArray *arr4 = [arr valueForKeyPath:@"@distinctUnionOfArrays.age"];
      NSLog(@"arr4 = %@", arr4);  //打印结果:arr4 = (22,18,19,20)
      复制代码
  • 此外还有@distinctUnionOfSets集合操作符等,感兴趣的可以到Apple官方文档查阅,这里不再一一赘述.
  • 补充一个,利用KVC的数组操作符,对二维数组进行铺开排序
NSArray *arr = @[@[@1, @3, @6],
                 @[@9, @4],
                 @[@8],
                 @[@7, @2, @5]];
NSArray *resultArr = [[arr valueForKeyPath:@"@unionOfArrays.self"] sortedArrayUsingComparator:^NSComparisonResult(id  _Nonnull obj1, id  _Nonnull obj2) {
        return [obj1 compare:obj2];
    }];

NSLog(@"%@", resultArr);    //打印结果: (1,2,3,4,5,6,7,8,9)
复制代码

KVC的流程

以下结论通过官方文档得出,感兴趣的同学可以按照流程进程验证.

普通赋值过程

setValue:forKey:的默认实现:提供keyvalue作为输入参数,尝试在接收调用的对象内部,对名为key的属性设置值value,具体步骤如下:

  1. 按照set<Key>:,_set<Key>的顺序查找对应的setter,如果能够找到,就将输入值作为参数并调用该方法(如有必要也将对参数进行解包,转换等操作)
  2. 如果没有找到简单的setter方法,并且类方法accessInstanceVariablesDirectly返回为YES,就会按照_<key>,_is<Key>,<key>,is<Key>的顺序查找实例变量.如果找到了,直接将value值设置给实例变量.
  3. 如果既没有找到setter方法,也没有找到实例变量,就会调用setValue:forUndefinedKey:方法.默认情况下,会在此处报出异常,但是任何一个NSObject的子类,都可以在此处做出相应合理的操作(重写该方法).

普通取值过程

valueForKey:的默认实现:提供一个key作为输入参数,在响应该方法的类的实例中,进行如下步骤的操作:

  1. 在实例中按照get<Key>,<key>,is<Key>,_<key>的顺序查找getter,如果找到了,就调用对应的方法并带着结果跳转到第5步,否则,将继续执行步骤2.
  2. 如果没有找到简单的getter方法,就在实例的方法中继续查找命名规则能够匹配countOf<Key>,objectIn<Key>AtIndex:,<key>AtIndexes:的方法.
    如果找到了其中一个方法,并且能够找到另外两个中的至少一个.就会创建一个集合代理对象,该对象响应NSArray的所有方法,然后返回该对象.否则,将继续执行步骤3.
    代理对象随后会将其接收到的所有的NSArray消息转换为countOf<Key>,objectIn<Key>AtIndex:<key>AtIndexes:消息的组合并给到创建它的key-value coding兼容对象.如果原始对象还实现了一个名为get<Key>:range:的可选方法,那么代理对象也会在适当的时候使用该方法。
    实际上,代理对象和创建它的key-value coding兼容对象一起工作,使得该属性的行为如同NSArray一样.
  3. 如果没有找到简单的getter方法,也没有找到步骤2中的数组访问的一系列方法,就会查找countOf<Key>,enumeratorOf<Key>memberOf<Key>:方法.
    如果这三个方法全都能够找到,就会创建一个响应NSSet全部方法的集合代理对象,并将其返回.否则,将继续执行步骤4.
    代理对象随后会将其接收到的所有NSSet消息转换为countOf<Key>,enumeratorOf<Key>memberOf<Key>:消息的组合并给到创建它的对象.
    实际上,代理对象和创建它的key-value coding兼容对象一起工作,使得该属性的行为如同NSSet一样.
  4. 如果既没有找到简单的getter方法,也没有找到步骤2和步骤3中的一系列方法,并且消息接收者(即调用valueForKey:的对象)的类的类方法accessInstanceVariablesDirectly返回YES,就会按照_<key>,_is<Key>,<key>is<Key>的顺序查找实例变量.
    如果找到了,就会立即获取实例变量的值并继续执行步骤5,否则将跳转到步骤6.
  5. 如果找到的属性值是一个对象指针,就返回这个值.
    如果找到的属性值是一个被NSNumber支持的基础数据类型,就将其存储到一个NSNumber实例中并返回.
    如果找到的属性值不被NSNumber支持,就将其转换为NSValue对象并返回.
  6. 如果以上步骤全都执行失败,就会调用valueForUndefinedKey:方法.默认情况下,会在此处报出异常,但是任何一个NSObject的子类,都可以在此处做出相应合理的操作(重写该方法).

其他

可变数组(NSMutableArray),可变有序集合(NSMutableOrderedSet)和可变集合(NSMutableSet)的步骤不在此处一一赘述,感兴趣的同学可以前往官方文档自行学习.

KVC的优缺点

  1. 通过KVC的取值和赋值过程可以看出,如果我们使用KVC操作取值赋值,并且命名不够规范的情况下,就会大大增加其要执行的步骤,虽然微乎其微,但仍然是性能的浪费.
  2. 尽量不要使用KVC对集合类型进行操作,不仅步骤复杂,还会在过程中创建额外的代理对象,造成不必要的性能消耗.
  3. KVC操作过程中的参数都是字符串,参数创建和传递过程中都会有安全隐患.
  4. 虽然KVC流程略复杂,字符串参数也显得比较不安全和不好维护,但它的优点也很明显,KVC的本质是对对象的方法列表和内存中实例变量的直接操作,我们可以通过KVC的方式访问私有的属性和成员变量.比如UIKit中的许多封装好的控件,并没有为我们提供足够的API对其进行样式调整,但我们可以通过KVC的方式操作其私有成员变量以达到目的.

补充

  • setValue:forKey:流程图

KVC setValueForKey-2.png

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享