通过iOS KVC模式打破传统编码壁垒

2,559 阅读7分钟

KVC(Key-Value Coding)即键值编码,是 iOS 开发中一种强大的机制,它允许开发者通过字符串形式的属性名称(key)来间接访问对象的属性,而无需直接调用属性的 gettersetter 方法。KVCKVOCore Data 等技术的基石,理解 KVC 对于深入掌握 iOS 开发至关重要。在本文中,我们将深入探讨 KVC 的实现原理和使用方法,并介绍如何使用 KVC 进行数据模型转换和对象关系映射。

实现原理

在 Objective-C 中,每个对象都有一个isa指针,指向其类对象的实例。KVC 机制利用isa指针、运行时(runtime)和消息传递机制来实现属性的访问和操作。

SetValue:forKey:

调用步骤

  1. 首先按照set<Key>、 _set<Key> 的顺序查找方法,如果找到了就调用并赋值,完成KVC流程
  2. 如果没找到以上两个方法,并且类方法accessInstanceVariablesDirectly返回 YES,则按照 _<Key>、_is<Key>、<Key>、is<Key>的顺序查找,如果找到其中一个就将传入的值赋值,并完成KVC流程
  3. 如果以上两步都没有找到,就调用 setValue:forUndefinedKey:方法,并抛出异常

Search Pattern for the Basic Setter

The default implementation of setValue:forKey:, given key and value parameters as input, attempts to set a property named key to value (or, for non-object properties, the unwrapped version of value, as described in Representing Non-Object Values) inside the object receiving the call, using the following procedure:

  1. Look for the first accessor named set<Key>: or _set<Key>, in that order. If found, invoke it with the input value (or unwrapped value, as needed) and finish.
  2. If no simple accessor is found, and if the class method accessInstanceVariablesDirectly returns YES, look for an instance variable with a name like _<key>_is<Key><key>, or is<Key>, in that order. If found, set the variable directly with the input value (or unwrapped value) and finish.
  3. Upon finding no accessor or instance variable, invoke setValue:forUndefinedKey:. This raises an exception by default, but a subclass of NSObject may provide key-specific behavior.

流程图

ValueForKey:

调用步骤

  1. 按照 get<Key>、<key>、is<Key>、_<key>的顺序在实例对象中查找是否有已实现的方法,如果找到就调用,并执行第5步,否则继续下一步
  2. 如果上一步中没有找到,并且是一个数组类型,则调用数组相关的方法 如countOf<Key>、objectIn<Key>AtIndex:、<Key>AtIndex
  3. 如果上一步没找到,并且是一个集合类型,则调用集合相关方法 如countOf<Key>、enumeratorOf<Key>、memberOf<Key>
  4. 如果以上都没找到,则判断accessInstanceVariablesDirectly 是否为 YES,如果为YES,则依次查找成员变量_<key>_is<Key><key>is<Key>,如果查找到则进行第5步,否则进行第6步
  5. 查找到值,需要对值进行处理
  • 如果检索到的值是一个对象指针,则直接返回该对象
  • 如果是一个 NSNumber标量,则将其存储在NSNumber中并返回
  • 如果不是一个 NSNumber标量,则存储在 NSValue中并返回
  1. 在1~4未找到的情况下,抛出异常

Search Pattern for the Basic Getter

The default implementation of valueForKey:, given a key parameter as input, carries out the following procedure, operating from within the class instance receiving the valueForKey: call.

  1. Search the instance for the first accessor method found with a name like get<Key><key>is<Key>, or _<key>, in that order. If found, invoke it and proceed to step 5 with the result. Otherwise proceed to the next step.

  2. If no simple accessor method is found, search the instance for methods whose names match the patterns countOf<Key> and objectIn<Key>AtIndex: (corresponding to the primitive methods defined by the NSArray class) and <key>AtIndexes: (corresponding to the NSArray method objectsAtIndexes:).

    If the first of these and at least one of the other two is found, create a collection proxy object that responds to all NSArray methods and return that. Otherwise, proceed to step 3.

    The proxy object subsequently converts any NSArray messages it receives to some combination of countOf<Key>objectIn<Key>AtIndex:, and <key>AtIndexes: messages to the key-value coding compliant object that created it. If the original object also implements an optional method with a name like get<Key>:range:, the proxy object uses that as well, when appropriate. In effect, the proxy object working together with the key-value coding compliant object allows the underlying property to behave as if it were an NSArray, even if it is not.

  3. If no simple accessor method or group of array access methods is found, look for a triple of methods named countOf<Key>enumeratorOf<Key>, and memberOf<Key>: (corresponding to the primitive methods defined by the NSSet class).

    If all three methods are found, create a collection proxy object that responds to all NSSet methods and return that. Otherwise, proceed to step 4.

    This proxy object subsequently converts any NSSet message it receives into some combination of countOf<Key>enumeratorOf<Key>, and memberOf<Key>: messages to the object that created it. In effect, the proxy object working together with the key-value coding compliant object allows the underlying property to behave as if it were an NSSet, even if it is not.

  4. If no simple accessor method or group of collection access methods is found, and if the receiver's class method accessInstanceVariablesDirectly returns YES, search for an instance variable named _<key>_is<Key><key>, or is<Key>, in that order. If found, directly obtain the value of the instance variable and proceed to step 5. Otherwise, proceed to step 6.

  5. If the retrieved property value is an object pointer, simply return the result.

    If the value is a scalar type supported by NSNumber, store it in an NSNumber instance and return that.

    If the result is a scalar type not supported by NSNumber, convert to an NSValue object and return that.

  6. If all else fails, invoke valueForUndefinedKey:. This raises an exception by default, but a subclass of NSObject may provide key-specific behavior.

