KVC(Key-Value Coding)之所以能“突破”访问限制,是因为它本质上是 Objective-C Runtime(运行时) 的一层高级封装。
编译器在编译阶段施加的 private、@protected 或 readonly 约束,在 Runtime 层面仅仅是元数据标签,并不能阻止基于内存偏移的强行访问。
以下是 KVC 绕过访问控制的具体原理:
1. 绕过私有性(private / @protected):基于内存偏移
当 KVC 发现对象没有对应的 Setter/Getter 方法时,它会通过 Runtime 提供的 Ivar 列表(Instance Variable List) 直接操作内存。
-
查找元数据: KVC 调用
class_getInstanceVariable等底层 API,在类的定义中搜索匹配的成员变量名(如_name)。 -
计算偏移量: 一旦找到这个变量的元数据(
Ivar结构体),KVC 就可以通过ivar_getOffset获取该变量相对于对象起始地址的内存偏移量。 -
强行读写: * 取值: 通过
(char *)object + offset定位到内存地址,直接读取数据。- 设值: 将数据直接写入对应的内存空间。
本质: 在底层 C 语言层面,只要知道指针地址和偏移量,就没有所谓的“私有”概念。KVC 正是利用了这种底层的“暴力”访问权。
2. 绕过 readonly:跳过编译检查
如果你将一个属性声明为 readonly,编译器会做两件事:
- 不生成
set方法。 - 阻止代码调用
obj.property = value。
然而,KVC 的设值逻辑并不是调用点语法(Dot Notation),而是调用 setValue:forKey:。
- KVC 的搜索算法(Setter Ivar)在找不到
set方法时,会自动转入 Ivar 查找阶段。 - 即便属性被标记为
readonly,只要其背后的成员变量(如_propertyName)存在,KVC 就会直接修改该变量的值,从而绕过了只读限制。
3. 安全阀:+accessInstanceVariablesDirectly
苹果虽然留了这个“后门”,但也提供了一个控制开关。
KVC 在尝试直接访问 Ivar 之前,会先调用该类的类方法 +accessInstanceVariablesDirectly:
- 默认返回
YES: 允许 KVC 搜索并操作私有成员变量。 - 手动返回
NO: KVC 将严格遵守封装原则。如果找不到对应的set/get方法,即使 Ivar 就在那,它也不会去碰,而是直接触发UndefinedKey异常。
4. 异常捕获与类型自动转换
KVC 绕过控制的另一个“黑科技”是自动装箱/拆箱:
- 当你尝试通过 KVC 给一个
int类型的私有变量设值时,你可以传递一个NSNumber。 - KVC 内部会解析其类型编码(Type Encoding),自动从
NSNumber中提取出int值再写入内存。这种灵活性是普通的指针操作不具备的。
总结对照
| 访问方式 | 编译器约束 (private/readonly) | 查找路径 | 是否能绕过 |
|---|---|---|---|
| 点语法 / 消息发送 | 强制生效 (编译报错) | 仅限公开方法/属性 | ❌ 无法绕过 |
KVC (setValue:) | 不生效 (运行时查找) | 方法 成员变量 | ✅ 直接操作内存偏移 |