SC #8:取消 Task
Source: Dev.to
注意: 在 Swift 中,取消
Task并不保证执行会立即停止。每个Task必须手动检查是否已被取消(例如,使用Task.checkCancellation()或Task.isCancelled)。
1. 基本示例:下载图片
func fetchImage() async throws -> UIImage? {
let imageTask = Task { () -> UIImage? in
let imageURL = URL(string: "https://httpbin.org/image")!
var imageRequest = URLRequest(url: imageURL)
imageRequest.allHTTPHeaderFields = ["accept": "image/jpeg"]
print("Starting network request...")
let (imageData, _) = try await URLSession.shared.data(for: imageRequest)
return UIImage(data: imageData)
}
// La tarea se cancela inmediatamente después de ser creada.
imageTask.cancel()
// Esperamos el valor (o el error) de la tarea.
return try await imageTask.value
}
发生了什么
| 步骤 | 描述 |
|---|---|
| 1️⃣ | 将对 URLSession.shared.data(for:) 的调用包装在返回 UIImage? 的 Task 中。 |
| 2️⃣ | 使用 imageTask.cancel() 取消任务。 |
| 3️⃣ | 使用 try await imageTask.value 等待结果。 |
| 4️⃣ | 网络请求在取消生效之前就已启动,但 URLSession 会遵循取消信号并中止请求。 |
控制台输出
Starting network request...
Image loading failed: Error Domain=NSURLErrorDomain Code=-999 "cancelled"
2. Task.checkCancellation()
Task.checkCancellation() 会在任务已被取消时自动抛出 CancellationError。
func fetchImage() async throws -> UIImage? {
let imageTask = Task { () -> UIImage? in
let imageURL = URL(string: "https://httpbin.org/image")!
// ...
try Task.checkCancellation() // 因为任务在调用 `checkCancellation()` 时已经被取消,网络请求不会执行。
}
}
3. Task.isCancelled
Task.isCancelled 允许在不抛出异常的情况下检查取消状态;我们可以手动处理。
func fetchImage() async throws -> UIImage? {
let imageTask = Task { () -> UIImage? in
let imageURL = URL(string: "https://httpbin.org/image")!
// ...
guard Task.isCancelled == false else {
print("Image request was cancelled")
return nil
}
let (imageData, _) = try await URLSession.shared.data(for: imageRequest)
// ...
}
imageTask.cancel()
return try await imageTask.value
}
输出
Image request was cancelled
4. 是否需要将 async 函数包装在 Task 中?
只有在我们想要取消操作时才需要显式创建 Task。如果不需要手动取消,可以直接编写不带包装器的函数:
func fetchImage() async throws -> UIImage? {
let imageURL = URL(string: "https://httpbin.org/image")!
var imageRequest = URLRequest(url: imageURL)
imageRequest.allHTTPHeaderFields = ["accept": "image/jpeg"]
try Task.checkCancellation()
let (imageData, _) = try await URLSession.shared.data(for: imageRequest)
try Task.checkCancellation()
return UIImage(data: imageData)
}
5. SwiftUI 中的手动取消
5.1 保存任务引用
@State private var image: UIImage?
@State private var imageDownloadingTask: Task?
var body: some View {
VStack {
if let image {
Image(uiImage: image)
} else {
Text("Loading...")
}
}
.onAppear {
imageDownloadingTask = Task {
do {
image = try await fetchImage()
print("Image loading completed")
} catch {
print("Image loading failed: \(error)")
}
}
}
.onDisappear {
imageDownloadingTask?.cancel()
}
}
5.2 使用 .task(priority:_: ) 修饰符
SwiftUI 在视图消失时会自动创建并取消任务。
@State private var image: UIImage?
var body: some View {
VStack {
if let image {
Image(uiImage: image)
} else {
Text("Loading...")
}
}
.task {
do {
image = try await fetchImage()
print("Image loading completed")
} catch {
print("Image loading failed: \(error)")
}
}
}
重要提示:
.task中的代码会在任何由.onAppear启动的代码之前执行。然而,任务的确切执行时机并不保证;.onAppear中的同步代码可能会在.task的任务之前执行。
6. 任务层级的取消(父任务 → 子任务)
当一个 父任务 被取消时,所有的 子任务 都会收到取消信号。每个子任务必须显式检查其状态(isCancelled 或 checkCancellation())。如果不检查,它们将继续执行。
示例
func parentTask() async {
let handler = Task {
print("Parent task started")
async let child1 = childTask(id: 1)
async let child2 = childTask(id: 2)
// Esperamos a que ambas subtareas terminen.
_ = try await [child1, child2]
}
// Simulamos algún trabajo y luego cancelamos.
try? await Task.sleep(nanoseconds: 200_000_000) // 0.2 s
handler.cancel()
do {
_ = try await handler.value
} catch let error as CancellationError {
print("La tarea fue cancelada")
} catch {
print("Hubo otro error")
}
print("Parent task finished")
}
func childTask(id: Int) async throws -> Int {
for i in 1..<4 {
try Task.checkCancellation() // <‑‑ verifica cancelación
try await Task.sleep(nanoseconds: 100_000_000) // 0.1 s
print("Subtarea: \(id), paso: \(i)")
}
print("Terminé la subtarea \(id)")
return id
}
可能的输出
- 如果父任务在子任务完成之前被取消,
childTask的每一次迭代都会抛出CancellationError,并且执行会停止。 - 如果某个子任务 未 检查取消状态,即使父任务已被取消,它仍会继续运行。
7. 快速概览
| 工具 | 功能 | 是否抛出异常? |
|---|---|---|
task.cancel() | 标记任务应停止。 | 否 |
Task.checkCancellation() | 如果任务已被取消,则抛出 CancellationError。 | 是 |
Task.isCancelled | 如果任务已被取消则返回 true。(在 guard/if 中使用) | 否 |
.task {} (SwiftUI) | 创建一个任务,当视图消失时会自动取消。 | 否(但内部任务可能会抛出) |
有了这些概念,你就可以 控制 并 优化 在 Swift 和 SwiftUI 应用中对异步操作的取消。祝编码愉快!
btarea: \(id)")
return id
}