KVC 的类型转换并不是简单的强制转换,而是一套基于 Objective-C 类型编码(Type Encoding) 的自动化**装箱(Boxing)与拆箱(Unboxing)**机制。
当你调用 setValue:forKey: 时,KVC 内部会通过 NSInvocation 或底层的 Runtime 指针操作,根据目标属性的实际类型进行转换。
1. 核心步骤:底层转换逻辑
假设你执行:[obj setValue:@(10) forKey:@"age"];(age 在类中定义为 NSInteger)。
第一步:获取类型编码 (Type Encoding)
KVC 首先通过 Runtime 函数 method_getTypeEncoding(针对 Setter 方法)或 ivar_getTypeEncoding(针对成员变量)获取目标属性的类型信息。
- 对于
NSInteger(在 64 位系统下),它得到的编码通常是"q"(long long)。
第二步:识别包装对象 (Unboxing)
KVC 检查传入的 value。如果是一个 NSNumber 或 NSValue,它会调用对象的内置方法将其转化为基础类型。
- 对于 NSNumber: KVC 会根据第一步获取的
"q",调用[value longLongValue]。 - 对于 NSValue: 如果目标是结构体(如
CGRect),它会调用[value getValue:]。
第三步:内存写入
- 如果是通过 Setter 访问:KVC 使用
objc_msgSend,但参数不再是NSNumber对象,而是拆箱后的原始数值(通过栈或寄存器传递)。 - 如果是通过 Ivar 访问:KVC 直接通过指针偏移找到地址,将原始数值拷贝到该内存块中。
2. 标量类型(Scalar)与对象的映射关系
KVC 预设了一套完整的映射表,用于在对象(Object)和标量(Scalar)之间转换:
| 目标属性类型 | 期望的包装对象 | 底层调用的拆箱方法 |
|---|---|---|
int / NSInteger | NSNumber | intValue / integerValue |
float / double | NSNumber | floatValue / doubleValue |
BOOL | NSNumber | boolValue |
CGRect / CGPoint | NSValue | cgRectValue / cgPointValue |
char * | NSString | UTF8String |
3. 结构体(Struct)的特殊处理
对于非基本数字类型(如自定义结构体),KVC 必须依靠 NSValue。
Objective-C
// 假设属性为 @property CGRect frame;
NSValue *value = [NSValue valueWithCGRect:CGRectMake(0, 0, 100, 100)];
[obj setValue:value forKey:@"frame"];
在底层,KVC 会对比 NSValue 内部存储的类型编码(value.objCType)与属性定义的类型编码。如果两者不匹配且无法转换,程序会抛出异常。
4. 转换失败:两个关键的拦截点
如果转换逻辑走不通,KVC 提供了两个防御性方法:
-
-setNilValueForKey:当你尝试把
nil传给一个int属性时。默认崩溃,你可以重写它来赋默认值(如0)。 -
-validateValue:forKey:error:这是 KVC 提供的验证机制。虽然它不会被
setValue:自动调用,但在某些框架(如 Core Data)中,它被用来在转换/赋值前检查数据合法性。
5. 性能影响:装箱的代价
虽然 KVC 很方便,但它的类型转换是有成本的:
- 内存开销: 必须先创建一个
NSNumber对象(即便它是 Tagged Pointer)。 - 查找开销: 需要解析类型编码字符串。
- 转换开销: 涉及多次函数跳转和指令分发。
结论: 在高性能循环中(如处理万级以上的模型转换),建议直接使用 Setter 方法 或 C 数组操作,避免 KVC 的类型自动转换开销。