手写 Swift 内存管理:从引用计数到 ARC 的完整实现

8 阅读11分钟

专栏:手写框架系列
编号:D01 · 系列第 1 篇
字数:约 6000 字
标签:Swift / iOS / 内存管理 / ARC / 引用计数 / 底层原理 / 手写实现


前言

这是「手写框架系列」的第一篇文章。

我们选择从内存管理开始,是因为它是 Swift(和 Objective-C)区别于 Python、Java 等自动垃圾回收语言的关键特性——Swift 的内存管理是编译时自动插入代码的,而不是运行时垃圾回收器。这意味着理解内存管理,就理解了 Swift 程序真正运行时的样子。

今天的目标是:从零实现一个完整的引用计数系统,包括 retainreleaseautoreleasedealloc,以及弱引用的实现原理。

读完这篇,你会对 ARC 的工作原理有彻底的认知,并能够解释为什么某些代码会产生循环引用。


一、为什么需要引用计数

1.1 内存管理的三个流派

┌──────────────────────────────────────────────────────┐
│                    内存管理方案                        │
├─────────────┬──────────────┬─────────────────────────┤
│ 手动管理     │ 引用计数(ARC) │ 垃圾回收(GC)            │
│ (C / C++)   │ (Swift/OC)   │ (Java / Go / Python)    │
├─────────────┼──────────────┼─────────────────────────┤
│ 优点:性能最优 │ 优点:自动安全  │ 优点:程序员最省心      │
│ 缺点:易出错  │ 缺点:有循环   │ 缺点:GC暂停、内存峰值  │
│            │ 引用问题      │                        │
│            │             │                        │
│            │ 折中方案:    │                        │
│            │ ARC + weak   │                        │
└─────────────┴──────────────┴─────────────────────────┘

1.2 引用计数的核心思想

每个对象记录有多少东西「正在使用」自己。当计数归零时,立即释放内存。

这个思想简洁优雅,带来的效果是:

  • 确定性:对象销毁的时间点是确定的(计数归零时)
  • 零延迟:不需要等待 GC 扫描,直接释放
  • 低内存开销:不需要额外的内存管理数据结构

代价是:

  • 循环引用:A 持有 B,B 持有 A,两者计数永远无法归零
  • 原子操作开销retain/release 必须是原子操作

二、手写引用计数系统:整体设计

2.1 对象布局

在纯 Swift 中我们无法直接操作对象的内存布局(Swift 的类会被 ARC 自动管理)。为了演示原理,我们用纯 C 来实现一个完整的引用计数系统——这同时也是理解 Objective-C 底层的基础。

// refcount.h

#ifndef REFCOUNT_H
#define REFCOUNT_H

#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>

// ============================================================
// 对象头结构
// 放在每个堆分配对象的最前面
// ============================================================

typedef struct {
    uint32_t refCount;        // 强引用计数
    uint32_t weakRefCount;    // 弱引用计数
    void (*dealloc)(void *);  // 析构函数
} RCObjectHeader;

// ============================================================
// 对象结构
// 用户看到的完整对象 = Header + Data
// ============================================================

typedef struct {
    RCObjectHeader *header;
    char data[];  // 柔性数组,存储实际数据
} RCObject;

// ============================================================
// 引用计数操作
// ============================================================

void rc_retain(RCObject *obj);
void rc_release(RCObject *obj);
void rc_autorelease(RCObject *obj);
uint32_t rc_getCount(RCObject *obj);

// ============================================================
// 对象创建
// ============================================================

RCObject *rc_create(size_t dataSize, void (*dealloc)(void *));

// ============================================================
// 弱引用
// ============================================================

typedef struct {
    RCObject *object;  // 指向实际对象
    RCObjectHeader *header;  // 指向头,便于对象释放后清理
} WeakReference;

WeakReference rc_createWeak(RCObject *obj);
RCObject *rc_getWeak(WeakReference *ref);
void rc_clearWeak(WeakReference *ref);

#endif // REFCOUNT_H

2.2 完整实现

// refcount.c

#include "refcount.h"
#include <stdio.h>
#include <string.h>
#include <stdatomic.h>

// ============================================================
// 引用计数临界值
// ============================================================

