iOS 内存管理:引用计数存储与 Weak 实现
1. 引用计数的存储策略
iOS 并不只是用一个简单的整数变量存储引用计数,而是分了三种情况:
-
Tagged Pointer:
- 部分体积很小、可编码进指针位模式的值对象可能使用 Tagged Pointer,例如某些
NSNumber、NSDate、短字符串。是否采用由系统实现决定,业务代码不应依赖具体类型一定是 Tagged Pointer。 - 指针位本身携带类型和值,不对应一块普通 Objective-C 堆对象内存,因此不走常规的对象
malloc/free和引用计数流程。它也不能简单理解成“分配在栈上或代码段”,因为这里根本没有一块与普通对象等价的独立实例内存。
- 部分体积很小、可编码进指针位模式的值对象可能使用 Tagged Pointer,例如某些
-
isa.extra_rc:
- 大多数普通对象。
- 在支持 nonpointer isa 的平台上,较小的附加引用计数会编码在
isa的extra_rc位域中。 - 位宽属于 Runtime 的平台与版本实现细节,不应在面试或业务文档里固定写成 19 位。阅读特定版本源码时,应以该架构下
isa_t的定义为准。
-
SideTables:
- 当内联空间不足时,部分引用计数信息会转移到 SideTable,并通过
has_sidetable_rc等状态配合管理。源码会采用批量借位、回填等策略减少频繁加锁,不能概括成永远严格“一半留在 isa、一半放进表”。
- 当内联空间不足时,部分引用计数信息会转移到 SideTable,并通过
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_rc、has_sidetable_rc、weakly_referenced适合帮助理解实现,但 Apple 没有承诺它们跨系统版本保持不变。