iOS复习必看!weak关键字底层原理(Deepseek&豆包)回答整理

一 weak引用

当一个对象被 weak 引用时,Runtime 需要把这个 weak 指针记录到一个全局的数据结构中,以便将来对象销毁时能够找到它并置为 nil。整个过程涉及编译器、Runtime 函数和复杂的数据结构。下面我们详细拆解这个过程。


1. 编译器对 weak 变量的处理

假设我们有如下代码:

objc 复制代码
NSObject *obj0 = [NSObject new];
__weak id obj1 = obj0;

在 ARC 环境下,编译器会将 __weak 变量的赋值操作编译为对 Runtime 函数的调用。具体来说:

  • 对于 __weak 变量的初始化 (首次赋值),编译器会调用 objc_initWeak
  • 对于 __weak 变量的重赋值 (即已经存在一个 weak 变量,再将其指向另一个对象),编译器会调用 objc_storeWeak

这两者最终都会调用核心函数 objc_storeWeak(id *location, id newObj),其中:

  • location 是 weak 指针的地址(例如 &obj1)。
  • newObj 是要指向的对象(即 obj0)。

所以,我们以 objc_storeWeak 为主线,讲解存储过程。


2. 进入 Runtime:objc_storeWeak 的主要流程

objc_storeWeak 的核心逻辑可以简化为以下步骤:

  1. 获取要指向的对象 newObj
  2. 如果有旧值(即这个 weak 指针之前已经指向过某个对象),需要先从 weak 表中移除该指针。
  3. 如果 newObj 不为 nil,则将当前 weak 指针注册到 newObj 的 weak 表中。
  4. 返回 newObj

下面重点说明注册过程,即如何将 weak 指针存储到表中。


3. 存储 weak 指针的核心步骤

3.1 获取 SideTable

Runtime 维护一个全局的 SideTables 哈希表(实际上是一个 StripedMap),它以对象的内存地址为 key,映射到一个 SideTable 结构体。这个 SideTable 包含了引用计数和 weak 表等。

为了操作 obj0 的 weak 表,需要先通过 obj0 的地址找到对应的 SideTable

c 复制代码
SideTable *table = &SideTables[obj0];

这里使用 obj0 的地址进行哈希计算,得到一个索引,从而取出对应的 SideTable

为什么需要 SideTable 主要为了锁分离 ,提高并发性能。不同的对象可能映射到不同的 SideTable,操作不同对象的 weak 引用时可以并行执行。

3.2 获取 weak_table_t

每个 SideTable 内部包含一个 weak_table_t 结构,它是一个独立的哈希表,专门管理所有指向该 SideTable 所管辖对象的 weak 引用。

c 复制代码
weak_table_t &weak_table = table->weak_table;

weak_table_t 的定义大致如下:

c 复制代码
struct weak_table_t {
    weak_entry_t *weak_entries;   // 哈希数组,元素是 weak_entry_t
    size_t    num_entries;        // 当前条目数
    uintptr_t mask;               // 掩码,用于哈希计算
    uintptr_t max_hash_displacement; // 最大偏移量
};

3.3 在 weak_table_t 中查找或创建 weak_entry_t

接下来,使用 对象地址 obj0 作为 key,在 weak_table_t 中查找对应的 weak_entry_t

weak_entry_t 是一个容器,它负责存储所有指向同一个对象的 weak 指针地址(即 location)。

  • 如果找到已有的 weak_entry_t,说明已经有一些 weak 指针指向 obj0,那么直接将当前 weak 指针地址添加到这个 weak_entry_t 中。
  • 如果没找到,说明这是第一个指向 obj0 的 weak 指针,需要创建一个新的 weak_entry_t,并将它插入到 weak_table_t 中。

3.4 weak_entry_t 的结构:如何存储多个 weak 指针

weak_entry_t 的设计目标是高效地存储多个指向同一个对象的 weak 指针。它的简化结构如下:

