4-27.【OC】【锁】weak 引用表的同步机制是怎样设计的?

3 阅读3分钟

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)、改、查操作,都必须在获取了所属 SideTableslock 之后才能进行。

3. 关键场景的同步流程

A. 初始化弱引用 (objc_initWeak)

  1. 计算对象地址的哈希值,定位到具体的 SideTable
  2. 加锁lock() 该 table。
  3. weak_table 中查找/创建该对象的 weak_entry_t
  4. 将新的弱引用指针地址插入到 entry 中。
  5. 解锁unlock()

B. 对象销毁时的清空 (weak_clear_no_lock)

这是最耗时的同步操作。当对象引用计数归零执行 dealloc 时:

  1. 进入 objc_destructInstance
  2. 定位并锁定对应的 SideTable
  3. 遍历清除:取出该对象对应的 weak_entry_t,遍历其中记录的所有 __weak 指针地址,将它们全部置为 nil
  4. weak_table 中移除该 entry。
  5. 释放锁。

4. 为什么这样设计?(设计优缺点)

  • 优点

    1. 细粒度化:通过 64 个桶位分散了锁竞争。不同对象(只要哈希不冲突)的弱引用操作可以并行执行。
    2. 内存效率:只有真正有弱引用的对象才会占用 SideTable 的条目。
  • 缺陷与风险

    1. 哈希冲突导致卡顿:如果多个高频操作的对象不幸落入同一个 SideTable,即使它们逻辑上无关,也会互相阻塞。
    2. dealloc 瓶颈:如果一个对象被成千上万个 __weak 指针引用,销毁时的锁持有时间会很长,阻塞其他尝试访问该 SideTable 的线程。

关联到您的 slowlog_0 堆栈

在您的卡顿日志中,主线程阻塞在 GTCModel 的处理过程中。GTCModel 往往涉及大量的对象创建和属性赋值。如果模型中大量使用了 weak 属性,或者在解析过程中频繁触发了旧对象的销毁:

  • 由于 iPhone 6S Plus 性能有限,大量的 weak 表查询和加锁解锁操作会累积成明显的毫秒级延迟。
  • 如果此时后台线程也在处理网络回调(如日志中看到的 CEBNetworkManager),并伴随对象释放,那么主线程就可能因为 SideTable 锁竞争 而被挂起。

优化建议: 在高性能要求的模型解析层,尽量减少 __weak 属性的使用,改用 nonatomic, assign(手动管理生命周期)或确保对象结构简单化,以减轻 Runtime 弱引用表的同步压力。