4-26.【OC】【锁】ARC 的 retain/release 路径中哪些地方涉及锁?

2 阅读3分钟

在 ARC(Automatic Reference Counting)环境下,retainrelease 看起来只是简单的加减计数,但由于它们必须是线程安全的,其实内部逻辑非常复杂,涉及到多层级的锁和原子操作。

在 Objective-C Runtime(libobjc)源码中,retain/release 的核心路径主要在以下三个地方涉及锁或同步机制:

1. SideTable 锁(全局散列表锁)

这是最关键的地方。为了节省内存,苹果并不会为每个对象都分配一个互斥锁。

  • 机制:Runtime 维护了一个全局的 SideTables(),它是一个哈希表,内部存储了多个 SideTable 结构体。每个 SideTable 包含一个 RefcountMap(引用计数表)和一把 spinlock_t(自旋锁,现代版本已改为 os_unfair_lock

  • 涉及场景

    • 当对象没有使用 Tagged Pointer 优化。
    • 且对象的引用计数溢出了其 isa 指针中存储的内联计数位(extra_rc)。
    • 此时,多出来的计数会存入 SideTable。每次 retain/release 操作这个外部表时,都必须先锁定该 SideTable
  • 性能风险:由于多个对象会通过地址哈希映射到同一个 SideTable,如果多个线程频繁操作不同的对象,但这些对象恰好落在了同一个 SideTable 槽位,就会产生锁竞争

2. isa 指针的原子操作(Atomic CAS)

在 64 位系统下,苹果引入了 Non-pointer isa。对象的引用计数优先存储在 isa 指针的某些位中(extra_rc)。

  • 涉及机制:虽然这里没有传统意义上的“锁”(如 Mutex),但它使用了 CPU 级别的 ldrex/strexCAS (Compare and Swap) 原子指令
  • 涉及场景:当 retain 增加 extra_rc 时,为了防止多线程同时修改 isa 导致数据损坏,必须使用原子操作。
  • 特性:这是一种无锁编程(Lock-free)技术,但在硬件层面,它依然会导致总线锁或缓存一致性协议的同步,这可以看作是一种更底层的微型锁。

3. Weak 表锁(Weak Table Lock)

虽然 retain 不直接涉及弱引用表,但 release 的终点——对象销毁(dealloc) 路径中深度涉及此锁。

  • 机制:在 SideTable 中还包含一个 weak_table_t
  • 涉及场景:当对象执行 release 导致引用计数归零并触发 dealloc 时,Runtime 必须锁定全局弱引用表,遍历并清除所有指向该对象的 __weak 指针,并将它们置为 nil
  • 特性:这是一把全局性质的锁(针对所属的 SideTable),在高并发销毁大量弱引用对象时,性能开销极大。

总结:Retain/Release 的锁路径

存储位置同步机制性能开销
Tagged Pointer无(直接操作值)极低(无锁)
isa (extra_rc)Atomic CAS (原子指令)
SideTable (RefcountMap)os_unfair_lock / Spinlock中(存在哈希冲突风险)
Weak Tableos_unfair_lock高(仅在 dealloc 或弱引用赋值时)

对您分析卡顿的启示

回到您上传的 slowlog_0

日志显示卡顿发生在 GTCModel 进行模型转换时。如果该模型对象非常复杂,包含大量的属性赋值和对象释放:

  1. SideTable 竞争:如果这些对象地址哈希冲突,主线程在执行 release 时可能会被其他后台线程的 retain 锁住。
  2. Weak 清除开销:如果模型中包含大量弱引用,dealloc 时的弱引用表锁竞争也会在老设备(iPhone 6S Plus)上造成明显的毫秒级阻塞。

这就是为什么在高性能场景下,我们提倡减少不必要的对象频繁创建与销毁,因为 ARC 背后的每一行 retain/release 都可能隐藏着一次锁竞争。