ARC 原理与 weak 底层实现(Side Table 深度解析)

7 阅读3分钟

ARC 原理与 weak 底层实现(Side Table 深度解析)

面向:有一定 iOS / Runtime 基础的开发者

目标:真正搞清楚 weak 到底 weak 了谁、SideTable 里存的是什么、为什么能自动置 nil


一、先给结论(非常重要)

weak 不是修饰对象,而是修饰“指针变量”

SideTable 记录的是:某个对象,被哪些 weak 指针地址指向

换句话说:

  • ❌ 不是「A weak 引用 B」
  • ❌ 不是「对象记住了谁 weak 它」
  • ✅ 是「Runtime 记住:哪些内存地址(weak 指针)指向了这个对象」

二、从一行代码开始

@property (nonatomic, weak) Person *person;

编译后本质是:

Person *__weak _person;

说明三点:

  1. _person 是一个普通指针变量
  2. weak 修饰的是这个指针变量的行为
  3. 并不是 Person 对象“变成了 weak”

三、明确三个核心角色

Person *p = [[Person alloc] init];
self.person = p; // weak

此时内存中存在三样东西:

角色含义
Person 对象真正的 OC 实例
strong 指针拥有对象(如 p)
weak 指针不拥有对象(如 self->_person)

weak 的对象不是 Person,而是 _person 这个指针变量。


四、objc_storeWeak 到底做了什么?

self.person = p;

编译后:

objc_storeWeak(&self->_person, p);

注意这里传入的两个参数:

  • &self->_person 👉 weak 指针的地址

  • p 👉 对象地址

Runtime 的真实意图:

登记:对象 p,被这个 weak 指针地址弱引用了


五、SideTable / weak_table 的真实逻辑结构

1️⃣ SideTable(简化)

struct SideTable {
    spinlock_t lock;
    RefcountMap refcnts;     // 强引用计数
    weak_table_t weak_table; // 弱引用表
};

2️⃣ weak_table_t

struct weak_table_t {
    weak_entry_t *weak_entries;
};

3️⃣ weak_entry_t(重点)

struct weak_entry_t {
    DisguisedPtr<objc_object> referent; // 被 weak 的对象
    weak_referrer_t *referrers;         // weak 指针地址数组
};

六、SideTable 中真正存的是什么?

用一张逻辑图表示:

SideTable
└── weak_table
    └── weak_entry
        ├── referent = Person 对象
        └── referrers = [
              &self->_person,
              &vc->_delegate,
              &cell->_model
          ]

关键点(一定要记住):

  • weak_table 的 key 是对象
  • value 是 所有指向它的 weak 指针地址

七、谁被 weak?谁被记录?

以:

self.person = p;

为例:

问题答案
谁被 weak?_person 这个指针变量
谁被引用?Person 对象
SideTable 记录什么?Person → weak 指针地址列表

八、对象释放时为什么能自动置 nil?

当 Person 的引用计数降为 0:

objc_destroyWeak(obj);

Runtime 的逻辑流程:

1. 找到 obj 对应的 weak_entry
2. 遍历 referrers(weak 指针地址)
3. 对每个地址执行:
      *(Person **)referrer = nil
4. 移除 weak_entry

⚠️ Runtime 完全不知道变量名,只操作内存地址。


九、用内存视角完整走一遍

1️⃣ 内存布局

0x1000  Person 对象

0x2000  p (strong)        → 0x1000
0x3000  self->_person     → 0x1000

2️⃣ weak_table

key: 0x1000
value: [0x3000]

3️⃣ Person 释放

free(0x1000)
*(0x3000) = nil

最终:

self.person == nil

十、为什么 weak 不会产生野指针?

修饰符行为
assign不 retain、不置 nil → 野指针
weakRuntime 扫表并置 nil

weak 的安全性来自 Runtime 的集中清理机制


十一、为什么 weak_table 以“对象”为中心?

因为:

对象释放是一个确定事件

以对象为 key:

  • 对象销毁 → 一次性清理所有 weak 指针
  • 性能可控
  • 逻辑集中

十二、常见误解澄清

❌ A weak 引用 B

✅ A 的某个指针 weak 指向 B

❌ 对象知道谁 weak 它

✅ Runtime 知道,对象本身不知道

❌ weak 是对象属性

✅ weak 是指针语义

❌ weak 只是“不 retain”

✅ weak = 不 retain + 注册 weak_table + 自动置 nil


十三、一句话总结

weak 的本质不是弱引用对象,

而是 Runtime 记录“哪些指针弱指向了这个对象”,

并在对象销毁时统一把这些指针置为 nil。


十四、下一步可继续深入

  • block 捕获下 weak_table 的变化过程

  • __unsafe_unretained 与 weak 的实现对比

  • objc-runtime 源码中 weak_entry 的真实实现