c 复制代码
struct weak_entry_t {
    DisguisedPtr<objc_object> referent;  // 指向对象,即 obj0
    union {
        struct {
            weak_referrer_t *referrers;   // 动态数组
            uintptr_t        out_of_line : 1; // 标志位,表示使用动态数组
            // ... 其他字段
        };
        struct {
            weak_referrer_t  inline_referrers[4]; // 静态数组,容量为4
        };
    };
};

这里有一个优化:当一个对象只有少量(<=4)weak 引用时,使用静态数组 inline_referrers,避免额外堆分配。当超过4个时,会转换为动态数组 referrers,可以动态扩容。

weak_referrer_t 本质上就是 id *,即 weak 指针的地址(例如 &obj1)。存储的是指针的地址,这样当对象销毁时,Runtime 可以找到这个地址并将其内容置为 nil

3.5 添加 weak 指针到 weak_entry_t

在找到或创建了 weak_entry_t 之后,调用 weak_entry_append 或类似函数,将当前 weak 指针的地址(location)添加到 weak_entry_t 的数组中。

添加时,会根据当前是静态数组还是动态数组,采取不同的插入逻辑。如果是静态数组且未满,直接放入;如果静态数组已满(即已有4个),则触发扩容,将静态数组内容迁移到新分配的动态数组,再加入新元素。

3.6 线程安全

整个过程中,对 SideTable 的操作都在其持有的自旋锁(spinlock_t)保护下进行,确保多个线程同时操作 weak 引用时的安全性。


4. 一个完整的例子

让我们用代码模拟这个过程:

objc 复制代码
NSObject *obj0 = [NSObject new];
__weak id obj1 = obj0;
__weak id obj2 = obj0;
  1. 编译器为 obj1 的赋值生成类似 objc_storeWeak(&obj1, obj0) 的代码。
  2. 进入 objc_storeWeak
    • 获取 obj0SideTable,加锁。
    • weak_table_t 中以 obj0 为 key 查找 weak_entry_t,未找到,创建一个新的 weak_entry_t,其中 referent 指向 obj0,静态数组 inline_referrers 为空。
    • &obj1 添加到 weak_entry_t 的数组中。
    • weak_entry_t 插入 weak_table_t
    • 解锁。
  3. 编译器为 obj2 的赋值生成 objc_storeWeak(&obj2, obj0)
  4. 再次进入 objc_storeWeak
    • 获取 obj0SideTable,加锁。
    • weak_table_t 中以 obj0 为 key 查找 weak_entry_t,这次找到了。
    • &obj2 添加到该 weak_entry_t 的数组中(此时数组内已有 &obj1&obj2)。
    • 解锁。

此时,obj0 的 weak 表中就记录了这两个 weak 指针的地址。


5. 为什么存储的是 weak 指针的地址,而不是指针本身?

当对象 obj0 销毁时,Runtime 会遍历 weak_entry_t 中的数组,对于数组中的每一项(即每个 weak_referrer_t,它是一个 id * 类型),执行 *referrer = nil,将 weak 变量本身置为 nil。如果存储的是指针的值(即 obj1 本身),那么 Runtime 只能知道这个 weak 指针指向了 obj0,却无法修改这个 weak 变量(因为不知道它的内存位置)。存储地址使得 Runtime 能够直接修改该变量的内容,从而实现自动置 nil


6. 小结

对象被 weak 引用后,存储到 weak 表中的过程可以概括为:

  1. 编译器将 weak 赋值转换为 objc_storeWeak 调用。
  2. Runtime 通过对象地址找到对应的 SideTable(分离锁)。
  3. SideTableweak_table_t 中以对象地址为 key 查找或创建 weak_entry_t
  4. weak_entry_t 是一个容器(静态或动态数组),存储所有指向该对象的 weak 指针的地址。
  5. 将当前 weak 指针地址添加到该容器中。

这种设计保证了:

  • 高效查找:通过两层哈希表,快速定位到对象对应的 weak 容器。
  • 并发友好 :通过 SideTable 分离锁,不同对象的 weak 操作可以并行。
  • 内存优化:使用小对象优化(inline array),减少大多数情况下的堆分配。
  • 自动置 nil:存储指针地址,使得对象销毁时可以直接修改 weak 变量的内容。

