6-2.【OC】【KVC/KVO】KVC 是如何绕过访问控制(private / @protected / readonly)来访问对象的?

3 阅读2分钟

KVC(Key-Value Coding)之所以能“突破”访问限制,是因为它本质上是 Objective-C Runtime(运行时) 的一层高级封装。

编译器在编译阶段施加的 private@protectedreadonly 约束,在 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,编译器会做两件事:

  1. 不生成 set 方法。
  2. 阻止代码调用 obj.property = value

然而,KVC 的设值逻辑并不是调用点语法(Dot Notation),而是调用 setValue:forKey:

  • KVC 的搜索算法(Setter \rightarrow 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:)不生效 (运行时查找)方法 \rightarrow 成员变量✅ 直接操作内存偏移