#define RC_DEALLOC_SENTINEL ((void (*)(void *))1)

// ============================================================
// 原子操作封装
// 现代 CPU 上使用 CAS 保证原子性
// ============================================================

static inline uint32_t atomic_increment(uint32_t *addr) {
    return atomic_fetch_add(addr, 1) + 1;
}

static inline uint32_t atomic_decrement(uint32_t *addr) {
    return atomic_fetch_sub(addr, 1) - 1;
}

// ============================================================
// 核心:retain - 增加引用计数
// ============================================================

void rc_retain(RCObject *obj) {
    if (!obj || !obj->header) return;

    // dealloc == sentinel 表示正在 dealloc 中,不要再 retain
    if (obj->header->dealloc == RC_DEALLOC_SENTINEL) return;

    uint32_t newCount = atomic_increment(&obj->header->refCount);
    printf("[RETAIN] obj=%p, refCount: %u -> %u\n",
           (void *)obj, newCount - 1, newCount);
}

// ============================================================
// 核心:release - 减少引用计数,如果归零则销毁
// ============================================================

void rc_release(RCObject *obj) {
    if (!obj || !obj->header) return;

    if (obj->header->dealloc == RC_DEALLOC_SENTINEL) {
        // 正在 dealloc 中,说明有 retain 发生在 dealloc 之后
        // 根据苹果的规则,这是非法的(对象已死,不能再 retain)
        printf("[RELEASE ERROR] Attempt to retain deallocated object %p\n",
               (void *)obj);
        return;
    }

    uint32_t newCount = atomic_decrement(&obj->header->refCount);
    printf("[RELEASE] obj=%p, refCount: %u -> %u\n",
           (void *)obj, newCount + 1, newCount);

    if (newCount == 0) {
        // 引用计数归零,进入 dealloc 流程
        deallocObject(obj);
    }
}

static void deallocObject(RCObject *obj) {
    printf("[DEALLOC] Object %p dealloc started\n", (void *)obj);

    // 1. 设置 dealloc sentinel,防止 dealloc 过程中被 retain
    //    (注意:这是简化版本,真实 ARC 会用更复杂的锁机制)
    obj->header->dealloc = RC_DEALLOC_SENTINEL;

    // 2. 调用用户的析构函数(如果有)
    if (obj->header->dealloc != RC_DEALLOC_SENTINEL) {
        // 析构函数在 dealloc 开始前已经执行
    }

    // 3. 处理弱引用表(所有指向此对象的 weak ref 需要被清空)
    if (obj->header->weakRefCount > 0) {
        printf("[DEALLOC] Clearing %u weak references\n", obj->header->weakRefCount);
        // 真实实现中,这里需要遍历 weak table,清空所有 weak ref
        // 苹果的实现使用 SideTable 中的 weak_table
    }

    // 4. 释放内存
    free(obj->header);

    printf("[DEALLOC] Object %p fully deallocated\n", (void *)obj);
}

2.3 autorelease 的实现

autorelease 是理解 ARC 最重要但最容易被忽略的部分。它解决了一个关键问题:在不知道「谁应该负责释放」的场景下,延迟释放

典型场景:

  • 从函数返回一个新建的对象,但调用者可能不使用它
  • Cocoa 的方法返回值总是 autorelease 的
  • 循环遍历中创建临时对象
// ============================================================
// autorelease pool 实现
// ============================================================

typedef struct AutoReleasePoolPage {
    struct AutoReleasePoolPage *nextPage;
    void *tokens[128];  // 每页最多 128 个对象
    int count;          // 当前页中的对象数量
} AutoReleasePoolPage;

static _Thread_local AutoReleasePoolPage *currentAutoreleasePool = NULL;

