在 Objective-C 的底层实现中,SideTable 是管理内存安全的核心仓库。它并不是一张简单的表,而是一个复杂的 多级嵌套数据结构,专门用来存储引用计数和弱引用信息。
我们可以通过“三层嵌套”的逻辑来剥开它的本质:
第一层:SideTables (全局散列表)
为了避免在多线程环境下,全球的对象共用一把锁导致性能瓶颈(锁竞争),系统并没有只创建一个巨大的表,而是维护了一个 SideTables 数组。
- 结构:一个固定长度的哈希数组(在不同架构下长度不同,如 8 或 64)。
- 分配逻辑:对象通过其内存地址进行哈希运算,映射到其中某一个
SideTable上。 - 作用:将锁的粒度拆分。操作对象 A 的引用计数时,只会锁住对象 A 所在的那个
SideTable,而不影响其他桶里的对象。
第二层:SideTable (具体的结构体)
当你定位到某一个 SideTable 时,你会发现它内部包含三个关键成员:
C++
struct SideTable {
spinlock_t slock; // 自旋锁,保证操作的线程安全
RefcountMap refcnts; // 引用计数表(哈希表)
weak_table_t weak_table; // 弱引用表(核心所在)
};
slock:确保在修改该表内的引用计数或弱引用关系时,其他线程无法同时写入。refcnts:当isa指针里的extra_rc位溢出时,多出来的引用计数就会存到这个哈希表里。
第三层:weak_table_t (弱引用的核心)
这是你问题的核心。weak_table 里面存储的是 对象地址 与 弱引用指针地址 的映射。
在 weak_table_t 内部,又包含了一个 weak_entries 数组。每一个 weak_entry_t 对应一个对象:
-
Key (referent) :被引用对象的 内存地址。
-
Value (referring_pointers) :一个 数组,里面存的是所有指向该对象的
__weak指针的地址。- 注:当弱引用较少时(< 4个),它直接存放在结构体内部的静态数组里;当弱引用变多时,会动态开辟哈希数组存储。
总结:当一个对象销毁时发生了什么?
- 对象调用
dealloc。 - Runtime 找到该对象对应的
SideTable。 - 在
weak_table中,以该 对象地址 为 Key,找到对应的weak_entry_t。 - 取出该 Entry 里的 所有弱引用指针地址。
- 遍历这些地址,将其内容清空:
*ptr = nil。 - 将该 Entry 从
weak_table中移除。
💡 一个有趣的细节:为什么不用更简单的数据结构?
苹果之所以设计这么复杂的嵌套(SideTables -> SideTable -> weak_table -> weak_entry),是为了在 内存开销 和 并发性能 之间取得极致平衡:
- 分片锁:降低了多核 CPU 下的竞争。
- 哈希查找:保证了即便有数百万个对象,寻找某个对象的弱引用表也是 级别的复杂度。