流程图

iOS KVC 的使用方法

iOS KVC 的使用非常简单,只需遵循以下规则:

  1. 属性名必须是字符串类型,可以直接使用属性名作为字符串,也可以使用通过 NSString 类的实例方法创建的字符串。
  2. getter 方法必须以 get 开头,并且将属性名的首字母大写。
  3. setter 方法必须以 set 开头,并且将属性名的首字母大写,同时要接收一个参数,即要设置的属性值。

下面是一个使用 KVC 访问和设置对象属性的示例:

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;

@property (nonatomic, assign) NSInteger age;

@end

@implementation Person

@end

// 创建 Person 对象
Person *person = [[Person alloc] init];

// 使用 KVC 访问属性
NSString *name = [person valueForKey:@"name"];
NSInteger age = [[person valueForKey:@"age"] integerValue];

// 使用 KVC 设置属性
[person setValue:@"Tom" forKey:@"name"];
[person setValue:@30 forKey:@"age"];
​

在上面的示例中,我们首先定义了一个名为 Person 的类,并定义了两个属性:name 和 age。然后我们创建了一个 Person 对象,并使用 KVC 机制访问和设置了其属性。

使用场景

字典与模型对象的转换

场景描述: 从网络请求或 JSON 数据中获取的字典数据,可以通过 KVC 快速转换为自定义的模型对象

示例:

NSDictionary *dict = @{@"name": @"Tom", @"age": @30};
Person *person = [[Person alloc] init];
[person setValuesForKeysWithDictionary:dict];

优点: 简化代码,避免手动逐个赋值。

扩展: 处理键名不一致的情况

假设字典的键是 @"nickname",而 Person 类的属性是 name,可以通过重写 setValue:forKey: 方法来处理:

@implementation Person

- (void)setValue:(id)value forKey:(NSString *)key {
    if ([key isEqualToString:@"nickname"]) {
        self.name = value;
    } else {
        [super setValue:value forKey:key];
    }
}

@end

动态访问和修改属性

场景描述: 在运行时动态地访问或修改对象的属性,而不需要直接调用属性的 getter 或 setter 方法

示例

NSString *name = [person valueForKey:@"name"];
[person setValue:@"Jerry" forKey:@"name"];

优点: 适用于需要根据运行时条件动态操作属性的场景

KVO(Key-Value Observing)的基础

场景描述: KVO 是 iOS 中观察属性变化的机制,而 KVC 是 KVO 实现的基础。通过 KVC 修改属性时,KVO 会触发相应的通知

示例:

[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
[person setValue:@"Jerry" forKey:@"name"]; // 触发 KVO 通知

优点: 实现数据驱动的 UI 更新

集合运算符

场景描述: KVC 提供了一些集合运算符,可以对数组或集合中的对象进行快速计算或操作。 示例:

NSArray *ages = @[@20, @30, @40];
NSNumber *avgAge = [ages valueForKeyPath:@"@avg.self"]; // 计算平均值
NSNumber *maxAge = [ages valueForKeyPath:@"@max.self"]; // 计算最大值

支持的运算符:

  • @avg:平均值
  • @count:数量
  • @sum:总和
  • @min:最小值
  • @max:最大值

优点: 简化集合数据的统计操作

总结

  • KVC 提供了一种便捷的方式将字典转换为对象,但需要确保字典的键与对象的属性名一致,并且值的类型兼容。
  • 对于复杂的场景(如键名不一致、嵌套对象等),可以通过重写 KVC 相关方法来实现自定义逻辑。
  • 在实际开发中,KVC 常用于解析 JSON 数据、实现字典转模型等功能。

参考文献

苹果官方文档 - 键值编码篇