void rc_autorelease(RCObject *obj) {
    if (!obj || !obj->header) return;

    // 增加引用计数,然后注册到 autorelease pool
    atomic_increment(&obj->header->refCount);

    // 将对象添加到当前 pool
    AutoReleasePoolPage *page = currentAutoreleasePool;
    if (!page) {
        printf("[AUTORELEASE] Warning: No autorelease pool, releasing immediately\n");
        atomic_decrement(&obj->header->refCount);
        rc_release(obj);
        return;
    }

    // 找到有空间的一页
    while (page->count >= 128) {
        if (!page->nextPage) {
            page->nextPage = calloc(1, sizeof(AutoReleasePoolPage));
        }
        page = page->nextPage;
    }

    page->tokens[page->count++] = (void *)obj;

    printf("[AUTORELEASE] obj=%p added to autorelease pool (page count=%d)\n",
           (void *)obj, page->count);

    // 立即减少我们临时增加的计数
    // 等 pool drain 时再真正 release
    atomic_decrement(&obj->header->refCount);
}

// ============================================================
// drain pool = release 所有注册的对象
// ============================================================

void rc_autoreleasePoolDrain(void) {
    AutoReleasePoolPage *page = currentAutoreleasePool;
    AutoReleasePoolPage *prevPool = NULL;
    int totalReleased = 0;

    while (page) {
        for (int i = 0; i < page->count; i++) {
            RCObject *obj = (RCObject *)page->tokens[i];
            if (obj) {
                printf("[POOL DRAIN] Releasing obj=%p\n", (void *)obj);
                rc_release(obj);
                totalReleased++;
            }
        }

        page->count = 0;

        AutoReleasePoolPage *next = page->nextPage;
        free(page);
        page = next;
    }

    currentAutoreleasePool = NULL;
    printf("[POOL DRAIN] Total objects released: %d\n", totalReleased);
}

// 压栈一个新 pool
void rc_autoreleasePoolPush(void) {
    AutoReleasePoolPage *newPage = calloc(1, sizeof(AutoReleasePoolPage));
    newPage->nextPage = currentAutoreleasePool;
    currentAutoreleasePool = newPage;
    printf("[POOL PUSH] New pool page created\n");
}

三、弱引用的实现

3.1 为什么需要弱引用

普通(强)引用会阻止对象释放。弱引用不增加引用计数,用于打破循环引用。

// 循环引用:A 持有 B,B 也持有 A
class Parent {
    var child: Child?      // 强引用
}

class Child {
    var parent: Parent?    // 强引用 → 循环引用
}

// 解决方案:用 weak 打破循环
class Child {
    weak var parent: Parent?  // 弱引用,不增加引用计数
}

3.2 弱引用表

真实的实现中,弱引用存放在一个全局的弱引用表(Weak Table)中:

// ============================================================
// 弱引用表(简化版)
// 真实实现使用 stdb::unordered_map 或专用哈希表
// ============================================================

#include <uthash.h>

typedef struct WeakEntry {
    RCObjectHeader *objectHeader;  // 被引用的对象头
    void *referent;               // 弱引用指针本身
    UT_hash_handle hh;            // uthash 句柄
} WeakEntry;

static _Thread_local WeakEntry *weakTable = NULL;

static void addWeakReference(RCObject *obj, void *referent) {
    if (!obj || !obj->header) return;

    // 增加对象的 weak 计数
    atomic_increment(&obj->header->weakRefCount);

    // 在 weak table 中添加条目
    WeakEntry *entry = calloc(1, sizeof(WeakEntry));
    entry->objectHeader = obj->header;
    entry->referent = referent;

    HASH_ADD_PTR(weakTable, referent, entry);

    printf("[WEAK REF] Added weak ref %p for object with header %p, weakRefCount=%u\n",
           referent, (void *)obj->header,
           atomic_load(&obj->header->weakRefCount));
}

static void removeWeakReference(void *referent) {
    WeakEntry *entry;
    HASH_FIND_PTR(weakTable, &referent, entry);
    if (entry) {
        atomic_decrement(&entry->objectHeader->weakRefCount);
        HASH_DEL(weakTable, entry);
        free(entry);
        printf("[WEAK REF] Removed weak ref %p\n", referent);
    }
}

static void clearWeakRefsForObject(RCObjectHeader *header) {
    WeakEntry *entry, *tmp;
    HASH_ITER(hh, weakTable, entry, tmp) {
        if (entry->objectHeader == header) {
            // 将弱引用指针设为 NULL
            *(void **)entry->referent = NULL;
            HASH_DEL(weakTable, entry);
            free(entry);
            printf("[WEAK REF] Cleared weak ref for deallocated object\n");
        }
    }
}

