WWDC Quick Look 💓 By SwiftGGTeam
ARC in Swift: Basics and beyond

ARC in Swift: Basics and beyond

观看原视频

Highlight

Apple 解释了 Swift 自动引用计数(ARC)如何按最后一次使用管理对象生命周期,并给出 weak/unowned 引用、withExtendedLifetime()、API 重设计和去除 deinit 副作用的安全写法来避免隐藏内存错误。

核心内容

Swift 的类是引用类型。一个 Traveler 对象可以被 traveler1traveler2 同时指向。开发者写的是普通赋值,编译器在背后插入 retainrelease

麻烦从“对象到底什么时候释放”开始。Swift 的对象生命周期以使用为基础。最低保证生命周期从初始化开始,到最后一次使用结束。它不保证一直活到右花括号。

大多数代码不关心这个细节。对象早一点释放或晚一点释放,结果相同。

问题出现在两个地方。第一,weakunowned 引用会让生命周期变得可观察。对象一旦释放,weak 读出来是 nilunowned 访问会 trap。第二,deinit 可以有副作用,例如打印日志、发布指标、写入全局状态。副作用发生的时间会影响程序结果。

Apple 在这场 session 里做的事情很直接:先说明 ARC 的最低保证,再展示哪些语言特性会暴露生命周期,最后给出三类修复方式。轻量方式是 withExtendedLifetime()。更稳的方式是改 API,让外部只能通过强引用访问对象。最彻底的方式是避免引用环,或者把 deinit 副作用移到显式方法里。

详细内容

ARC 的最低保证:对象活到最后一次使用

01:49)ARC(Automatic Reference Counting,自动引用计数)由 Swift 编译器驱动。编译器插入 retainrelease。运行时引用计数归零后,对象可以被释放。

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 指向 AccountAccount 又指回 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)常见修复方式是在环的一边使用 weakweak 不参与引用计数。被引用对象释放后,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 = accounttraveler 的最后一次使用。
  • 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:23deinit 会在对象释放前运行。它可以打印、发布指标、写全局状态。这些副作用让对象释放时间变得可观察。

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 varunowned!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

评论

GitHub Issues · utterances