Highlight
Apple 解释了 Swift 自动引用计数(ARC)如何按最后一次使用管理对象生命周期,并给出 weak/unowned 引用、withExtendedLifetime()、API 重设计和去除 deinit 副作用的安全写法来避免隐藏内存错误。
核心内容
Swift 的类是引用类型。一个 Traveler 对象可以被 traveler1 和 traveler2 同时指向。开发者写的是普通赋值,编译器在背后插入 retain 和 release。
麻烦从“对象到底什么时候释放”开始。Swift 的对象生命周期以使用为基础。最低保证生命周期从初始化开始,到最后一次使用结束。它不保证一直活到右花括号。
大多数代码不关心这个细节。对象早一点释放或晚一点释放,结果相同。
问题出现在两个地方。第一,weak 和 unowned 引用会让生命周期变得可观察。对象一旦释放,weak 读出来是 nil,unowned 访问会 trap。第二,deinit 可以有副作用,例如打印日志、发布指标、写入全局状态。副作用发生的时间会影响程序结果。
Apple 在这场 session 里做的事情很直接:先说明 ARC 的最低保证,再展示哪些语言特性会暴露生命周期,最后给出三类修复方式。轻量方式是 withExtendedLifetime()。更稳的方式是改 API,让外部只能通过强引用访问对象。最彻底的方式是避免引用环,或者把 deinit 副作用移到显式方法里。
详细内容
ARC 的最低保证:对象活到最后一次使用
(01:49)ARC(Automatic Reference Counting,自动引用计数)由 Swift 编译器驱动。编译器插入 retain 和 release。运行时引用计数归零后,对象可以被释放。
final class Traveler {
var name: String
var destination: String?
init(name: String) {
self.name = name
}
}
func test() {
let traveler1 = Traveler(name: "Lily")
let traveler2 = traveler1
traveler2.destination = "Big Sur"
print("Done traveling")
}
test()
关键点:
final class Traveler定义一个引用类型,实例会放在堆上。name是普通存储属性,用来标识旅行者。destination是可选字符串,演示对象状态变化。init(name:)创建对象时设置name,对象初始引用计数为 1。traveler1是第一个强引用。traveler2 = traveler1复制引用,两个常量指向同一个对象。traveler2.destination = "Big Sur"是traveler2的最后一次使用。print("Done traveling")没有再使用Traveler对象,ARC 允许对象在它之前或附近被释放,具体位置取决于编译器插入的 release。
这段代码的关键事实是:Swift 保证对象至少活到最后一次使用。观察到的释放点可能更晚,因为 ARC 优化会改变 retain/release 的位置。
引用循环:两个强引用会让对象永远释放不了
(06:37)旅行 App 增加积分账户后,Traveler 指向 Account,Account 又指回 Traveler。如果两边都是强引用,函数结束后两个对象的引用计数仍然不为 0,内存泄漏就发生了。
final class Traveler {
var name: String
var account: Account?
init(name: String) {
self.name = name
}
func printSummary() {
if let account = account {
print("\(name) has \(account.points) points")
}
}
}
final class Account {
var traveler: Traveler
var points: Int
init(traveler: Traveler, points: Int) {
self.traveler = traveler
self.points = points
}
}
func test() {
let traveler = Traveler(name: "Lily")
let account = Account(traveler: traveler, points: 1000)
traveler.account = account
traveler.printSummary()
}
test()
关键点:
Traveler.account是强引用,因为普通类属性默认强引用对象。Account.traveler也是强引用。Account(traveler: traveler, points: 1000)让账户持有旅行者。traveler.account = account让旅行者持有账户。test()结束后,局部常量消失,但两个对象仍互相持有。- ARC 只看引用计数,不会自动识别并打破这个环。
weak 引用:打破环,也暴露生命周期
(09:05)常见修复方式是在环的一边使用 weak。weak 不参与引用计数。被引用对象释放后,Swift 运行时会把 weak 引用读成 nil。
final class Traveler {
var name: String
var account: Account?
init(name: String) {
self.name = name
}
func printSummary() {
if let account = account {
print("\(name) has \(account.points) points")
}
}
}
final class Account {
weak var traveler: Traveler?
var points: Int
init(traveler: Traveler, points: Int) {
self.traveler = traveler
self.points = points
}
}
func test() {
let traveler = Traveler(name: "Lily")
let account = Account(traveler: traveler, points: 1000)
traveler.account = account
traveler.printSummary()
}
test()
关键点:
weak var traveler: Traveler?不增加Traveler的引用计数。weak属性必须能表示对象已经不存在,所以这里是可选类型。Traveler仍然强持有Account。Account不再强持有Traveler,引用环被打断。traveler.printSummary()通过强引用访问旅行者,调用期间对象生命周期有保证。
这段代码展示了 weak 的正确位置:用来打断引用环。它没有把 weak 引用当成主要访问路径。
通过 weak 访问对象:optional binding 只能隐藏问题
(10:05)如果把 printSummary() 移到 Account 上,就会通过 weak 引用访问 Traveler。演讲指出,今天这段代码可能打印成功,但那只是 observed lifetime(观察到的生命周期)刚好够长。编译器优化后,Traveler 可能在调用 account.printSummary() 前已经释放。
final class Traveler {
var name: String
var account: Account?
init(name: String) {
self.name = name
}
}
final class Account {
weak var traveler: Traveler?
var points: Int
init(traveler: Traveler, points: Int) {
self.traveler = traveler
self.points = points
}
func printSummary() {
print("\(traveler!.name) has \(points) points")
}
}
func test() {
let traveler = Traveler(name: "Lily")
let account = Account(traveler: traveler, points: 1000)
traveler.account = account
account.printSummary()
}
test()
关键点:
Account.printSummary()通过traveler!访问 weak 引用。traveler.account = account是traveler的最后一次使用。account.printSummary()调用时,Traveler的最低保证生命周期已经结束。- 如果
Traveler已释放,traveler读出来是nil。 traveler!对nil强制解包会导致崩溃。
(11:14)把强制解包改成 optional binding 会让崩溃变成静默跳过。
final class Account {
weak var traveler: Traveler?
var points: Int
init(traveler: Traveler, points: Int) {
self.traveler = traveler
self.points = points
}
func printSummary() {
if let traveler = traveler {
print("\(traveler.name) has \(points) points")
}
}
}
关键点:
if let traveler = traveler会在 weak 引用为nil时跳过打印。- 代码不崩溃,测试可能更难发现问题。
- 业务结果仍然错了,因为摘要没有输出。
- session 把这种情况归为 silent bug(静默错误)。
withExtendedLifetime():显式延长对象生命周期
(11:45)Swift 提供 withExtendedLifetime()。它可以把对象生命周期延长到闭包结束,避免 weak 引用在调用过程中变成 nil。
final class Traveler {
var name: String
var account: Account?
init(name: String) {
self.name = name
}
}
final class Account {
weak var traveler: Traveler?
var points: Int
init(traveler: Traveler, points: Int) {
self.traveler = traveler
self.points = points
}
func printSummary() {
if let traveler = traveler {
print("\(traveler.name) has \(points) points")
}
}
}
func test() {
let traveler = Traveler(name: "Lily")
let account = Account(traveler: traveler, points: 1000)
traveler.account = account
withExtendedLifetime(traveler) {
account.printSummary()
}
}
test()
关键点:
withExtendedLifetime(traveler)告诉编译器延长traveler的生命周期。- 闭包内调用
account.printSummary()。 Account.printSummary()读取 weak 引用时,Traveler仍然存活。- 这个办法需要开发者记住所有危险调用点。
- 演讲把它称为易碎方案,维护成本会随代码库增长。
更稳的设计:只允许通过强引用访问对象
(12:55)更好的修复方式是改类的 API。把 printSummary() 放回 Traveler,把 Account 内部的 weak 引用隐藏起来。调用方只能通过强引用访问旅行者。
final class Traveler {
var name: String
var account: Account?
init(name: String) {
self.name = name
}
func printSummary() {
if let account = account {
print("\(name) has \(account.points) points")
}
}
}
final class Account {
private weak var traveler: Traveler?
var points: Int
init(traveler: Traveler, points: Int) {
self.traveler = traveler
self.points = points
}
}
func test() {
let traveler = Traveler(name: "Lily")
let account = Account(traveler: traveler, points: 1000)
traveler.account = account
traveler.printSummary()
}
test()
关键点:
private weak var traveler让外部代码无法把 weak 引用当访问入口。Traveler.printSummary()通过self读取name,当前方法调用持有强引用。account.points只读取账户数据,不需要反向读取旅行者。- API 迫使调用方使用
traveler.printSummary()。 - 生命周期正确性由类型设计保证,减少人工记忆。
避免引用环:把共享信息抽出来
(14:20)演讲给出的更彻底方式是重新建模。Account 需要的只是旅行者个人信息,不一定要反向持有整个 Traveler。把个人信息抽成 PersonalInfo 后,关系从环变成树。
final class PersonalInfo {
var name: String
init(name: String) {
self.name = name
}
}
final class Traveler {
var info: PersonalInfo
var account: Account?
init(info: PersonalInfo) {
self.info = info
}
}
final class Account {
var info: PersonalInfo
var points: Int
init(info: PersonalInfo, points: Int) {
self.info = info
self.points = points
}
}
func test() {
let info = PersonalInfo(name: "Lily")
let traveler = Traveler(info: info)
let account = Account(info: info, points: 1000)
traveler.account = account
print("\(account.info.name) has \(account.points) points")
}
test()
关键点:
PersonalInfo保存name,成为可共享的小对象。Traveler持有PersonalInfo和可选Account。Account持有同一个PersonalInfo,无需持有Traveler。traveler.account = account只形成单向关系。- 没有 weak 引用,生命周期错误的入口也消失了。
deinit 副作用:释放时间会影响外部结果
(15:23)deinit 会在对象释放前运行。它可以打印、发布指标、写全局状态。这些副作用让对象释放时间变得可观察。
final class Traveler {
var name: String
var destination: String?
init(name: String) {
self.name = name
}
deinit {
print("\(name) is deinitializing")
}
}
func test() {
let traveler1 = Traveler(name: "Lily")
let traveler2 = traveler1
traveler2.destination = "Big Sur"
print("Done traveling")
}
test()
关键点:
deinit在对象释放前执行。print("\(name) is deinitializing")是外部可见副作用。traveler2.destination = "Big Sur"是对象最后一次使用。print("Done traveling")不使用对象。deinit可能在Done traveling前后运行,代码不能依赖这个顺序。
(19:08)更稳的做法是把发布行为从 deinit 移到显式方法里,再用 defer 保证离开作用域时调用。deinit 只做校验。
final class TravelMetrics {
let id: UInt
var destinations = [String]()
var category: String?
var published = false
init(id: UInt) {
self.id = id
}
func computeTravelInterest() {
category = destinations.contains("Big Sur") ? "Nature" : nil
}
func publish() {
published = true
print("id: \(id), count: \(destinations.count), category: \(category ?? "nil")")
}
}
final class Traveler {
var name: String
var destination: String?
private var travelMetrics: TravelMetrics
init(name: String, id: UInt) {
self.name = name
self.travelMetrics = TravelMetrics(id: id)
}
func updateDestination(_ destination: String) {
self.destination = destination
travelMetrics.destinations.append(destination)
}
func publishAllMetrics() {
travelMetrics.computeTravelInterest()
travelMetrics.publish()
}
deinit {
assert(travelMetrics.published)
}
}
func test() {
let traveler = Traveler(name: "Lily", id: 1)
defer { traveler.publishAllMetrics() }
traveler.updateDestination("Big Sur")
traveler.updateDestination("Catalina")
}
test()
关键点:
TravelMetrics保存目的地、分类和是否已发布。computeTravelInterest()根据已记录目的地计算分类。publish()显式发布指标,并把published设为true。Traveler.updateDestination(_:)更新目的地,同时记录指标。publishAllMetrics()把计算和发布放在普通方法里。defer { traveler.publishAllMetrics() }保证test()退出前发布指标。deinit只检查指标是否已发布,不承担业务发布责任。
(19:50)Xcode 13 增加了实验性构建设置 Optimize Object Lifetimes。打开后,Swift 编译器会更积极地缩短对象生命周期,让观察到的生命周期更接近最低保证生命周期。这可能暴露原来隐藏的 weak/unowned 或 deinit 顺序错误。
// Xcode 13 Build Settings
// Optimize Object Lifetimes: Yes
关键点:
- 这是 Swift 编译器的构建设置。
- 打开后,对象更常在最后一次使用后立即释放。
- 依赖 observed lifetime 的代码更容易暴露问题。
- 修复方向仍然是上面三类:延长生命周期、改 API、移除 deinit 副作用。
核心启发
-
做什么:给现有项目做一次 weak/unowned 访问审计。
- 为什么值得做:session 指出 weak/unowned 让生命周期可观察,optional binding 可能把错误变成静默跳过。
- 怎么开始:搜索
weak var、unowned、!和if let,重点检查通过 weak 引用发起业务行为的代码,把入口改成强引用方法或用withExtendedLifetime()包住临界调用。
-
做什么:把对象销毁时发布指标的逻辑改成显式提交。
- 为什么值得做:deinit 副作用的顺序不稳定,编译器优化后可能提前发布未计算完成的数据。
- 怎么开始:把
deinit { publish() }改成publishAllMetrics(),在调用方用defer { object.publishAllMetrics() }保证退出前执行,deinit只保留assert(published)。
-
做什么:重构互相持有的模型对象。
- 为什么值得做:引用环需要 weak 打断,weak 又带来生命周期观察问题。演讲建议先思考能否避免环。
- 怎么开始:画出类之间的持有关系,找出双方都需要的数据,把它抽成
PersonalInfo这类共享值或小对象,让关系变成单向结构。
-
做什么:在 CI 或本地调试配置里打开
Optimize Object Lifetimes。- 为什么值得做:这个设置会更稳定地缩短对象生命周期,帮助提前暴露依赖 observed lifetime 的隐藏错误。
- 怎么开始:在 Xcode 13 的 Build Settings 中启用实验性
Optimize Object Lifetimes,重点跑覆盖 weak/unowned 和 deinit 发布逻辑的测试。
-
做什么:为内存泄漏写最小回归测试。
- 为什么值得做:引用环会让对象在局部引用消失后仍然存活,问题常在功能正常时被忽略。
- 怎么开始:为容易成环的模型创建和释放对象,配合 Xcode 内存工具或 XCTest 内存检查记录对象是否按预期释放;发现环后优先改数据结构,再考虑
weak。
关联 Session
- What‘s new in Swift — 了解 Swift 5.5 的语言更新,以及 async/await、结构化并发和 actors 的入口。
- Detect and diagnose memory issues — 学习用 Xcode、MetricKit 和 XCTest 发现内存问题与性能回归。
- Swift concurrency: Behind the scenes — 理解 Swift 编译器和运行时如何通过任务、线程池和优化影响程序行为。
- Protect mutable state with Swift actors — 从共享可变状态的角度继续学习 Swift 的安全设计。
评论
GitHub Issues · utterances