KVC(Key-Value Coding)即键值编码,是 iOS 开发中一种强大的机制,它允许开发者通过字符串形式的属性名称(key)来间接访问对象的属性,而无需直接调用属性的 getter 和 setter 方法。KVC 是 KVO、Core Data 等技术的基石,理解 KVC 对于深入掌握 iOS 开发至关重要。在本文中,我们将深入探讨 KVC 的实现原理和使用方法,并介绍如何使用 KVC 进行数据模型转换和对象关系映射。
实现原理
在 Objective-C 中,每个对象都有一个isa指针,指向其类对象的实例。KVC 机制利用isa指针、运行时(runtime)和消息传递机制来实现属性的访问和操作。
SetValue:forKey:
调用步骤
- 首先按照
set<Key>、 _set<Key>的顺序查找方法,如果找到了就调用并赋值,完成KVC流程 - 如果没找到以上两个方法,并且类方法
accessInstanceVariablesDirectly返回 YES,则按照_<Key>、_is<Key>、<Key>、is<Key>的顺序查找,如果找到其中一个就将传入的值赋值,并完成KVC流程 - 如果以上两步都没有找到,就调用
setValue:forUndefinedKey:方法,并抛出异常
Search Pattern for the Basic Setter
The default implementation of
setValue:forKey:, givenkeyandvalueparameters as input, attempts to set a property namedkeytovalue(or, for non-object properties, the unwrapped version ofvalue, as described in Representing Non-Object Values) inside the object receiving the call, using the following procedure:
- 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.- If no simple accessor is found, and if the class method
accessInstanceVariablesDirectlyreturnsYES, look for an instance variable with a name like_<key>,_is<Key>,<key>, oris<Key>, in that order. If found, set the variable directly with the input value (or unwrapped value) and finish.- Upon finding no accessor or instance variable, invoke
setValue:forUndefinedKey:. This raises an exception by default, but a subclass ofNSObjectmay provide key-specific behavior.
流程图
ValueForKey:
调用步骤
- 按照
get<Key>、<key>、is<Key>、_<key>的顺序在实例对象中查找是否有已实现的方法,如果找到就调用,并执行第5步,否则继续下一步 - 如果上一步中没有找到,并且是一个数组类型,则调用数组相关的方法 如
countOf<Key>、objectIn<Key>AtIndex:、<Key>AtIndex - 如果上一步没找到,并且是一个集合类型,则调用集合相关方法 如
countOf<Key>、enumeratorOf<Key>、memberOf<Key> - 如果以上都没找到,则判断
accessInstanceVariablesDirectly是否为YES,如果为YES,则依次查找成员变量_<key>,_is<Key>,<key>,is<Key>,如果查找到则进行第5步,否则进行第6步 - 查找到值,需要对值进行处理
- 如果检索到的值是一个对象指针,则直接返回该对象
- 如果是一个 NSNumber标量,则将其存储在NSNumber中并返回
- 如果不是一个 NSNumber标量,则存储在 NSValue中并返回
- 在1~4未找到的情况下,抛出异常
Search Pattern for the Basic Getter
The default implementation of
valueForKey:, given akeyparameter as input, carries out the following procedure, operating from within the class instance receiving thevalueForKey:call.
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.If no simple accessor method is found, search the instance for methods whose names match the patterns
countOf<Key>andobjectIn<Key>AtIndex:(corresponding to the primitive methods defined by theNSArrayclass) and<key>AtIndexes:(corresponding to theNSArraymethodobjectsAtIndexes:).If the first of these and at least one of the other two is found, create a collection proxy object that responds to all
NSArraymethods and return that. Otherwise, proceed to step 3.The proxy object subsequently converts any
NSArraymessages it receives to some combination ofcountOf<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 likeget<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 anNSArray, even if it is not.If no simple accessor method or group of array access methods is found, look for a triple of methods named
countOf<Key>,enumeratorOf<Key>, andmemberOf<Key>:(corresponding to the primitive methods defined by theNSSetclass).If all three methods are found, create a collection proxy object that responds to all
NSSetmethods and return that. Otherwise, proceed to step 4.This proxy object subsequently converts any
NSSetmessage it receives into some combination ofcountOf<Key>,enumeratorOf<Key>, andmemberOf<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 anNSSet, even if it is not.If no simple accessor method or group of collection access methods is found, and if the receiver's class method
accessInstanceVariablesDirectlyreturnsYES, search for an instance variable named_<key>,_is<Key>,<key>, oris<Key>, in that order. If found, directly obtain the value of the instance variable and proceed to step 5. Otherwise, proceed to step 6.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 anNSNumberinstance and return that.If the result is a scalar type not supported by NSNumber, convert to an
NSValueobject and return that.If all else fails, invoke
valueForUndefinedKey:. This raises an exception by default, but a subclass ofNSObjectmay provide key-specific behavior.
流程图
iOS KVC 的使用方法
iOS KVC 的使用非常简单,只需遵循以下规则:
- 属性名必须是字符串类型,可以直接使用属性名作为字符串,也可以使用通过 NSString 类的实例方法创建的字符串。
- getter 方法必须以 get 开头,并且将属性名的首字母大写。
- 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 数据、实现字典转模型等功能。