Swift: Swift 闭包进阶:逃逸闭包、自动闭包与循环引用

闭包在异步编程中扮演着核心角色,但也带来了生命周期管理和内存泄漏等挑战。本课深入闭包的高级用法,让你在复杂的应用场景中安全高效地使用闭包。

1. 你将学到


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 标记的闭包可以逃出函数作用域。

100%
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 自动将表达式包装成闭包,实现延迟求值

100%
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):

100%
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
数据已获取

💡 提示: 输出说明 ViewControllerNetworkManager 都被正确释放了。闭包中的 [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 优先用 weakweak 将引用变为可选类型,安全但需要解包。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]。这是内存安全的重要设计。

📖 小节


📝 作业

  1. 基础题: 写一个 delayPrint 函数,接收一个 String@escaping () -> Void 闭包,用 DispatchQueue.main.asyncAfter 延迟 1 秒后执行闭包。调用时传入字符串和打印该字符串的闭包。
  2. 进阶题: 写一个 Logger 类,包含一个 log(_ message: @autoclosure () -> String) 方法,在 isEnabled 为 true 时才打印消息。演示延迟求值的效果。
  3. 挑战题: 创建一个 TaskManager 类,内部存储一个 [() -> Void] 逃逸闭包数组。提供 addTask(_:)executeAll() 方法。在 ViewController 中使用它,确保 ViewController 被释放时不会造成内存泄漏(需使用捕获列表)。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