6-7.【OC】【KVC/KVO】KVC 在多线程下可能导致哪些问题?

4 阅读3分钟

KVC 并不是线程安全的。因为它底层涉及大量的 Runtime 查找、内存偏移计算以及自动装箱/拆箱,在多线程环境下,这些动态特性会带来几种非常隐蔽且致命的风险。

以下是 KVC 在多线程下的典型问题:


1. setter/getter 的原子性失效

即使你将属性声明为 atomic,通过 KVC 访问也无法保证绝对安全。

  • 原因: atomic 只能保证由编译器自动生成的 setter/getter 方法是同步的。
  • 风险: 当 KVC 绕过方法直接访问 Ivar(成员变量) 时,它完全脱离了 atomic 的保护锁。如果线程 A 正在通过 KVC 写入一个较大的结构体(如 CGRect),而线程 B 同时读取,可能会读到只有一半被修改的“中间态”脏数据。

2. 集合代理对象的竞态条件

当你使用 mutableArrayValueForKey: 获取代理数组时,风险会成倍增加。

  • 原因: KVC 返回的代理对象(如 NSKeyValueFastMutableArray)内部并没有内置线程锁。
  • 风险: 如果两个线程同时持有同一个 Key 的代理对象并执行 addObject:,由于它们最终可能都会操作同一个底层的 _ivar 数组,这会导致 多线程同时修改非线程安全集合,直接引发 EXC_BAD_ACCESS 崩溃。

3. 访问期间的对象销毁(野指针)

KVC 在查找路径(KeyPath)时涉及多个对象的连续访问。

  • 例子: [self valueForKeyPath:@"department.manager.name"]
  • 风险: 在 KVC 逐级解析路径的过程中,如果另一个线程突然将 departmentmanager 置为 nil 甚至释放了该对象,KVC 内部持有的临时指针就会变成野指针。由于 KVC 内部逻辑较长,这种“中间环节断裂”极易导致偶发性的随机崩溃。

4. 类型转换与内存破坏

KVC 在处理非对象类型(如 int, struct)时,会进行拆箱写入。

  • 风险: 如果线程 A 正在利用 KVC 解析类型编码(Type Encoding)并准备写入内存,而线程 B 正在动态修改该类的结构(虽然罕见,但在某些动态 AOP 框架中可能发生),或者多个线程同时对同一个标量 Ivar 进行装箱/拆箱写入,可能会导致内存对齐错误或写入的值被截断。

KVC 线程风险总结对照

维度风险点结果
原子性绕过 atomic 直接操作 Ivar数据不一致(脏读/脏写)
集合操作多个 mutableArrayValue 同时写数组崩溃 (Bad Access)
路径解析路径中间节点在解析时被释放野指针崩溃
性能全局 SideTable 锁竞争(涉及 weak 时)线程卡顿 / 性能下降

⚠️ 防御建议

如果你必须在多线程环境下使用 KVC,建议采取以下措施:

  1. 加锁同步: 在 KVC 调用外层手动加锁(如 @synchronizeddispatch_semaphore)。
  2. 先取值后操作: 尽量先将对象取出来存为局部强引用,再进行操作,避免直接使用复杂的 KeyPath
  3. 避免在多线程中使用集合代理: 对于数组操作,手动使用 dispatch_async 到特定串行队列,而不是依赖 mutableArrayValueForKey: