6-5.【OC】【KVC/KVO】KVC 如何处理类型转换?例如,NSNumber → NSInteger,底层做了什么?

3 阅读3分钟

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。如果是一个 NSNumberNSValue,它会调用对象的内置方法将其转化为基础类型。

  • 对于 NSNumber: KVC 会根据第一步获取的 "q",调用 [value longLongValue]
  • 对于 NSValue: 如果目标是结构体(如 CGRect),它会调用 [value getValue:]

第三步:内存写入

  • 如果是通过 Setter 访问:KVC 使用 objc_msgSend,但参数不再是 NSNumber 对象,而是拆箱后的原始数值(通过栈或寄存器传递)。
  • 如果是通过 Ivar 访问:KVC 直接通过指针偏移找到地址,将原始数值拷贝到该内存块中。

2. 标量类型(Scalar)与对象的映射关系

KVC 预设了一套完整的映射表,用于在对象(Object)和标量(Scalar)之间转换:

目标属性类型期望的包装对象底层调用的拆箱方法
int / NSIntegerNSNumberintValue / integerValue
float / doubleNSNumberfloatValue / doubleValue
BOOLNSNumberboolValue
CGRect / CGPointNSValuecgRectValue / cgPointValue
char *NSStringUTF8String

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 提供了两个防御性方法:

  1. -setNilValueForKey:

    当你尝试把 nil 传给一个 int 属性时。默认崩溃,你可以重写它来赋默认值(如 0)。

  2. -validateValue:forKey:error:

    这是 KVC 提供的验证机制。虽然它不会被 setValue: 自动调用,但在某些框架(如 Core Data)中,它被用来在转换/赋值前检查数据合法性。


5. 性能影响:装箱的代价

虽然 KVC 很方便,但它的类型转换是有成本的:

  • 内存开销: 必须先创建一个 NSNumber 对象(即便它是 Tagged Pointer)。
  • 查找开销: 需要解析类型编码字符串。
  • 转换开销: 涉及多次函数跳转和指令分发。

结论: 在高性能循环中(如处理万级以上的模型转换),建议直接使用 Setter 方法C 数组操作,避免 KVC 的类型自动转换开销。