简单来说:可以,完全没问题。 KVC 的强大之处就在于它打破了“对象”与“非对象”之间的边界,让你能像处理对象一样处理 int、bool 甚至是自定义的 CGRect。
在底层,KVC 靠的是一套严密的 “自动装箱/拆箱(Boxing/Unboxing)” 机制和 Runtime 类型编码解析。
1. 访问非对象类型的表现
当你使用 KVC 时,所有的非对象类型都会被自动包装成 NSNumber 或 NSValue:
- 标量 (Scalar):
int,float,BOOL,NSInteger等,会被包装成NSNumber。 - 结构体 (Struct):
CGRect,CGPoint,CGSize等,会被包装成NSValue。
Objective-C
// 1. 设值:传入的是对象(NSNumber),底层存的是标量(int)
[person setValue:@25 forKey:@"age"];
// 2. 取值:底层取的是标量,返回的是对象(NSValue)
NSValue *rectValue = [view valueForKey:@"frame"];
CGRect frame = [rectValue rectValue];
2. 底层原理:自动装箱与拆箱
KVC 能够实现这种“跨界访问”,主要分为三个技术环节:
A. 获取类型编码 (Type Encoding)
当调用 setValue:forKey: 时,KVC 首先通过 Runtime 函数(如 ivar_getTypeEncoding 或 method_getTypeEncoding)获取该属性的 Type Encoding。
- 如果属性是
int,编码是"i"。 - 如果属性是
CGRect,编码是"{CGRect={CGPoint=dd}{CGSize=dd}}"。
B. 拆箱 (Unboxing) —— 设值过程
如果 KVC 发现目标地址是标量或结构体,它会检查你传入的参数:
-
校验类型: 检查传入的对象是否是
NSNumber或NSValue。 -
提取原始数据: * 如果是
NSNumber,根据类型编码调用对应的intValue或floatValue。- 如果是
NSValue,调用getValue:将二进制数据拷贝出来。
- 如果是
-
内存写入: 根据 Ivar 的偏移量(Offset),直接将提取出的原始字节写入对象的内存地址。
C. 装箱 (Boxing) —— 取值过程
调用 valueForKey: 时,流程反过来:
- 读取原始数据: 根据偏移量从内存中读取对应的字节。
- 创建包装对象: 根据类型编码,动态创建一个
NSNumber或NSValue,将原始数据封装进去并返回。
3. 特殊边界处理
对于非对象类型,KVC 有两个非常特殊的底层拦截点:
处理 nil 赋值:setNilValueForKey:
如果你尝试 [obj setValue:nil forKey:@"age"](age 是 int),KVC 会发现无法将 nil 拆箱为基本类型。
- 默认行为: 抛出
NSInvalidArgumentException并崩溃。 - 底层原理: 在崩溃前,它会调用
setNilValueForKey:。你可以通过重写此方法,在给基本类型赋nil时提供一个默认值(比如0),从而避免崩溃。
验证逻辑:validateValue:forKey:error:
这是 KVC 提供的可选接口。在底层,它可以让你在数据真正写入非对象内存之前,检查这个数值是否在合法范围内(例如 age 不能为负数)。
4. 性能考量
虽然 KVC 访问非对象类型非常方便,但它的底层开销比直接访问要大得多:
- 创建包装对象: 每次取值都要在堆上创建一个
NSNumber/NSValue。 - 字符串解析: 每次都要解析 Key 字符串和类型编码字符串。
- 反射开销: 涉及多次 Runtime 查找。
| 访问方式 | 性能 | 转换成本 |
|---|---|---|
| 点语法 / 直接访问 | 极快 | 无 |
| KVC | 较慢 | 需要装箱/拆箱 |