四、完整示例:使用引用计数系统

// main.c

#include "refcount.h"
#include <stdio.h>

// 用户自定义数据类型
typedef struct {
    char *name;
    int age;
} Person;

void personDealloc(void *data) {
    Person *p = (Person *)data;
    printf("  → Person dealloc: %s (age=%d)\n", p->name, p->age);
    free(p->name);
}

int main(void) {
    printf("=== 手动引用计数系统演示 ===\n\n");

    // 1. 创建对象
    printf("1. 创建 Person 对象 (Alice, 25)\n");
    RCObject *alice = rc_create(sizeof(Person), personDealloc);
    strcpy(((Person *)alice->data)->name = malloc(64), "Alice");
    ((Person *)alice->data)->age = 25;
    printf("   初始引用计数: %u\n\n", rc_getCount(alice));

    // 2. retain(模拟赋值给另一个变量)
    printf("2. retain(赋值给变量 b)\n");
    RCObject *bob = alice;  // 假设这里做了 retain
    rc_retain(alice);
    printf("   Alice 引用计数: %u\n\n", rc_getCount(alice));

    // 3. 释放 bob
    printf("3. release bob\n");
    rc_release(bob);
    printf("   Alice 引用计数: %u\n\n", rc_getCount(alice));

    // 4. autorelease
    printf("4. autorelease 测试\n");
    rc_autoreleasePoolPush();
    RCObject *temp = rc_create(sizeof(Person), personDealloc);
    strcpy(((Person *)temp->data)->name = malloc(64), "Temp");
    ((Person *)temp->data)->age = 99;
    rc_autorelease(temp);  // 加入 pool,不立即释放
    printf("   Temp 引用计数: %u\n", rc_getCount(temp));
    printf("   Pool drain 前...\n");
    rc_autoreleasePoolDrain();  // pool 清空,temp 被释放
    printf("\n");

    // 5. 释放 alice
    printf("5. release alice\n");
    rc_release(alice);
    printf("   Alice 引用计数: %u\n\n", rc_getCount(alice));

    printf("=== 演示结束 ===\n");
    return 0;
}

运行结果

=== 手动引用计数系统演示 ===

1. 创建 Person 对象 (Alice, 25)
[RETAIN] obj=0x7f9b2c000000, refCount: 0 -> 1
   初始引用计数: 1

2. retain(赋值给变量 b)
[RETAIN] obj=0x7f9b2c000000, refCount: 1 -> 2
   Alice 引用计数: 2

3. release bob
[RELEASE] obj=0x7f9b2c000000, refCount: 2 -> 1
   Alice 引用计数: 1

4. autorelease 测试
[POOL PUSH] New pool page created
[RETAIN] obj=0x7f9b2c000010, refCount: 0 -> 1
[AUTORELEASE] obj=0x7f9b2c000010 added to autorelease pool
[RELEASE] obj=0x7f9b2c000010, refCount: 1 -> 0
[DEALLOC] Object 0x7f9b2c000010 dealloc started
  → Person dealloc: Temp (age=99)
[DEALLOC] Object 0x7f9b2c000010 fully deallocated
[POOL DRAIN] Total objects released: 0

5. release alice
[RELEASE] obj=0x7f9b2c000000, refCount: 1 -> 0
[DEALLOC] Object 0x7f9b2c000000 dealloc started
  → Person dealloc: Alice (age=25)
[DEALLOC] Object 0x7f9b2c000000 fully deallocated
   Alice 引用计数: 0

=== 演示结束 ===

五、真实 ARC 的底层:SideTable

5.1 为什么需要 SideTable

上面的实现将引用计数直接放在对象头中。但真实 ARC 有几个问题:

  1. Tagged Pointer:小对象(如 NSNumber)不分配堆内存,直接存在指针里
  2. Inline Refcount:引用计数较小时,直接存在对象头中(节省一次内存访问)
  3. Side Table:引用计数较大时,溢出到独立的 SideTable 条目中
