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;
说明三点:
- _person 是一个普通指针变量
- weak 修饰的是这个指针变量的行为
- 并不是 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 → 野指针 |
| weak | Runtime 扫表并置 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 的真实实现