iOS 内存管理:引用计数存储与 Weak 实现

1. 引用计数的存储策略

iOS 并不只是用一个简单的整数变量存储引用计数,而是分了三种情况:

  1. Tagged Pointer

    • 部分体积很小、可编码进指针位模式的值对象可能使用 Tagged Pointer,例如某些 NSNumberNSDate、短字符串。是否采用由系统实现决定,业务代码不应依赖具体类型一定是 Tagged Pointer。
    • 指针位本身携带类型和值,不对应一块普通 Objective-C 堆对象内存,因此不走常规的对象 malloc/free 和引用计数流程。它也不能简单理解成“分配在栈上或代码段”,因为这里根本没有一块与普通对象等价的独立实例内存。
  2. isa.extra_rc

    • 大多数普通对象。
    • 在支持 nonpointer isa 的平台上,较小的附加引用计数会编码在 isaextra_rc 位域中。
    • 位宽属于 Runtime 的平台与版本实现细节,不应在面试或业务文档里固定写成 19 位。阅读特定版本源码时,应以该架构下 isa_t 的定义为准。
  3. SideTables

    • 当内联空间不足时,部分引用计数信息会转移到 SideTable,并通过 has_sidetable_rc 等状态配合管理。源码会采用批量借位、回填等策略减少频繁加锁,不能概括成永远严格“一半留在 isa、一半放进表”。

2. SideTables 结构

SideTables 可以理解为按对象地址散列的一组 SideTable。分片的目的是让不同对象尽量落入不同锁域,降低全局单锁竞争。分片数量与哈希实现属于 Runtime 版本和平台细节,不要写死为 64。

struct SideTable {
    spinlock_t slock;        // 自旋锁
    RefcountMap refcnts;     // 引用计数表 (Map<Object*, count>)
    weak_table_t weak_table; // Weak 引用表
};

3. Weak 引用底层实现 (Deep Dive)

Weak 指针具有 “对象销毁时自动置 nil” 的特性。

3.1 数据结构

  • weak_table_t:每个 SideTable 内部维护的 Weak 表,并非整个进程只有一张独立的全局表。

  • weak_entry_t:某个对象对应的所有 weak 指针集合(类似一个数组)。

3.2 初始化 (storeWeak)

__weak id obj = target 执行时:

  • 调用 objc_storeWeak(&obj, target)。

  • 从 SideTables 中找到 target 对应的 SideTable。

  • 取出 weak_table。

  • 以 target 为 key,查找或创建 weak_entry_t。

  • 将 &obj (指针的地址) 添加到 entry 数组中。

3.3 销毁流程 (dealloc)

当对象引用计数为 0 时,触发 dealloc -> _objc_rootDealloc:

  • 检查 isa.weakly_referenced。如果为 false,直接释放内存。

  • 如果为 true,调用 object_dispose -> objc_destructInstance -> clearDeallocating。

  • clearDeallocating 核心逻辑

    • 获取对应的 SideTable 加锁。

    • 在 weak_table 中找到该对象的 entry。

    • 遍历 entry 中登记的所有 weak 存储地址,将这些位置写为 nil

    • 从表中移除 entry。

    • 随后对象实例内存才进入最终释放阶段。

4. 读取 Weak 为什么不能只是普通指针读取

弱引用的读取还要解决并发问题。设想线程 A 正在读取 weak,线程 B 同时释放目标对象。如果只是读取一个裸地址,A 可能拿到一个刚刚被释放的悬空指针。

因此 ARC 下读取 weak 通常会走 objc_loadWeakRetained 一类 Runtime 路径:在相应同步保护下确认对象仍可存活,并临时取得一个强引用;表达式结束后再释放这个临时引用。objc_loadWeak 则可在此基础上返回 autoreleased 结果。具体调用序列会受编译器优化影响,但“读取需要与销毁同步”才是重点。

这也解释了两个实践结论:

  • weak 不是零成本语法糖。频繁读写 weak 会涉及哈希查找、锁和 Runtime 调用。
  • 需要在一段逻辑里稳定使用 weak 对象时,先提升为局部强引用更清晰:id strongObject = weakObject;,随后围绕 strongObject 工作。

5. 常见误区

  • 循环引用不是 GC 语言里的“环就回收不了”:Objective-C ARC 依靠引用计数,只要强引用环始终存在,计数就不会归零,需要主动打断其中至少一条强边。
  • weak 不保证回调一定发生:它只保证对象销毁后存储位置清零,不能替代生命周期设计、取消订阅或线程同步。
  • 不要依赖私有位域做业务判断extra_rchas_sidetable_rcweakly_referenced 适合帮助理解实现,但 Apple 没有承诺它们跨系统版本保持不变。