第28章 循环引用:weak、unowned 与闭包捕获

第二十八章:循环引用:weak、unowned 与闭包捕获

两个对象互相拉着对方的手,谁都不肯先走,内存就会一直涨。这不是玄学,而是 ARC 的必然结果。解决循环引用的工具只有几个,但每个都有明确的使用条件,弄清楚之后再也不会“看到 self 就加 weak”。

28.1 强引用环的形成

两个对象互相强引用,就变成了“你不走我也不走”。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
final class Person {
    var apartment: Apartment?
    deinit { print("Person 释放") }
}

final class Apartment {
    var tenant: Person?
    deinit { print("Apartment 释放") }
}

do {
    let person = Person()
    let apartment = Apartment()
    person.apartment = apartment
    apartment.tenant = person
}
// 这一段跑完什么都不打印:两个 deinit 都没有被触发

person 持有 apartment,apartment 也持有 person。离开作用域后,外部引用消失了,但两者仍互相强引用,引用计数都不为零,所以 deinit 从未执行。

28.2 weak:自动变 nil 的引用

把其中一边改成 weak:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
final class Person {
    var apartment: Apartment?
    deinit { print("Person 释放") }
}

final class Apartment {
    weak var tenant: Person?
    deinit { print("Apartment 释放") }
}

do {
    let person = Person()
    let apartment = Apartment()
    person.apartment = apartment
    apartment.tenant = person
}
// prints: Person 释放
// prints: Apartment 释放

释放顺序可能因实现和上下文略有不同,但两个对象都能被释放。

weak 的规则:

  • 只能用于类实例。
  • 必须是 var。
  • 类型必须是可选值,因为对象释放后引用自动变成 nil。

弱引用不会增加引用计数。它适合“我的存在不决定对方是否存在的”关系,例如委托、父节点指向子节点、观察者。

28.3 委托模式:弱引用最经典的舞台

委托(delegate)是弱引用最正经的用武之地:协议要求“遵循者是类”,持有方用 weak 存它,于是被委托的对象不会因为“被持有”而活不下来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
protocol DownloadDelegate: AnyObject {
    func didFinish()
}

final class Downloader {
    weak var delegate: DownloadDelegate?

    func finish() {
        delegate?.didFinish()
    }
}

final class ViewController: DownloadDelegate {
    func didFinish() { print("UI 更新") }
}

let vc = ViewController()
let downloader = Downloader()
downloader.delegate = vc
downloader.finish()
// prints: UI 更新

委托通常不应该由下载器强持有,否则下载器死了、委托还在拉它,或者反过来形成环。weak 让这种关系自然单向。

28.4 unowned:确定对方不会先走

unowned 也不增加引用计数,但它不是可选值。对象释放后再访问会触发运行时错误:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
final class Owner {
    var pet: Pet?
    deinit { print("Owner 释放") }
}

final class Pet {
    unowned let owner: Owner
    init(owner: Owner) { self.owner = owner }
    deinit { print("Pet 释放") }
}

do {
    let owner = Owner()
    let pet = Pet(owner: owner)
    owner.pet = pet
}
// prints: Owner 释放
// prints: Pet 释放

这里 Pet 的生命周期不可能长于 Owner,因此可以使用 unowned。如果这个前提不成立,就应该用 weak。

还要注意:Owner 的 deinit 执行时,Pet 可能还短暂存在。因此不要在需要访问 owner 的清理逻辑使用这种写法;一旦生命周期关系没有那么确定,weak 更安全。

选择规则:可能为 nil 用 weak;逻辑上永远有值且确定生命周期用 unowned;不确定就回到 weak。

28.5 闭包捕获强引用

闭包会捕获它用到的 self:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
final class TimerLike {
    var count = 0
    var handler: (() -> Void)?

    func start() {
        handler = {
            self.count += 1
            print(self.count)
        }
    }

    deinit { print("TimerLike 释放") }
}

do {
    let timer = TimerLike()
    timer.start()
    timer.handler?()
}
// prints: 1
// 这里故意不打印释放,因为 timer 自己持有 handler,handler 又持有 timer

对象持有闭包,闭包又持有对象,循环就出现了。解决方式是捕获列表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
final class TimerLike {
    var count = 0
    var handler: (() -> Void)?

    func start() {
        handler = { [weak self] in
            guard let self else { return }
            self.count += 1
            print(self.count)
        }
    }

    deinit { print("TimerLike 释放") }
}

do {
    let timer = TimerLike()
    timer.start()
    timer.handler?()
}
// prints: 1
// prints: TimerLike 释放

[weak self] 让闭包不增加对象的引用计数。进入闭包后立刻 guard let self,后续代码可以像普通强引用一样写。

如果闭包和对象生命周期完全绑定,也可以写 [unowned self]。但一旦对象先释放,unowned 访问会崩溃,所以除非关系非常明确,优先 weak。

28.6 异步任务里的 self

异步闭包同样可能长期持有对象:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
final class Loader {
    var value = 0

    func load() {
        Task { [weak self] in
            guard let self else { return }
            self.value = 42
            print(self.value)
        }
    }
}

并发章节会继续讨论任务生命周期。这里的规则不变:只要闭包被保存或被任务持有,就要检查它是否强引用了 self。

28.7 本章小结

引用方式是否增加计数类型要求适用场景
强引用是任意默认所有权
weak否类、可选值、var可能先释放的关系、委托
unowned否类、非可选生命周期确定且不会先释放
捕获列表控制闭包捕获任意闭包打破闭包与对象之间的环

28.8 本章易错点速查

易错点正确理解
看到 self 就加 weak只有存在环或生命周期不确定时才有必要
用 weak 修饰非可选属性必须可选且是 var
把 unowned 当默认选择对象先释放会触发崩溃
闭包捕获 self 后忘记捕获列表可能导致对象永远无法释放
捕获列表里写 [weak self] 后直接访问属性必须先解包 self
认为循环引用只会出现在两个对象之间对象—闭包—对象也是经典环路

28.9 下章预告

下一章从内存安全进入所有权:独占访问、borrowing、consuming、不可复制类型和不可逃逸类型。它们是 Swift 6 系列中变化最快、也最能体现语言方向的部分。

最后修改 September 19, 2026: 更新 (3489033b1)