iOS 知识点 - ARC / 引用计数 / SideTable / weak 表
2025年12月15日 16:50
前瞻
本文在理解 isa 指针和 iOS 内存模型的基础上,深入讲解 ARC 下对象的引用计数机制、SideTable、weak 表以及 autorelease 的工作原理。
前置知识:
- Runtime 篇: juejin.cn/post/757172…
- iOS「操作系统 & 内存模型 & 文件系统」篇: juejin.cn/post/758169…
一、对象内存布局与引用计数
1. 对象结构(64 位系统)
struct objc_object {
isa_t isa;
};
-
isa本质是一个「位域」结构,它不仅存储类信息,也存储对象状态、部分引用计数等信息。
2. isa 位域拆解
| 位 | 作用 |
|---|---|
| nonpointer | 是否为非指针 isa(现代 runtime) |
| has_assoc | 是否存在关联对象 |
| has_cxx_dtor | 是否需要调用 C++ 析构函数 |
| shiftcls | 类指针 |
| magic | 校验 magic number |
| weakly_referenced | 是否有 weak 指针引用 |
| deallocating | 对象是否正在释放 |
| has_sidetable_rc | 是否存在溢出的 SideTable RC |
| extra_rc | 内嵌引用计数(通常 19 位) |
核心思想:
- 小对象的 引用计数 尽可能保存在 isa.extra_rc 中,提升访问效率;(快路径)
- 当 引用计数 溢出或者存在 weak/assoc 时才会使用
SideTable。(慢路径)
二、SideTable(由 runtime 维护的全局 哈希分片表)
1. SideTable 结构
struct SideTable {
spinlock_t slock; // 全局自旋锁,保证 SideTable 的线程安全
RefcountMap refcnts; // 溢出的引用计数
weak_table_t weak_table; // 所有的弱引用
assoc_map_t assoc_map; // 关联对象
};
-
RefcountMap: 对象指针 -> 溢出的引用计数typedef objc::DenseMap<DisguisedPtr<objc_object>, size_t> RefcountMap; -
weak_table_t: 对象指针 -> 所有 weak 指针地址 -
assoc_map_t: 对象指针 -> 关联对象字典
2. SideTable 使用条件
| 情况 | 使用原因 |
|---|---|
| rc 溢出 | isa 内部位数有限,额外部分存 SideTable |
| 存在 weak 引用 | weak 指针需要统一管理 |
| 存在 关联对象 | 关联对象信息存 SideTable |
3. 哈希策略和性能优化
-
计算:对象指针通过 hash 函数计算桶;
-
哈希冲突:开放定址法、拉链法 来解决;
-
性能与安全: “striped hash 分片” 减少线程竞争,“spinlock” 保证线程安全。(所以
weak引用 访问性能低于isa内部引用) -
注意点:
- 小对象引用计数尽量保存在
isa内部; - weak/assoc 访问会产生 锁开销;
-
autorelease对象频繁创建也可能影响性能。
- 小对象引用计数尽量保存在
4. 哈希分片表
在 objc4 源码中:
static StripedMap<SideTable> SideTables;
-
SideTables是一个全局静态变量,类型是StripedMap<SideTable>。 - 也就是说,
SideTables是一个由多条SideTable组成的全局哈希分片表。
为什么要 “分片存储” ?
- 如果只有一个全局
SideTable,全局加锁会导致所有对象的 “引用计数”、weak、assoc 都竞争同一把锁,性能很差; - 于是 Apple 使用了
StripedMap———— 把全局 SideTable 分成多片,每个分片独立加锁。
三、retain/release 逻辑
1. retain
id objc_retain(id obj) {
if (!obj) return nil;
if (obj->isa.nonpointer) {
// 1. 先尝试在 isa 中增加计数
if (!tryRetainInIsa(obj)) {
// 2. 如果溢出,则加锁访问 SideTable
retainInSideTable(obj);
}
} else {
// 指针 isa,直接在 SideTable 中操作
retainInSideTable(obj);
}
return obj;
}
-
快路径:
isa.extra_rc通过 CAS(Compare-And-Swap)无锁增加。 -
慢路径:
SideTable加锁保证线程安全。
CAS(Compare-And-Swap)
CAS 是 CPU 提供的、由寄存器+总线锁保证的 原子级 “比较与交换” 指令,能在多线程下无锁实现数据同步。
如果内存地址 addr 当前的值等于预期值,就把它改成 new_value,否则什么也不做。
这个过程是不可分割的,不会被线程切换打断的。
- 要么完全成功(值被更新)
- 要么完全失败(值没变)
2. release
void objc_release(id obj) {
if (!obj) return;
if (obj->isa.nonpointer) {
// 1. 减少 isa 内部计数
if (!decrementIsaCount(obj)) {
// 2. isa 内部计数 = 0,需要访问 SideTable
if (!decrementSideTableCount(obj)) {
// 3. SideTable 引用计数 = 0,调用 dealloc。
dealloc(obj);
}
}
} else {
// 指针 isa,直接减少 SideTable 引用计数,为 0 调用 dealloc。
if (!decrementSideTableCount(obj)) {
dealloc(obj);
}
}
}
- 线程安全通过 “spinlock + atomic” 实现
- dealloc 前会处理 weak/assoc 等附加信息:
四、weak 指针实现细节:weak 表
1. weak_table 结构
struct weak_table_t {
weak_entry_t **buckets; // 哈希桶数组
size_t num_buckets;
spinlock_t slock;
};
每个对象的 weak_entry 记录指向他的所有的 weak 地址:
struct weak_entry_t {
DisguisedPtr<objc_object> referent; // 被引用对象
weak_referrer_t *referrers; // 所有 weak 指针地址
};
2. weak 指针注册
执行 __weak id w = obj 时:
- 检查
obj是否为空; - 在
weak_table中找到或创建weak_entry; - 将 w 的地址注册到
entry.referrers中; - 标记
isa.weakly_referenced= 1。
通过 CAS 保证 weak 指针注册过程的线程安全。
3. 对象释放与 weak 清空
-
objc_release检测到 rc == 0; - 检查
isa.weakly_referenced:- 遍历
weak_entry.referrers,将所有 weak 地址置空; - 删除
weak_entry
- 遍历
- 执行 dealloc。
通过 “
SideTable.spinlock” 和 “atomic_store对 weak 指针原子写入” 保证线程安全。
五、autorelease 与 ARC
-
autorelease将对象放入当前线程的AutoreleasePoolPage栈中延迟释放; - RunLoop 结束或 pool drain 时,再集体释放
autorelease对象。
六、对象生命周期总结
对象创建
│
▼
retain/release -> isa.extra_rc
├─> 溢出 -> SideTable.refcnts
└─> 有 weak -> SideTable.weak_table
└─> weak 指针地址注册
└─> 有 assoc -> SideTable.assoc_map
│
rc == 0
│
├─> weak 清空
├─> 关联对象清空
└─> 调用 dealloc