在 Swift 开发中,闭包(Closure)是非常强大的特性,但当我们处理异步操作时,通常需要用到 逃逸闭包(@escaping)。今天通过几个简单的例子来看它是什么以及如何使用。
1. 什么是逃逸闭包?
如果一个闭包被作为一个参数传递给函数,并且在函数返回之后才被调用,那么这个闭包就称为“逃逸”了(即逃离了函数的作用域)。
在 Swift 中,如果闭包可能在函数执行结束后才执行,我们必须在函数定义的参数类型前标注 @escaping
2. 代码示例
不使用 @escaping(非逃逸,默认)
func doSomething(action: () -> Void) {
print("函数开始")
action() // 在函数返回前被执行
print("函数结束")
}
doSomething {
print("闭包执行")
}
// 输出: 函数开始 -> 闭包执行 -> 函数结束
使用 @escaping(逃逸)
当我们在网络请求或延迟操作中使用闭包时:
var completionHandlers: [() -> Void] = []
// 需要用 @escaping 标记,因为 action 会在函数外被存储
func perfoemAsyncTask(completion: @escaping () -> Void) {
//第一种情况:将闭包存储起来,在函数返回后某个时刻调用
self.completionHandlers = completion
// 第二种情况:异步操作:延迟 2 秒执行
print("开始网络请求...")
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
print("网络请求完成")
completion() // 此时函数 perfoemAsyncTask 已经结束,所以必须是逃逸的
}
}
如果去掉 @escaping,编译器会报错:“Closure is escaping but it is not marked @escaping”。逃逸闭包意味着闭包可能在任意时间被调用,因此开发者需要确保闭包内部引用的对象在调用时仍然有效,避免访问已释放的内存。
3. 常见使用场景
典型的场景是网络请求、文件 I/O 或耗时任务的回调:
func fetchData(from url: URL, completion: @escaping (Data?, Error?) -> Void) {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
// 这个闭包在数据返回后执行,发生在 fetchData 返回之后
completion(data, error)
}
task.resume()
}
将闭包作为属性存储,以便将来调用:
class EventHandler {
var onEvent: (() -> Void)?
func registerHandler(_ handler: @escaping () -> Void) {
onEvent = handler
}
}
4.使用注意事项
逃逸闭包最常见的陷阱是循环引用。当一个类持有逃逸闭包,而闭包内部又捕获了该类的实例(即 self),就会形成强引用循环。
class ViewController {
var request: URLSessionDataTask?
var completion: ((Data?) -> Void)?
func startRequest() {
request = URLSession.shared.dataTask(with: URL(string: "https://example.com")!) { [weak self] data, _, _ in
// 使用 weak self 打破循环引用
self?.completion?(data)
}
request?.resume()
}
}
在逃逸闭包中捕获 self 时,务必使用 [weak self] 或 [unowned self] 来打破可能的循环引用。