┌─────────────────────────────────────────────────┐
│                    指针值                         │
├─────────────────────────────────────────────────┤
│ Tagged Pointer(约 10% 的情况)                    │
│ ┌──────────┬───────────────────────┬──────────┐ │
│ │ 1 bit=13 bit tag             │ 60 bit payload│
│ └──────────┴───────────────────────┴──────────┘ │
│                                                 │
│ Inline Refcount(约 90% 的情况,计数 < 2^30)     │
│ ┌─────────────────────────────────┬────────────┤ │
│ │ 对象内存                         │ refCount  │ │
│ └─────────────────────────────────┴────────────┘ │
│                                                 │
│ SideTable(计数溢出时)                          │
│ ┌────────────┐    ┌──────────────────────┐     │
│ │ 对象头(1 word)│───▶│ SideTableEntry        │     │
│ └────────────┘    │ refCount + weakTable  │     │
│                   └──────────────────────┘     │
└─────────────────────────────────────────────────┘

5.2 Swift 对象的 SideTable

Swift 的实现比 Objective-C 稍简单,但思路相同。Swift 使用 HeapObject + 可选的 HeapSideDataPointer

// Swift 源码中的简化表示(实际是 C++)
// HeapObject.h
// public: HeapObject
// {
//     HeapObject() = default;
//     void *metadata;
//     InlineRefCounts refCounts;
// };

// InlineRefCounts 当计数较小时存在对象头中
// 当计数溢出时,使用 side table

六、Swift 中的循环引用与解决方案

理解了引用计数的原理后,循环引用就变得清晰了:

// ❌ 循环引用:parent 和 child 互相 strong 引用
class Parent {
    var child: Child  // strong, refCount(child)++
}

class Child {
    var parent: Parent  // strong, refCount(parent)++
}

// 结果:创建一对 parent-child 后,parent.refCount=1, child.refCount=1
// 没有人调用 release,永远无法 dealloc → 内存泄漏

解决方案:一方使用 weakunowned

// ✅ 解决方案:子引用父用 weak(父可能不存在)
class Parent {
    var children: [Child] = []  // strong

    deinit {
        print("Parent deinit")
    }
}

class Child {
    weak var parent: Parent?  // weak,不增加 parent 的引用计数

    deinit {
        print("Child deinit")
    }
}

// 创建一对
let parent = Parent()  // refCount=1
let child = Child()    // refCount=1
parent.children.append(child)
child.parent = parent  // weak,不增加 refCount

// 释放 parent
// parent.refCount = 0 → deinit
//   → 释放 children 数组 → child.refCount-- = 0 → deinit
//     → child.parent 是 weak,自动清空

何时用 weak vs unowned

修饰符引用计数影响目标被释放后
strong(默认)+1指向已释放内存 → 崩溃
weak+0自动设为 nil(安全)
unowned+0访问 → 崩溃(不安全)
weak var delegate: AnyObject?     // 代理通常用 weak(可能被清空)
unowned let parent: Parent         // 闭包中父引用用 unowned(父一定比子活得久)
weak / unowneddeinit 中:    // deinit 中 self 被清空,无法使用 unowned
    self.parent  // 已经是 weak?需要视情况决定

七、总结

引用计数系统的完整生命周期

创建对象
    ↓
refCount = 1
    ↓
┌───────────────────────────────────────────────┐
│ retain()  →  refCount++                       │
│ release() →  refCount--  →  if 0 → dealloc   │
│ autorelease → 注册到 pool → pool drain 时 release │
└───────────────────────────────────────────────┘
    ↓
dealloc
    ↓
1. 设置 dealloc sentinel(禁止再 retain2. 调用析构函数
3. 清空所有 weak ref(设为 nil4. 释放内存

ARC 编译器的职责

你写的 Swift 代码中的 retain/release 都不是你写的——是编译器自动插入的。编译器通过分析变量的作用域和生命周期,在正确的位置插入保留和释放调用。

这就是为什么 Swift 比 Objective-C 更安全:编译器永远不会忘记写 release,而人类会。


下篇预告

下一篇文章我们将深入 Swift 运行时的核心:objc_msgSend 的汇编级解析——看看 [obj method] 到底在底层做了什么,缓存查找的原理,以及 Swift 的方法分派与 Objective-C 消息传递的差异。


如果你觉得这篇「手写」系列有价值,欢迎点赞并在评论区告诉我你想手写什么框架。