普通视图

发现新文章,点击刷新页面。
昨天 — 2025年12月15日掘金 iOS

iOS 知识点 - ARC / 引用计数 / SideTable / weak 表

作者 齐生1
2025年12月15日 16:50

前瞻

本文在理解 isa 指针和 iOS 内存模型的基础上,深入讲解 ARC 下对象的引用计数机制、SideTable、weak 表以及 autorelease 的工作原理。

前置知识:


一、对象内存布局与引用计数

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 时:

  1. 检查 obj 是否为空;
  2. weak_table 中找到或创建 weak_entry
  3. 将 w 的地址注册到 entry.referrers 中;
  4. 标记 isa.weakly_referenced = 1。

通过 CAS 保证 weak 指针注册过程的线程安全。

3. 对象释放与 weak 清空

  1. objc_release 检测到 rc == 0;
  2. 检查 isa.weakly_referenced:
    • 遍历 weak_entry.referrers,将所有 weak 地址置空;
    • 删除 weak_entry
  3. 执行 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
❌
❌