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 逐级解析路径的过程中,如果另一个线程突然将
department或manager置为nil甚至释放了该对象,KVC 内部持有的临时指针就会变成野指针。由于 KVC 内部逻辑较长,这种“中间环节断裂”极易导致偶发性的随机崩溃。
4. 类型转换与内存破坏
KVC 在处理非对象类型(如 int, struct)时,会进行拆箱写入。
- 风险: 如果线程 A 正在利用 KVC 解析类型编码(Type Encoding)并准备写入内存,而线程 B 正在动态修改该类的结构(虽然罕见,但在某些动态 AOP 框架中可能发生),或者多个线程同时对同一个标量 Ivar 进行装箱/拆箱写入,可能会导致内存对齐错误或写入的值被截断。
KVC 线程风险总结对照
| 维度 | 风险点 | 结果 |
|---|---|---|
| 原子性 | 绕过 atomic 直接操作 Ivar | 数据不一致(脏读/脏写) |
| 集合操作 | 多个 mutableArrayValue 同时写 | 数组崩溃 (Bad Access) |
| 路径解析 | 路径中间节点在解析时被释放 | 野指针崩溃 |
| 性能 | 全局 SideTable 锁竞争(涉及 weak 时) | 线程卡顿 / 性能下降 |
⚠️ 防御建议
如果你必须在多线程环境下使用 KVC,建议采取以下措施:
- 加锁同步: 在 KVC 调用外层手动加锁(如
@synchronized或dispatch_semaphore)。 - 先取值后操作: 尽量先将对象取出来存为局部强引用,再进行操作,避免直接使用复杂的
KeyPath。 - 避免在多线程中使用集合代理: 对于数组操作,手动使用
dispatch_async到特定串行队列,而不是依赖mutableArrayValueForKey:。