Swift 自动引用计数(ARC)
自动引用计数(Automatic Reference Counting,简称 ARC)是 Swift 管理类实例内存的方式。
它自动跟踪每个实例被多少个引用持有,一旦没人再引用,就释放这块内存。
ARC 处理不了的是「互相引用」和「闭包捕获自己」这两种情况,需要配合 weak 与 unowned 手动打破。
ARC 的工作原理
每创建一个类实例,ARC 就为它记一个引用计数。
每多一个强引用,计数加一;每少一个强引用,计数减一;计数归零时,实例被销毁并调用 deinit。
这些加减由编译器自动插入,不需要手写 retain 或 release。
实例
class Runoob {
let name: String
init(name: String) {
self.name = name
print("\(name) 被创建")
}
deinit {
print("\(name) 被销毁")
}
}
var site: Runoob? = Runoob(name: "Runoob") // 引用计数 1
print("创建完成")
site = nil // 引用计数归零,触发 deinit
print("置 nil 完成")
执行以上代码输出:
Runoob 被创建 创建完成 Runoob 被销毁 置 nil 完成
注意:ARC 只作用于类实例。结构体和枚举是值类型,它们的生命周期由作用域和复制决定,不存在引用计数。
强引用与引用计数
默认的引用都是强引用,只要还有一个强引用存在,实例就不会被释放。
把变量置为 nil 只是减少一个引用,计数没到零,实例就还活着。
实例
var ref2 = ref1 // 引用计数 2
var ref3 = ref1 // 引用计数 3
ref1 = nil // 引用计数 2
ref2 = nil // 引用计数 1
print("还剩 ref3 持有实例")
ref3 = nil // 引用计数 0,实例销毁
print("全部释放")
执行以上代码输出:
RUNOOB 被创建 还剩 ref3 持有实例 RUNOOB 被销毁 全部释放
deinit 只能由 ARC 调用,不能手动调用,也不能在里面抛错。
循环强引用
如果两个实例互相持有对方的强引用,它们的引用计数永远不会归零。
这就是循环强引用(retain cycle),也是 ARC 最常见的内存泄漏来源。
实例
let name: String
var apartment: Apartment? // 强引用
init(name: String) {
self.name = name
print("\(name) 被创建")
}
deinit {
print("\(name) 被销毁")
}
}
class Apartment {
let unit: String
var tenant: Person? // 也是强引用,形成循环
init(unit: String) {
self.unit = unit
print("公寓 \(unit) 被创建")
}
deinit {
print("公寓 \(unit) 被销毁")
}
}
var runoob: Person? = Person(name: "Runoob")
var unitA: Apartment? = Apartment(unit: "A-101")
runoob!.apartment = unitA // Person 持有 Apartment
unitA!.tenant = runoob // Apartment 持有 Person
runoob = nil // 局部变量置 nil
unitA = nil // 局部变量置 nil
print("两个对象互相持有,引用计数都还是 1,deinit 不会执行")
执行以上代码输出:
Runoob 被创建 公寓 A-101 被创建 两个对象互相持有,引用计数都还是 1,deinit 不会执行
注意输出里没有任何「被销毁」,说明两个实例都还留在内存里。
打破循环的办法是:把其中一端的引用改成 weak 或 unowned。
弱引用 weak
弱引用不会增加引用计数,目标被释放时它自动变成 nil。
因为可能变成 nil,弱引用必须是 var 声明的可选类型。
把上面 Apartment 的 tenant 改成弱引用,循环就被打破了。
实例
class Apartment {
let unit: String
weak var tenant: Person? // 弱引用:不增加引用计数
init(unit: String) {
self.unit = unit
print("公寓 \(unit) 被创建")
}
deinit {
print("公寓 \(unit) 被销毁")
}
}
var runoob: Person? = Person(name: "Runoob")
var unitA: Apartment? = Apartment(unit: "A-101")
runoob!.apartment = unitA // Person 强引用 Apartment
unitA!.tenant = runoob // Apartment 弱引用 Person
runoob = nil // Person 没有其他强引用,被销毁
print("租客是否还在:\(String(describing: unitA!.tenant))")
unitA = nil // Apartment 也被销毁
print("结束")
执行以上代码输出:
Runoob 被创建 公寓 A-101 被创建 Runoob 被销毁 租客是否还在:nil 公寓 A-101 被销毁 结束
弱引用适合表达「不是拥有关系」:代理对象、父节点回指子节点、缓存条目等。
无主引用 unowned
无主引用同样不增加引用计数,但它假设目标永远比引用者活得久。
因此它可以是 let,也可以是非可选类型;代价是目标被释放后再访问它,程序会直接崩溃。
经典场景是「信用卡一定属于某位客户,且客户一定比卡活得久」。
实例
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
print("客户 \(name) 被创建")
}
deinit {
print("客户 \(name) 被销毁")
}
}
class CreditCard {
let number: String
unowned let customer: Customer // 无主引用:卡一定属于某位客户
init(number: String, customer: Customer) {
self.number = number
self.customer = customer
print("信用卡 \(number) 被创建")
}
deinit {
print("信用卡 \(number) 被销毁")
}
}
var runoob: Customer? = Customer(name: "Runoob")
runoob!.card = CreditCard(number: "6222-0000", customer: runoob!)
print("卡主:\(runoob!.card!.customer.name)")
runoob = nil // 客户被释放,卡也随之释放,没有循环引用
print("结束")
执行以上代码输出:
客户 Runoob 被创建 信用卡 6222-0000 被创建 卡主:Runoob 客户 Runoob 被销毁 信用卡 6222-0000 被销毁 结束
三种引用方式的区别可以这样记:
| 引用方式 | 增加引用计数 | 声明要求 | 目标释放后 | 适用场景 |
|---|---|---|---|---|
| 强引用 | 是 | 无特殊要求 | 目标不会被释放 | 常规持有 |
弱引用 weak | 否 | 必须是 var 的可选类型 | 自动置为 nil | 目标可能先释放,如代理 |
无主引用 unowned | 否 | 可以是 let、可以非可选 | 访问时崩溃 | 目标生命周期不短于引用者 |
注意:拿不准就用
weak。它虽然多一层可选判断,但出错时只是拿到 nil,而unowned出错会直接崩溃。
闭包引起的循环引用
闭包会捕获它用到的外部变量,包括 self。
如果实例又持有这个闭包,就构成了「实例 → 闭包 → 实例」的循环。
实例
let site: String
var onFinish: (() -> String)? // 实例持有闭包
init(site: String) {
self.site = site
}
func prepare() {
// 闭包捕获 self,而实例又持有这个闭包 → 循环引用
onFinish = {
return "站点:\(self.site)"
}
}
deinit {
print("Reporter 被销毁")
}
}
var reporter: Reporter? = Reporter(site: "www.runoob.com")
reporter!.prepare()
print(reporter!.onFinish!())
reporter = nil
print("闭包持有 self,实例没有被销毁")
执行以上代码输出:
站点:www.runoob.com 闭包持有 self,实例没有被销毁
赋值给实例属性的闭包、以及 lazy var 里引用 self 的闭包,都属于这一类。
捕获列表与 guard let self
解决办法是在闭包开头写捕获列表 [weak self],让闭包不持有实例。
由于 self 变成了可选类型,访问前要先解包,标准写法就是 guard let self else { return }。
实例
func prepare() {
// 捕获列表 [weak self]:闭包不再持有实例
onFinish = { [weak self] in
// Swift 5.7 起可以简写为 guard let self
guard let self else { return "对象已释放" }
return "站点:\(self.site)"
}
}
var reporter: Reporter? = Reporter(site: "www.runoob.com")
reporter!.prepare()
let closure = reporter!.onFinish!
print(closure()) // 实例还在
reporter = nil // 释放实例,闭包里的 self 变成 nil
print(closure()) // guard let self 生效,安全返回
执行以上代码输出:
站点:www.runoob.com Reporter 被销毁 对象已释放
guard let self else { return } 的作用是把「self 已释放」这种情况提前处理掉,避免后续代码访问一个空对象。
在返回 Void 的闭包里直接写 return,在返回值的闭包里则返回一个兜底值。
| 捕获列表 | 闭包与 self 的关系 | self 的类型 | 适用场景 |
|---|---|---|---|
| 不写 | 强引用,容易形成循环 | Self | 非逃逸闭包,或确认不产生循环 |
[weak self] | 弱引用 | Self? | self 可能先于闭包释放 |
[unowned self] | 无主引用 | Self | self 生命周期一定不短于闭包 |
只在闭包会被存储、逃逸出当前作用域时才需要捕获列表;同步执行完就丢弃的闭包不会造成循环。
常见问题
下面几个问题在排查内存泄漏时最常遇到。
什么时候该写捕获列表
当闭包被实例持有、或者闭包会逃逸并长期存活,而闭包里又用到了 self 时,就加上捕获列表。
反过来,传给 map、sorted 这类同步执行的闭包,通常不需要。
为什么加了 weak 之后还要 guard let self
因为 weak 把 self 变成了可选类型,直接用会有编译错误。
用 guard let self else { return } 一次性解包,后面就能像普通实例一样访问。
weak 和 unowned 到底怎么选
目标可能先释放、或者两者的生命周期没有明确关系时,用 weak。
只有当你能明确保证目标一定比引用者活得久,并且不想处理可选值时,才用 unowned。
循环引用能自动被发现吗
编译器不能,ARC 也不能。它只负责引用计数,不分析对象之间的关系。
排查时可以用 Xcode 的 Memory Graph 或 Instruments 的 Leaks 模板,观察那些本该销毁却没有 deinit 的对象。
