在 ARC(Automatic Reference Counting)环境下,retain 和 release 看起来只是简单的加减计数,但由于它们必须是线程安全的,其实内部逻辑非常复杂,涉及到多层级的锁和原子操作。
在 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/strex或CAS(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 Table | os_unfair_lock | 高(仅在 dealloc 或弱引用赋值时) |
对您分析卡顿的启示
回到您上传的 slowlog_0:
日志显示卡顿发生在 GTCModel 进行模型转换时。如果该模型对象非常复杂,包含大量的属性赋值和对象释放:
- SideTable 竞争:如果这些对象地址哈希冲突,主线程在执行
release时可能会被其他后台线程的retain锁住。 - Weak 清除开销:如果模型中包含大量弱引用,
dealloc时的弱引用表锁竞争也会在老设备(iPhone 6S Plus)上造成明显的毫秒级阻塞。
这就是为什么在高性能场景下,我们提倡减少不必要的对象频繁创建与销毁,因为 ARC 背后的每一行 retain/release 都可能隐藏着一次锁竞争。