Swift: Swift 闭包进阶:逃逸闭包、自动闭包与循环引用
闭包在异步编程中扮演着核心角色,但也带来了生命周期管理和内存泄漏等挑战。本课深入闭包的高级用法,让你在复杂的应用场景中安全高效地使用闭包。
1. 你将学到
@escaping逃逸闭包的使用场景与生命周期@autoclosure自动闭包实现延迟求值- 闭包捕获列表及其解决循环引用的原理
- 通过
weak和unowned避免内存泄漏 - 理解闭包的值捕获时机
2. 一个移动开发者的真实故事
(1) 痛点:网络回调崩溃且内存泄漏
Charlie 在开发一个社交 App,用户发表评论后需要调用 API 并刷新 UI:
SWIFT
func postComment(text: String, onComplete: () -> Void) {
// 模拟网络请求
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// 编译错误!非逃逸闭包不能在异步回调中使用
onComplete()
}
}
编译器报错——闭包在 postComment 返回后才执行,但 onComplete 默认是非逃逸的。更糟的是,即使用了逃逸闭包,他遇到了内存泄漏:视图控制器被销毁后,闭包依然持有它的引用,导致泄漏。
(2) @escaping + 捕获列表的解法
SWIFT
// 标记为逃逸闭包,允许异步执行
func postComment(text: String, onComplete: @escaping () -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
guard let self = self else { return }
self.showSuccess()
onComplete()
}
}
三个改动:加 @escaping 标记逃逸、加 [weak self] 弱引用、用 guard let 安全解包。
(3) 收益:安全的内存管理
| 维度 | 改造前 | 改造后 |
|---|---|---|
| 异步回调 | 编译错误 | 正常运行 |
| 内存泄漏 | ViewController 无法释放 | weak self 自动断引用 |
| 崩溃风险 | 回调时对象已释放 | guard let 检测后安全返回 |
| 代码意图 | 闭包生命周期隐式 | @escaping 明确声明逃逸语义 |
3. 逃逸闭包 @escaping
默认情况下,闭包参数是非逃逸的——它们在函数返回前执行完毕。用 @escaping 标记的闭包可以逃出函数作用域。
sequenceDiagram
participant Caller as 调用者
participant Func as 函数
participant Closure as 闭包
Caller->>Func: 传入闭包
Note over Func: 非逃逸:函数内执行完毕
Caller->>Func: 传入 @escaping 闭包
Func->>Closure: 存储到外部变量
Func-->>Caller: 函数返回
Note over Closure: 函数返回后仍可调用
Caller->>Closure: 在异步回调中执行
| 特性 | 非逃逸 | @escaping |
|---|---|---|
| 执行时机 | 函数返回前 | 函数返回前后均可 |
| 存储到外部 | ❌ 不允许 | ✅ 可存储到变量/属性 |
self. 隐式引用 |
✅ 可省略 | ⚠️ 必须显式写 |
| 编译器优化 | 可省略内存管理开销 | 需 ARC 管理 |
| 性能 | 更优 | 稍有额外开销 |
▶ 示例:逃逸闭包的使用
SWIFT
// ============================================
// 逃逸闭包与非逃逸闭包的对比
// ============================================
import Foundation
var completionHandlers: [() -> Void] = []
// 非逃逸闭包——函数内同步执行
func syncOperation(task: () -> Void) {
print("开始同步任务")
task()
print("同步任务结束")
}
syncOperation {
print(" 执行中...")
}
// 逃逸闭包——存储到外部数组
func asyncOperation(task: @escaping () -> Void) {
print("添加异步任务")
completionHandlers.append(task) // 如果不加 @escaping 会报错
}
asyncOperation {
print(" 异步任务执行")
}
print("函数已返回,闭包尚未执行")
// 稍后执行存储的闭包
completionHandlers.first?()
输出:
TEXT 📖 仅展示开始同步任务 执行中... 同步任务结束 添加异步任务 函数已返回,闭包尚未执行 异步任务执行
4. 自动闭包 @autoclosure
@autoclosure 自动将表达式包装成闭包,实现延迟求值:
graph TB
A["assert(condition: 2 > 1)"] --> B["正常求值:立即计算"]
C["assert(condition: 2 > 1, message: \"error\")"] --> D["@autoclosure:仅在 condition 为 false 时求值"]
D --> E["避免字符串拼接开销"]
| 场景 | 普通参数 | @autoclosure |
|---|---|---|
| 求值时机 | 调用时立即求值 | 仅在闭包被调用时求值 |
| 性能优化 | ❌ 总是计算 | ✅ 按需计算 |
| 语法 | 需写 { } 闭包 |
写普通表达式即可 |
▶ 示例:自动闭包的延迟求值
SWIFT
// ============================================
// @autoclosure 实现延迟日志输出
// ============================================
var debugEnabled = false
func log(_ message: @autoclosure () -> String) {
if debugEnabled {
print("[DEBUG] \(message())")
} else {
print("日志未开启,跳过(消息未求值)")
}
}
// 即使拼接字符串开销很大,不开启时也不会执行
debugEnabled = false
log("expensive " + "string " + "operation " + "skipped")
debugEnabled = true
log("this " + "will " + "be " + "logged")
输出:
TEXT 📖 仅展示日志未开启,跳过(消息未求值) [DEBUG] this will be logged🔥 易错:
@autoclosure很容易掩盖性能问题。仅在确实需要延迟求值时使用,不要在公共 API 中滥用。过度使用会降低代码可读性。
5. 捕获列表与循环引用
(1) 强引用循环
闭包捕获外部变量时,默认是强引用。当闭包和对象相互持有对方时,会产生强引用循环(retain cycle):
graph TB
A[ViewController] -->|强引用| B[闭包属性]
B -->|强引用| A
C[双方互相持有 → 内存泄漏]
A --> C
B --> C
(2) 捕获列表语法
在闭包的 { } 开始处使用 [ ] 声明捕获列表:
| 声明 | 含义 | 适用场景 |
|---|---|---|
[weak self] |
弱引用,self 变为可选类型 |
最常用,安全 |
[unowned self] |
无主引用,self 不是可选但需保证存在 |
确定 self 不会为 nil 时 |
[weak delegate = self.delegate] |
捕获表达式 | 捕获特定属性而非 self |
▶ 示例:循环引用与解决方案
SWIFT
// ============================================
// 闭包循环引用:weak vs unowned
// ============================================
import Foundation
class NetworkManager {
var onComplete: (() -> Void)?
func fetchData() {
// 模拟异步请求——逃逸闭包
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
guard let self = self else { return }
print("数据已获取")
self.onComplete?()
}
}
deinit {
print("NetworkManager deinit")
}
}
class ViewController {
let manager = NetworkManager()
var data: String?
func loadData() {
// 使用捕获列表避免循环引用
manager.onComplete = { [weak self] in
guard let self = self else { return }
self.data = "New data"
print("UI 已更新")
}
manager.fetchData()
}
deinit {
print("ViewController deinit")
}
}
// 模拟使用和销毁
var vc: ViewController? = ViewController()
vc?.loadData()
// 释放视图控制器
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
print("释放 ViewController")
vc = nil // 不会被泄漏
}
输出:
TEXT 📖 仅展示释放 ViewController NetworkManager deinit ViewController deinit 数据已获取💡 提示: 输出说明
ViewController和NetworkManager都被正确释放了。闭包中的[weak self]确保了不产生循环引用。asynAfter的回调执行时self已经是 nil,所以"UI 已更新"不会被打印——这正是我们想要的安全行为。
▶ 示例:闭包属性的常见陷阱
SWIFT
// ============================================
// 闭包属性中的循环引用
// ============================================
class Counter {
var value = 0
var incrementHandler: (() -> Void)?
func setup() {
// 错误:闭包持有 self,self 持有 incrementHandler → 循环引用
incrementHandler = { [weak self] in
self?.value += 1
}
}
deinit {
print("Counter deinit")
}
}
var counter: Counter? = Counter()
counter?.setup()
print("释放前 value = \(counter?.value ?? 0)")
counter = nil // 正确释放
print("已释放")
输出:
TEXT 📖 仅展示释放前 value = 0 Counter deinit 已释放
6. 完整示例:安全的异步图片加载器
SWIFT
// ============================================
// 完整示例:异步图片加载器
// 功能:@escaping + 捕获列表 + 内存安全
// ============================================
import Foundation
// 1. 图片缓存
class ImageCache {
private var cache: [String: Data] = [:]
func get(_ key: String) -> Data? { return cache[key] }
func set(_ key: String, data: Data) { cache[key] = data }
deinit { print("ImageCache deinit") }
}
// 2. 图片加载器(使用逃逸闭包)
class ImageLoader {
let cache = ImageCache()
func loadImage(from url: String, completion: @escaping (Data?) -> Void) {
// 检查缓存
if let cached = cache.get(url) {
completion(cached)
return
}
// 模拟网络请求——逃逸闭包异步执行
DispatchQueue.global().asyncAfter(deadline: .now() + 1) { [weak self] in
guard let self = self else {
// self 已释放,安全返回
completion(nil)
return
}
// 模拟下载数据
let mockData = Data([0x01, 0x02, 0x03])
self.cache.set(url, data: mockData)
DispatchQueue.main.async {
completion(mockData)
}
}
}
deinit { print("ImageLoader deinit") }
}
// 3. 使用(安全释放测试)
var loader: ImageLoader? = ImageLoader()
loader?.loadImage(from: "https://example.com/photo.jpg") { data in
if let _ = data {
print("图片加载成功")
}
}
// 立即释放——闭包中的 weak self 保证不崩溃
loader = nil
print("Loader 已释放,异步回调不会崩溃")
// 保持运行等待异步完成
RunLoop.main.run(until: Date(timeIntervalSinceNow: 2))
输出:
TEXT 📖 仅展示Loader 已释放,异步回调不会崩溃 ImageLoader deinit ImageCache deinit
❓ 常见问题
Q 什么时候必须用 @escaping?
A 当闭包被存储到外部变量(如数组、属性),或在异步回调(DispatchQueue、URLSession)中使用时,必须标记 @escaping。编译器会强制检查。
Q weak 和 unowned 如何选择?
A 优先用
weak。weak 将引用变为可选类型,安全但需要解包。unowned 假设对象永不释放,如果对象提前释放会崩溃。除非 100% 确定 self 生命周期长于闭包,否则别用 unowned。Q @autoclosure 会导致强引用循环吗?
A 可能。因为 @autoclosure 会隐式捕获外部变量。例如
func foo(_ closure: @autoclosure () -> Void) 调用时如果传入了 self.someMethod(),闭包会持有 self。建议谨慎使用。Q 非逃逸闭包为什么不需写 [weak self]?
A 非逃逸闭包在函数返回前已执行完毕,不会超过 self 的生命周期。Swift 编译器知道这一点,所以允许隐式引用 self。这也是非逃逸闭包的安全优势之一。
Q 逃逸闭包中的 self 必须显式写吗?
A 是的。
@escaping 闭包中引用 self 必须显式写出,这是 Swift 强制要求的语法提醒——提醒你考虑是否需要 [weak self]。这是内存安全的重要设计。📖 小节
@escaping标记闭包可以逃出函数作用域,用于异步回调和闭包存储@autoclosure将表达式自动包装成闭包,实现延迟求值- 捕获列表
[weak self]防止闭包和对象之间的强引用循环 weak将引用变为可选类型,需要解包使用;unowned假定对象永不释放- 非逃逸闭包可以安全隐式引用 self,逃逸闭包必须显式引用
- 使用
guard let self = self模式安全解包弱引用
📝 作业
- 基础题: 写一个
delayPrint函数,接收一个String和@escaping () -> Void闭包,用DispatchQueue.main.asyncAfter延迟 1 秒后执行闭包。调用时传入字符串和打印该字符串的闭包。 - 进阶题: 写一个
Logger类,包含一个log(_ message: @autoclosure () -> String)方法,在isEnabled为 true 时才打印消息。演示延迟求值的效果。 - 挑战题: 创建一个
TaskManager类,内部存储一个[() -> Void]逃逸闭包数组。提供addTask(_:)和executeAll()方法。在ViewController中使用它,确保ViewController被释放时不会造成内存泄漏(需使用捕获列表)。