Task
Swift
中的Task
是一种异步操作,它提供了一种替代DispatchQueue.main.async{}
等传统方法的方法。通过使用Task
,我们可以简化代码并更好地控制异步操作。此外,Task还提供了其他选项,可以进一步增强任务执行。
先看一个Task
的基本使用:
class TaskViewModel: ObservableObject {@Published var image: UIImage? = nilfunc fetchImage() async {do {guard let url = URL(string: "https://picsum.photos/1000") else { return }let (data, _) = try await URLSession.shared.data(from: url)await MainActor.run {self.image = UIImage(data: data)print("---> 图片请求成功!")}} catch {print("\(error.localizedDescription)")}}
}struct TaskViewDemo: View {@StateObject private var viewModel = TaskViewModel()var body: some View {VStack {if let image = viewModel.image {Image(uiImage: image).resizable().scaledToFit().frame(width: 200, height: 200)}}.onAppear {Task {await viewModel.fetchImage()}}}
}
上面代码中我们在TaskViewModel
中请求一张图片,然后在View
中显示,请求是在onAppear
中使用了Task
,前面的文章中也早就涉及过。
现在如果再请求一张图片,在同一个Task
中请求两个图片,比如下面的代码:
class TaskViewModel: ObservableObject {@Published var image: UIImage? = nil@Published var image2: UIImage? = nilfunc fetchImage() async {do {guard let url = URL(string: "https://picsum.photos/1000") else { return }let (data, _) = try await URLSession.shared.data(from: url)await MainActor.run {self.image = UIImage(data: data)print("---> 图片请求成功!")}} catch {print("\(error.localizedDescription)")}}func fetchImage2() async {do {guard let url = URL(string: "https://picsum.photos/1000") else { return }let (data, _) = try await URLSession.shared.data(from: url)await MainActor.run {self.image2 = UIImage(data: data)print("---> 图片请求成功!")}} catch {print("\(error.localizedDescription)")}}
}struct TaskViewDemo: View {@StateObject private var viewModel = TaskViewModel()var body: some View {VStack {if let image = viewModel.image {Image(uiImage: image).resizable().scaledToFit().frame(width: 200, height: 200)}if let image = viewModel.image2 {Image(uiImage: image).resizable().scaledToFit().frame(width: 200, height: 200)}}.onAppear {Task {await viewModel.fetchImage()await viewModel.fetchImage2()}}}
}
在同一个Task
里面又添加了一个图片的请求,那么执行顺序是怎么样的呢?相比大家也猜到了,第二个会在第一个请求回来后再去请求,毕竟有await
在那呢。
现在对代码做一下改动,分别用两个Task
去请求两个图片,调用如下:
.onAppear {Task {await viewModel.fetchImage()}Task {await viewModel.fetchImage2()}
}
从效果上看,两个Task
之间是不存在相互依赖的关系的,两张图片几乎是同一时间请求回来的。
TaskPriority
在 SwiftUI
中,Task(priority:operation:)
是用于创建并启动异步任务的一种方式,其中 TaskPriority
参数用于指定任务的优先级。优先级决定了系统在调度和执行任务时如何分配资源,特别是在多任务执行时,优先级较高的任务可能会优先获得执行机会。
TaskPriority
是一个枚举类型,定义了任务的优先级。它包含以下几种常见的优先级选项:
.high
:高优先级任务,通常用于需要立即响应的任务。例如,用户界面的更新任务。.medium
:中等优先级任务,适用于一般任务。默认情况下,许多任务会使用此优先级。.low
:低优先级任务,适用于可以延迟执行的任务。例如,后台数据同步或预加载任务。.background
:后台任务优先级,用于那些不需要用户立即注意的任务。适用于后台处理或预加载数据。.userInitiated
:用户发起的任务优先级,通常用于用户直接触发的任务,比如用户按下按钮后启动的操作。优先级略高于.medium
。.utility
:实用任务优先级,适用于需要一定时间完成,但不需要立即响应的任务。比如下载数据或处理文件。
上面的图片中,我们用代码将每个优先级都打印了一下,通过数字可以更好的看出优先级顺序。
另外这里还要强调一下,不是说高优先级的一定会最先去执行,最终的调度仍然由系统管理。系统会根据当前的负载、资源可用性等因素综合决定任务的执行顺序。
在一个Task
中再调用一个Task
,那么子Task
的优先级会继承父Task
的优先级。
当不指定优先级的时候,默认优先级是high。现在我们指定一个并在内部再添加一个Task看看。
如果子Task
未指定优先级,那么就会继承父一级的优先级。如果指定了就不继承了,比如下图:
如果子Task
又不想继承父Task
优先级,又不想指定优先级,那么可以用
Task.detached {print("child priority : \(Task.currentPriority)")
}
效果如下:
不过关于Task.detached方法
,苹果也有特别的说明,如下:
/// Don’t use a detached task if it’s possible
/// to model the operation using structured concurrency features like child tasks.
/// Child tasks inherit the parent task’s priority and task-local storage,
/// and canceling a parent task automatically cancels all of its child tasks.
/// You need to handle these considerations manually with a detached task.
/// You need to keep a reference to the detached task
/// if you want to cancel it by calling theTask.cancel()
method.
/// Discarding your reference to a detached task
/// doesn’t implicitly cancel that task,
/// it only makes it impossible for you to explicitly cancel the task.
从第一句话就可以看出苹果是不推荐用这个Task.detached
方法的。
cancel(取消任务)
Task.cancel()
是 Task
中的一个方法,用于请求取消正在运行的任务。了解如何使用 Task.cancel()
非常重要,尤其是在处理长时间运行的异步操作时,确保可以在需要时安全地停止这些操作。
Task.cancel()
是一个实例方法,用于标记任务为取消状态。当你调用 cancel()
方法时,你实际上是在通知任务:“你应该尽快停止工作。” 但是需要注意的是,调用 cancel()
并不会立即终止任务,而是设置一个取消标志(isCancelled
属性变为 true
),然后任务内部代码需要定期检查这个标志,并自行决定是否中止任务。
如何正确使用 Task.cancel()
呢
- 启动任务:
可以使用Task { ... }
来启动一个异步任务。 - 请求取消:
当需要取消任务时,可以调用cancel()
方法。 - 任务响应取消:
任务内部应定期检查Task.isCancelled
属性,确保在任务被取消时能够及时响应,并终止正在执行的操作。
还是接着上面的代码说,我们将上面的代码放到导航栏的第二页,由主页面push出来,到第二个界面后请求图片的时候加上5秒延迟,并在onDisappear
方法中调用cancel()
方法,然后未到5秒的时候返回到第一个界面。代码如下:
func fetchImage() async {try? await Task.sleep(nanoseconds: 5_000_000_000)do {guard let url = URL(string: "https://picsum.photos/1000") else { return }let (data, _) = try await URLSession.shared.data(from: url)await MainActor.run {self.image = UIImage(data: data)print("---> 图片请求成功!")}} catch {print("\(error.localizedDescription)")}
}
上面代码添加了5秒的延迟。
struct TaskViewDemo: View {@StateObject private var viewModel = TaskViewModel()@State private var fetchImageTask: Task<(), Never>? = nilvar body: some View {VStack {if let image = viewModel.image {Image(uiImage: image).resizable().scaledToFit().frame(width: 200, height: 200)}}.onAppear {fetchImageTask = Task {await viewModel.fetchImage()}}.onDisappear {fetchImageTask?.cancel()}}
}
上面代码中定义了一个fetchImageTask
实例变量,用来持有Task
实例,并在onDisappear
方法调用cancel()
方法。
很明显,在未到5秒的时候返回,控制台能看到系统的已取消输出。
但是实际开发中很少有这种等几秒的情况,如果在ViewModel中有循环处理事件,那么就要判断一下当前的任务是否已经取消了
Task.checkCancellation()
调用checkCancellation()
方法后,如果任务中途取消了,那么该方法将抛出异常,代码块也不会再继续执行了。调用时如果不想处理异常错误,直接使用try?
即可:
try? Task.checkCancellation()
简单示例如下:
for work in workArray {// 检查是否已经取消try? Task.checkCancellation()// 任务代码// ......
}
还要说一下,定义Task
实例的时候,因为Task
是泛型,需要指定Success
和Failure
。
@frozen public struct Task<Success, Failure> : Sendable where Success : Sendable, Failure : Error {
}
.task(SwiftUI)
.task
修饰符用于在视图的生命周期中启动一个异步任务。这些任务会在视图出现时自动启动,并且可以与 Swift
的并发功能一起使用,如 async
和 await
。
.task(priority: TaskPriority? = nil, _ action: @escaping () async -> Void) -> some View
priority
:可选的任务优先级。可以是.low, .medium, .high, .userInitiated,.background
等。默认为 nil,表示使用系统默认优先级。action
:在视图出现时执行的异步代码块。
使用场景
- 视图加载时初始化数据:在视图加载时,使用
.task
修饰符来启动异步数据加载或初始化任务。 - 执行后台操作:启动一些不影响 UI 但需要后台处理的任务,例如分析数据、同步远程服务器等。
比如刚才的示例中:
.task {await viewModel.fetchImage()
}
.task
修饰符使得在 SwiftUI 中处理异步任务变得更加简洁和直观,特别是在与 Swift 的并发功能结合使用时,能够轻松处理复杂的异步操作。
注意事项
- 任务取消:
.task
启动的任务会在视图被移除时自动取消。因此,如果视图在任务完成之前被移除,该任务将不会继续运行。 - 与
.onAppear
的区别:.task
类似于.onAppear
,但.task
专门用于启动异步任务,并且能够更好地处理Swift
并发模型。.onAppear
通常用于启动同步代码或不需要异步处理的逻辑。 - 任务优先级:可以使用
priority
参数来设置任务的优先级,但这通常在需要优化性能或资源分配时才需要考虑。
小结
本篇文章主要介绍了Task
的基本使用,优先级,以及取消等功能,在SwiftUI
中也可以使用.task
修饰符来代替Task
,而且视图销毁的时候还能自动cancel
任务。关于Task
和.task
的使用,想必大家通过这篇文章也有所了解了。感谢大家的阅读。
最后,希望能够帮助到有需要的朋友,如果觉得有帮助,还望点个赞,添加个关注,笔者也会不断地努力,写出更多更好用的文章。