二 释放weak引用

当一个对象的引用计数降为 0 时,系统会回收该对象。在这个过程中,Runtime 必须完成两件与 weak 相关的重要工作:

  1. 将所有指向该对象的 weak 指针置为 nil,防止产生悬垂指针。
  2. 清理该对象在 weak 表中对应的条目(weak_entry_t),避免表膨胀。

下面我们从"引用计数归零"开始,逐步剖析整个流程,重点讲解 weak 的处理机制。


1. 引用计数归零的入口

在 ARC 下,当对象最后一次被释放时,会调用 objc_releaseobjc_release 内部会执行:

c 复制代码
if (--newRetainCount == 0) {
    // 引用计数变为 0,准备销毁
    ((id)obj)->dealloc();
}

因此,引用计数归零的最终结果就是调用对象的 dealloc 方法。


2. dealloc 的核心调用链

对象的 dealloc 方法(通常由编译器自动生成)最终会调用 Runtime 函数 objc_destructInstance,该函数负责真正的析构工作。objc_destructInstance 的执行顺序大致如下:

  1. 如果有 C++ 析构函数,先调用。
  2. 如果有关联对象,则移除关联对象。
  3. 调用 clearDeallocating,这是处理 weak 和引用计数表的关键步骤。

3. clearDeallocating 的作用

objc_object::clearDeallocating 函数的简化逻辑如下:

c 复制代码
void objc_object::clearDeallocating() {
    // 获取 SideTable
    SideTable *table = SideTable::tableForPointer(this);
    
    // 加锁,防止并发操作
    table->lock();
    
    // 处理 weak 引用:将指向当前对象的所有 weak 指针置 nil,并移除 weak_entry_t
    weak_clear_no_lock(table, this);
    
    // 处理引用计数表:清除该对象在 RefcountMap 中的条目
    table->refcnts.erase(this);
    
    table->unlock();
}

可见,weak 的处理先于引用计数表的清理。这是因为 weak_clear_no_lock 需要读取 weak_table_t,而该表在对象销毁后便不再需要,所以先处理 weak 再清理 refcnts 是合理的。


4. weak_clear_no_lock 详解:将 weak 指针置 nil 并清理 entry

weak_clear_no_lock 是真正执行 weak 清理的函数。它的实现思路是:

  1. 根据对象地址(this)找到对应的 weak_table_t
  2. weak_table_t 中查找该对象对应的 weak_entry_t
  3. 如果找到了,就遍历 weak_entry_t 中的所有 weak 指针地址,将每个指针的内容置为 nil
  4. weak_table_t 中移除这个 weak_entry_t(释放其占用的内存)。

下面我们拆解每一步。

4.1 获取 SideTable 和 weak_table_t

c 复制代码
SideTable *table = &SideTables[this];
weak_table_t *weak_table = &table->weak_table;

这里 this 就是待释放对象的地址。通过对象地址取模得到对应的 SideTable(分离锁),然后取出其中的 weak_table_t

4.2 在 weak_table_t 中查找 weak_entry_t

以对象地址为 key,在 weak_table_t 的哈希表(weak_entries 数组)中查找对应的 weak_entry_t。如果找不到,说明没有 weak 指针指向该对象,直接返回。否则,进入下一步。

4.3 遍历 weak_entry_t,将 weak 指针置 nil

weak_entry_t 内部存储着所有指向该对象的 weak 指针的地址 (即 weak_referrer_t,类型是 id *)。weak_clear_no_lock 会遍历这个容器(无论是静态数组还是动态数组),对每个 referrer 执行:

c 复制代码
*referrer = nil;

这一步直接修改了 weak 变量的内容,使其变为 nil。由于 weak 变量本身是 __weak 修饰的,它们的存储位置可能是栈上的局部变量,也可能是堆上的实例变量,但都是有效的内存地址,因此可以直接赋值。

