专栏:手写框架系列
编号:D01 · 系列第 1 篇
字数:约 6000 字
标签:Swift / iOS / 内存管理 / ARC / 引用计数 / 底层原理 / 手写实现
前言
这是「手写框架系列」的第一篇文章。
我们选择从内存管理开始,是因为它是 Swift(和 Objective-C)区别于 Python、Java 等自动垃圾回收语言的关键特性——Swift 的内存管理是编译时自动插入代码的,而不是运行时垃圾回收器。这意味着理解内存管理,就理解了 Swift 程序真正运行时的样子。
今天的目标是:从零实现一个完整的引用计数系统,包括 retain、release、autorelease、dealloc,以及弱引用的实现原理。
读完这篇,你会对 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 有几个问题:
- Tagged Pointer:小对象(如 NSNumber)不分配堆内存,直接存在指针里
- Inline Refcount:引用计数较小时,直接存在对象头中(节省一次内存访问)
- Side Table:引用计数较大时,溢出到独立的 SideTable 条目中
┌─────────────────────────────────────────────────┐
│ 指针值 │
├─────────────────────────────────────────────────┤
│ Tagged Pointer(约 10% 的情况) │
│ ┌──────────┬───────────────────────┬──────────┐ │
│ │ 1 bit=1 │ 3 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 → 内存泄漏
解决方案:一方使用 weak 或 unowned
// ✅ 解决方案:子引用父用 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 / unowned 在 deinit 中: // deinit 中 self 被清空,无法使用 unowned
self.parent // 已经是 weak?需要视情况决定
七、总结
引用计数系统的完整生命周期
创建对象
↓
refCount = 1
↓
┌───────────────────────────────────────────────┐
│ retain() → refCount++ │
│ release() → refCount-- → if 0 → dealloc │
│ autorelease → 注册到 pool → pool drain 时 release │
└───────────────────────────────────────────────┘
↓
dealloc
↓
1. 设置 dealloc sentinel(禁止再 retain)
2. 调用析构函数
3. 清空所有 weak ref(设为 nil)
4. 释放内存
ARC 编译器的职责
你写的 Swift 代码中的 retain/release 都不是你写的——是编译器自动插入的。编译器通过分析变量的作用域和生命周期,在正确的位置插入保留和释放调用。
这就是为什么 Swift 比 Objective-C 更安全:编译器永远不会忘记写 release,而人类会。
下篇预告
下一篇文章我们将深入 Swift 运行时的核心:objc_msgSend 的汇编级解析——看看 [obj method] 到底在底层做了什么,缓存查找的原理,以及 Swift 的方法分派与 Objective-C 消息传递的差异。
如果你觉得这篇「手写」系列有价值,欢迎点赞并在评论区告诉我你想手写什么框架。