在 Swift 中,@escaping 用于标记闭包参数,表明该闭包在函数执行完之后 仍可能被调用。换句话说,带有 @escaping 标记的闭包会在函数作用域之外执行,比如在异步操作、回调中使用的闭包通常是 @escaping 的。
为什么需要 @escaping
默认情况下,闭包参数是非逃逸(non-escaping)的,意味着闭包会在函数执行完毕之前被调用。如果要在函数返回后延迟调用闭包(例如异步网络请求的回调),需要将闭包声明为 @escaping,否则编译器会报错。
使用 @escaping 的场景
1、异步操作:比如网络请求、数据库查询等,需要延迟执行的操作。
2、存储闭包供后续调用:如果需要将闭包存储在一个变量或数组中,以便在函数返回后再调用,这种情况下也需要 @escaping。
这里是一个带有 @escaping 闭包的简单示例:
func fetchData(completion: @escaping (String) -> Void) {
DispatchQueue.global().async {
// 模拟耗时的网络请求
sleep(2) // 模拟延迟
let result = "网络请求完成,获取数据成功"
// 调用完成后的回调,把结果传给调用者
completion(result)
}
}
fetchData { result in
print(result)
// 更新 UI,例如显示获取的数据
print("在主线程更新 UI")
}
在这个例子中:
completion 闭包参数被标记为 @escaping,因为闭包会在 performAsyncTask 返回后被调用。
DispatchQueue.global().async 创建了一个异步任务,2秒后执行 completion 闭包。这种情况下,闭包必须是 @escaping。
理解 completion 的作用
fetchData 是一个异步函数,它可能需要 2 秒才能获取数据。但我们不希望阻塞主线程。
completion 闭包的作用是:在异步任务完成后执行一些额外操作。在这个例子中,就是将获取的数据更新到 UI 上。
如果不调用 completion,我们不会知道数据获取何时完成,UI 就无法更新。这种情况下,completion 是一种通知机制,让调用者知道异步任务已经完成,并且可以根据需求执行特定操作。
例如,我们根据上面的代码,再添加一个sleep(2):
func fetchData(completion: @escaping (String) -> Void) {
DispatchQueue.global().async {
// 模拟耗时的网络请求
sleep(2) // 模拟延迟
let result = "网络请求完成,获取数据成功"
sleep(2) // 模拟延迟
// 调用完成后的回调,把结果传给调用者
completion(result)
}
}
这段代码会先延迟2秒,然后定义result,再延迟两秒钟。最后调用completion(result)方法进行回调。
fetchData { result in
print(result)
// 更新 UI,例如显示获取的数据
print("在主线程更新 UI")
}
当completion(result)回调完成后,我们才能看到fetchData输出 result 以及“在主线程更新UI”的提示。
画图理解流程
首先,这是我们的fetchData方法以及调用fetchData的代码。我们需要明白fetchData方法的completion是一个闭包:
completion: @escaping (String) -> Void
这个闭包表示一个以String为参数,返回Void的闭包。因此,我们在调用的时候,是将fetchData { result in …}传递给 completion参数:
将这个闭包传递给completion参数,注意的是,闭包的类型与completion参数 (String) -> Void 一致。
然后在fetchData内部开始调用异步任务。
在异步任务执行完毕后,completion(result)会被调用。
Completion(result)实际上是调用了fetchData调用者传入的闭包 {result in …},并将result参数传递给它。
fetchData { result in // result 为 变量 result
print(result) // "网络请求完成,获取数据成功"
// 更新 UI,例如显示获取的数据
print("在主线程更新 UI")
}
后,completion(result)调用的就是fetchData闭包,在 { result in … } 闭包内部,result 是传入的字符串(即 “网络请求完成,获取数据成功”)。
print(result) 会输出这个字符串。
@escaping 的必要性
在 Swift 中,闭包默认是 非逃逸(non-escaping)的,这意味着闭包会在函数内部执行完毕,不会离开函数的作用范围。而 @escaping 标记的闭包表示这个闭包可能在函数返回之后才会执行。
由于 completion 闭包是在异步任务内调用的,而这个任务在 fetchData 返回后还在执行中,意味着闭包没有在 fetchData 函数内完成调用,而是“逃逸”到了函数之外。
@escaping 的关键在于函数作用域和生命周期管理。在这里,fetchData函数会立即返回,但异步任务在全局队列中执行,可能需要一段时间才能完成。
因为闭包会在异步任务完成后才被调用,这意味着闭包的生命周期要比 fetchData的生命周期更长。使用 @escaping 明确指出这一点,确保闭包在异步任务中能正常存活并执行,而不会被过早释放。
为什么complection会被调用?
在 fetchData 函数中,completion 被作为参数传入,所以在函数内部,completion 可以像普通变量一样被使用。这意味着当 fetchData 被调用时,可以直接在函数体中通过 completion() 来调用该闭包。
因为 completion 是作为参数传入的闭包,我们可以在异步任务完成后直接调用它,类似于调用一个函数来执行传入的操作。
@escaping 和非 @escaping 的区别
非逃逸闭包:闭包在函数执行完之前就会被调用。这是闭包的默认行为。
逃逸闭包(@escaping):闭包可以在函数返回之后被调用。
总结
@escaping 表示闭包逃逸出当前函数,在函数返回之后还可能会被执行。它适用于需要延迟执行、异步操作、或在函数返回后调用闭包的情况。
同时确保闭包生命周期延长,允许在函数返回后调用它,从而避免函数返回后闭包被释放。