4.4 从 weak_table_t 中移除 weak_entry_t

遍历并置 nil 完成后,weak_entry_t 已经没有任何作用了。需要将其从 weak_table_t 的哈希表中删除,并释放 weak_entry_t 占用的内存(如果是动态数组,也要释放)。

这一步涉及哈希表的删除操作,具体会:

  • weak_entries 数组中对应的槽位标记为空。
  • 减少 weak_table_tnum_entries 计数。
  • 如果 weak_entry_t 使用了动态数组(即 out_of_line 标志为 1),则释放 referrers 指向的堆内存。

5. 引用计数表的清理

weak_clear_no_lock 之后,clearDeallocating 还会调用:

c 复制代码
table->refcnts.erase(this);

refcnts 是一个 DenseMap(或类似结构),存储了该对象的额外引用计数信息(例如 weak 引用计数、deallocating 标志等)。由于对象即将被销毁,这些信息也不再需要,因此从表中删除。


6. 为什么 weak 处理必须加锁?

在整个过程中,SideTable 的锁一直持有,直到 clearDeallocating 结束。这是因为可能有多个线程同时操作同一个对象的 weak 引用(例如一个线程正在释放对象,另一个线程正在对这个对象取 weak 值),锁保证了操作的原子性,避免出现数据竞争。


7. 完整流程图示

c 复制代码
对象引用计数 → 0
    ↓
调用 dealloc
    ↓
objc_destructInstance
    ↓
clearDeallocating
    ↓
获取 SideTable,加锁
    ↓
weak_clear_no_lock
    ├── 根据对象地址查找 weak_table_t
    ├── 在 weak_entries 中找到 weak_entry_t
    ├── 遍历 referrers 数组
    │     └── 将每个 weak 指针内容置为 nil
    └── 从 weak_entries 中移除 weak_entry_t,释放内存
    ↓
从 refcnts 中擦除该对象的条目
    ↓
解锁 SideTable
    ↓
对象内存被释放(free)

8. 总结

当对象引用计数归零时,Runtime 通过以下步骤处理 weak

  1. 定位 SideTable :通过对象地址找到对应的 SideTable,加锁保证线程安全。
  2. 查找 weak_entry_t :在 weak_table_t 的哈希表中找到该对象对应的 weak_entry_t
  3. 遍历并置 nil :遍历 weak_entry_t 中存储的所有 weak 指针地址,将每个指针的值设为 nil。由于存储的是指针的地址,所以可以直接修改 weak 变量的内容。
  4. 清理 entry :从 weak_table_t 中删除该 weak_entry_t,释放相关内存。
  5. 清理引用计数表 :从 refcnts 中删除该对象的条目。
  6. 解锁 :完成 weak 和引用计数表的清理后,解锁 SideTable

这一过程确保了在对象被彻底销毁前,所有指向它的 weak 指针都被安全地置为 nil,从而避免程序出现野指针崩溃。同时,通过 SideTable 和锁的分离设计,保证了多线程环境下的性能和正确性。

相关推荐
xiaofeichaichai12 小时前
Webpack
前端·webpack·node.js
问心无愧051312 小时前
ctf show web入门111
android·前端·笔记
唐某人丶12 小时前
模型越来越强,我们还需要 Agent 工程吗?—— 从价值重估到 Harness 实践
前端·agent·ai编程
智码看视界12 小时前
现代Web开发基础:全栈工程师的起航点
前端·后端·c5全栈
JS菌13 小时前
手写一个 AI Agent 全栈项目:从沙箱执行到子智能体的完整实现
前端·人工智能·后端
excel14 小时前
HLS TS 文件损坏的元凶:Git 提交与拉取
前端
Aphasia31114 小时前
https连接传输流程
前端·面试
徐小夕14 小时前
万字长文!千万级文档 RAG 知识库系统落地实践
前端·算法·github
threelab14 小时前
Three.js 物理模拟着色器 | 三维可视化 / AI 提示词
开发语言·前端·javascript·人工智能·3d·着色器