首页 / Swift 编程语言教程 / 自动引用计数 ARC

Swift 编程语言教程

自动引用计数 ARC

本教程共 93 篇 · 第 75 篇 · 更新于 2026-08-08 · 约 7 分钟阅读

SwiftSwift 编程语言教程ARC自动引用计数内存管理强引用引用类型

本节目标:理解 Swift 如何用 ARC 自动管理类实例的内存,搞清楚引用计数的运作方式和强引用的含义。

写代码的时候你几乎从没手动释放过内存,这不是你偷懒,而是 Swift 替你干了。Swift 用一套叫 ARC 的机制,跟踪每个类实例被谁引用着,等没人引用了就自动把它占用的内存收回来。这套机制叫自动引用计数(Automatic Reference Counting,ARC)。

ARC 只在类实例上生效。结构体、枚举这些是值类型,复制就直接拷贝,根本不存在”引用”这一说,所以也用不上引用计数。理解了这一点,你就抓住了内存管理的一半真相。

1-1 为什么需要 ARC

程序运行时要不断创建对象,每个对象都占着一块内存。如果对象用完了却一直赖在内存里不走,内存就会越占越多,这叫内存泄漏。反过来,如果一个对象还在用,你却提前把它删了,再去访问它就会崩溃。

早期的编程语言要么靠程序员手动记住释放,要么靠垃圾回收器(GC)在后台定期扫描。Swift 选了中间路线:用引用计数。它的思路特别朴素——给每个类实例配一个计数器,记录”现在有几个人指着它”。有人指向它,计数加一;有人不再指向它,计数减一。当计数变成零,说明没人要它了,系统立刻把它销毁,内存收回。

Note

ARC 这个名字里的”自动”,意思是你写代码时完全不用管计数。你只管正常地赋值、传参、nil 掉变量,编译器生成的代码会替你维护计数。

这套机制最舒服的地方在于”刚刚好”。对象在还有人用的时候绝不会被回收,等真没人用了马上回收,不拖泥带水,也不像 GC 那样有不确定性停顿。

1-2 引用计数的基本规则

你把一个类实例赋值给变量、常量或属性时,那个变量就对实例建立了一个强引用(strong reference)。“强”这个字很关键:只要还存在至少一个强引用,ARC 就不敢回收这个实例。哪怕其他强引用都没了,只要还有一个,对象就活得稳稳的。

想象一个房间里有一盏灯,灯旁边站着几个人。每个人代表一个强引用。只要房间里还有人(至少一个强引用),灯就亮着(对象活着)。当最后一个人离开(最后一个强引用断开),灯就灭了(对象被回收)。这就是引用计数的日常比喻。

强引用之所以”强”,是因为它牢牢抓着实例不放。只要它还在,实例就绝不会被销毁。绝大多数时候,这正是你想要的——你引用的东西当然得在。问题出在特殊的结构上,比如两个对象互相抓着对方,那种情况后面几章专门讲。

1-3 看 ARC 工作的全过程

光说规则太虚,直接看一个例子。下面这个 Person 类在初始化和销毁时各打印一句话,方便我们观察 ARC 的时机。

class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) 被初始化了")
    }
    deinit {
        print("\(name) 被销毁了")
    }
}

var reference1: Person?
var reference2: Person?
var reference3: Person?

reference1 = Person(name: "张三")
// 打印:张三 被初始化了

reference2 = reference1
reference3 = reference1
// 现在张三这个实例有 3 个强引用

创建 张三 时打印了初始化信息,说明实例诞生了。随后把它又赋给 reference2reference3,于是这一个实例被三个变量同时指着,引用计数是 3。

接着断开其中两个强引用:

reference1 = nil
reference2 = nil
// 引用计数变成 1,实例还在,没打印销毁信息

此时还有 reference3 指着,计数是 1,所以 张三 还活着,没触发 deinit。只有最后一条也断开,计数归零,对象才真正被回收:

reference3 = nil
// 打印:张三 被销毁了

这一刻你会看到 deinit 的打印。这就是 ARC 在背后完成的回收——你没写一行释放代码,它自己算清了计数。

1-4 deinit 帮你确认回收时机

deinit 叫反初始化器,是类专属的(结构体、枚举没有)。实例即将被销毁的那一刻,它会被自动调用一次。你常把它当作”这个对象真的释放了”的证据,也用来做清理工作,比如关闭文件、取消通知监听。

初学者常踩一个坑:以为把变量设成 nil 就一定触发销毁。其实只有当引用计数因此归零才会销毁。如果还有别的强引用指着同一个实例,你设 nil 只是放开了自己那只手,对象照旧活着。

Tip

调试内存问题时,在 deinit 里打印类名或标识,能让你直观看到对象什么时候释放、什么时候没释放。这比猜要靠谱得多。

1-5 值类型不玩引用计数

必须反复强调:ARC 只管类。你定义一个 structenum,把它赋值给另一个变量时,发生的是整份拷贝,不是引用。拷贝出来的新值和旧值各占各的内存,谁也不指向谁,自然不存在”引用计数”这一说。

看下面这个结构体的行为:

struct Resolution {
    var width = 0
    var height = 0
}

let hd = Resolution(width: 1920, height: 1080)
var cinema = hd
cinema.width = 2048

print(hd.width)      // 1920
print(cinema.width)  // 2048

cinema 拿到的是 hd 的一份独立拷贝。改 cinema.width 完全不影响 hd。这里没有计数、没有销毁时机问题,因为两块内存互不相干。

Swift 里所有基础类型——IntDoubleBoolStringArrayDictionary——背后都是结构体,都是值类型。所以你平时大量操作的数组、字典,其实走的都是值语义,和 ARC 无关。这一点对你后续理解”值语义与写时复制”那一章很重要。

1-6 你真正需要操心 ARC 的场景

日常写代码,ARC 基本透明,你感受不到它。只有在少数情况下,引用计数会卡住,对象永远释放不掉,这时候你才需要动手干预:

第一,两个类实例互相持有强引用,谁也不肯先松手,导致计数永远到不了零。这叫循环强引用(strong reference cycle),是内存泄漏的头号原因。

第二,类实例持有一个闭包,而这个闭包又在里面捕获了 self,形成”实例抓着闭包、闭包抓着实例”的循环。

这两种情况分别在下一章和第 77 章细讲,破解办法是 weakunowned 这两种”弱一点”的引用。但在那之前,你先得把”强引用会牢牢抓住实例”这个前提刻进脑子里。

Warning

如果某段代码反复创建对象却从没看到对应的 deinit 打印,八成是出现了循环强引用。别忽视它,它会随着程序运行悄悄吃掉内存,时间一长就会变卡甚至崩溃。

1-7 小结与本模块位置

ARC 是 Swift 内存管理的地基:它用引用计数为类实例自动收内存,只在引用计数归零时销毁对象。强引用是默认行为,也是循环引用的根源。值类型不参与 ARC,因为复制即拷贝。

这一章是模块九”内存管理与值语义”的入口。往下你会学到怎么用 weak/unowned 破解类之间的循环引用,怎么处理闭包捕获造成的循环,以及值类型那套”写时复制”的巧妙优化。把 ARC 吃透,后面几章就顺了。