weak 引用表的同步机制是 Objective-C Runtime 中设计最精巧的部分之一,它必须在“高频访问”、“内存节省”和“多线程安全”之间取得平衡。
其核心设计可以概括为:多级哈希表结构 + 细粒度自旋锁(现为 os_unfair_lock)+ 分段加锁策略。
1. 核心架构:SideTables 锁池
为了避免全局一把锁导致的严重竞争,Runtime 并没有设计一个单一的弱引用表,而是设计了一个锁池——SideTables()。
-
分段锁(Striped Map) :系统内部维护了一个长为 64(或 8)的数组,每个数组元素都是一个
SideTable结构体。 -
结构定义:
C++
struct SideTable { spinlock_t slock; // 对应的细粒度锁 RefcountMap refcnts; // 引用计数哈希表 weak_table_t weak_table; // 弱引用哈希表 }; -
映射机制:当你操作一个对象的
weak指针时,系统通过hash(对象地址) % 64找到对应的SideTable。这意味着只有地址哈希冲突的对象才会竞争同一把锁。
2. 弱引用表的内部结构 (weak_table_t)
在具体的 SideTable 内部,同步机制进一步延伸到对 weak_table_t 的操作:
- weak_table_t:存储了所有指向某些对象的弱引用条目。
- weak_entry_t:这是真正的存储单元,记录了某个对象地址及其对应的所有
__weak指针地址。 - 同步逻辑:所有的增(
objc_initWeak)、删(dealloc)、改、查操作,都必须在获取了所属SideTable的slock之后才能进行。
3. 关键场景的同步流程
A. 初始化弱引用 (objc_initWeak)
- 计算对象地址的哈希值,定位到具体的
SideTable。 - 加锁:
lock()该 table。 - 在
weak_table中查找/创建该对象的weak_entry_t。 - 将新的弱引用指针地址插入到 entry 中。
- 解锁:
unlock()。
B. 对象销毁时的清空 (weak_clear_no_lock)
这是最耗时的同步操作。当对象引用计数归零执行 dealloc 时:
- 进入
objc_destructInstance。 - 定位并锁定对应的
SideTable。 - 遍历清除:取出该对象对应的
weak_entry_t,遍历其中记录的所有__weak指针地址,将它们全部置为nil。 - 从
weak_table中移除该 entry。 - 释放锁。
4. 为什么这样设计?(设计优缺点)
-
优点:
- 细粒度化:通过 64 个桶位分散了锁竞争。不同对象(只要哈希不冲突)的弱引用操作可以并行执行。
- 内存效率:只有真正有弱引用的对象才会占用
SideTable的条目。
-
缺陷与风险:
- 哈希冲突导致卡顿:如果多个高频操作的对象不幸落入同一个
SideTable,即使它们逻辑上无关,也会互相阻塞。 - dealloc 瓶颈:如果一个对象被成千上万个
__weak指针引用,销毁时的锁持有时间会很长,阻塞其他尝试访问该SideTable的线程。
- 哈希冲突导致卡顿:如果多个高频操作的对象不幸落入同一个
关联到您的 slowlog_0 堆栈
在您的卡顿日志中,主线程阻塞在 GTCModel 的处理过程中。GTCModel 往往涉及大量的对象创建和属性赋值。如果模型中大量使用了 weak 属性,或者在解析过程中频繁触发了旧对象的销毁:
- 由于 iPhone 6S Plus 性能有限,大量的
weak表查询和加锁解锁操作会累积成明显的毫秒级延迟。 - 如果此时后台线程也在处理网络回调(如日志中看到的
CEBNetworkManager),并伴随对象释放,那么主线程就可能因为 SideTable 锁竞争 而被挂起。
优化建议: 在高性能要求的模型解析层,尽量减少 __weak 属性的使用,改用 nonatomic, assign(手动管理生命周期)或确保对象结构简单化,以减轻 Runtime 弱引用表的同步压力。