其它相关内容请见虚拟现实(VR)/增强现实(AR)&visionOS开发学习笔记
苹果系统借助现代处理器的多核可同步执行多条代码,提升同一时间内程序所能执行的任务。例如,一段代码从网上下载文件,另一段代码可以在屏幕上显示进度。此时,我们不能等待第一个执行完后再执行第二个,而必须要同步执行这两个任务。
要并行处理代码,系统将代码单元分组成任务。在Swift中,任务可以通过异步和并发编程实现。异步编程是一种编程模式,代码在完成任务前等待处理完成。这样系统可以在不同进程间共享计算资源。等待期间,系统可使用资源执行其它任务。而并发编程实现的代码可以利用多核同步执行多个任务。
图9-1:异步和并发编程
因很多应用可以同时运行,系统并不会对每个应用分配指定的核数。系统会创建一些执行线程,将任务分配给这些线程,然后根据可用资源决定哪个核执行哪些线程。在图9-1的示例中,左边是一个异步任务,从网上加载图片然后在屏幕上显示。在等待服务响应时,线程处于空闲状态可以执行其它任务,因此系统可以使用它执行更新进度条的任务。右图中创建了并发任务,因此在不同进程中同步执行。
任务
异步和并发的代码由任务定义。Swift标准库中包含有Task
结构体用于创建和管理这些任务。下面是结构体的初始化方法:
- Task(priority: TaskPriority?, operation: Closure):这个初始化方法创建并运行新任务。
priority
参数是一个辅助系统决定何时执行任务的结构体。这一结构体中包含类型属性定义标准优先级。当前有background
、high
、low
、medium
、userInitiated
和utility
。operation
参数是一个闭包,内含任务执行的语句。
Task
结构带有如下属性用于取消任务。
- isCancelled:该属性返回一个表示任务是否被取消的布尔值。
- cancel():取消任务的方法。
还有一些类型属性和方法,可用于从当前任务获取信息或创建执行指定处理的任务。以下是最常用的。
- currentPriority:该属性返回当前任务的优先级。这是一个
TaskPriority
结构体,有属性background
、high
、low
、medium
、userInitiated
和utility
。 - isCancelled:该属性返回一个表示当前任务是否取消的布尔值。
- sleep(nanoseconds: UInt64):本方法按照
nanoseconds
参数指定的时间挂起当前任务。
虽然可以在代码的任意地方创建Task
结构体初始化异步任务,SwiftUI自带了如下的修饰符在视图出现时进行创建。
- task(priority: TaskPriority, Closure):此修饰符在视图出现时执行第二个参数所指定的任务。
priority
参数是一个结构体,辅助系统决定何时执行任务。值有background
、high
、low
、medium
、userInitiated
和utility
。 - task(id: Value, priority: TaskPriority, Closure):此修饰符在视图出现时执行第三个参数所指定的任务。
id
参数是用于标识任务的值。每当这个值发生改变时,任务就会重启。priority
参数是一个结构体,辅助系统决定何时执行任务。值有background
、high
、low
、medium
、userInitiated
和utility
。
Async和Await
异步和并发任务在Swift中通过async
和await
关键字定义。例如要创建异步任务,我们使用async
标注方法,然后使用await
等待该方法执行完成。这表示在另一异步方法内只能通过await
关键字调用一个异步方法,创建一个无限循环。开启这一循环,我们使用task()
修饰符在视图出现时初始化异步任务,如下所示。
示例9-1:初始化异步任务
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, world!")
.padding()
}
.task(priority: .background) {
let imageName = await loadImage(name: "image1")
print(imageName)
}
}
func loadImage(name: String) async -> String {
try? await Task.sleep(nanoseconds: 3 * 1000000000)
return "Name: \(name)"
}
}
本例中使用background
优先级创建任务,表示它相对其它并行任务不具备优先级。在闭包中,我们调用loadImage()
方法,然后在控制台中打印返回值。我们定义的这个方法模拟从网上下载图片。稍后我们会学习如何下载数据及连接网络,但这里我们使用了sleep()
方法让任务暂停3秒,假装在下载图片(方法接收值的单位是纳秒)。停顿结束后,方法返回带文件名的字符串。要以异步定义方法,我们在参数的后面添加async
关键字,然后使用await
关键字调用它,表示任务必须等待处理完成。
task()
修饰符创建任务并添加到线程中。在视图加载后,会执行赋值给修饰符的闭包。在闭包中,我们调用loadImage()
方法,等待其完成。方法停顿3秒、返回字符串。此后,任务继续执行语句,在控制台上打印消息。
✍️跟我一起做:创建一个多平台项目。使用示例9-1中的代码更新ContentView
结构体。在模拟器中运行应用。3秒后会看到控制中打印的消息。
一个任务可执行多个异步处理。例如,下例中调用了loadImage()
3次来下载3张图片。
示例9-2:运行多异步处理
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, world!")
.padding()
}
.task(priority: .background) {
let imageName1 = await loadImage(name: "image1")
let imageName2 = await loadImage(name: "image2")
let imageName3 = await loadImage(name: "image3")
print("\(imageName1), \(imageName2), \(imageName3)")
}
}
func loadImage(name: String) async -> String {
try? await Task.sleep(nanoseconds: 3 * 1000000000)
return "Name: \(name)"
}
}
这些处理逐条按顺序执行。任务会等待上一条处理结束再处理下一条。本例中,整个任务耗时9秒完成(每个处理3秒)。
✍️跟我一起做:使用示例9-2中的代码更新ContentView
结构体。在模拟器中运行应用。9秒后会看到控制台中打印的消息。
只需在视图加载后运行异步任务使用task()
修饰符很有用,但大多数时候任务和视图的生命周期无依赖关系,必须使用Task
初始化方法显式地创建。比如可以通过onAppear()
方法和Task
结构体来重现上例。
示例9-3:显式定义任务
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, world!")
.padding()
}
.onAppear {
Task(priority: .background) {
let imageName1 = await loadImage(name: "image1")
let imageName2 = await loadImage(name: "image2")
let imageName3 = await loadImage(name: "image3")
print("\(imageName1), \(imageName2), \(imageName3)")
}
}
}
func loadImage(name: String) async -> String {
try? await Task.sleep(nanoseconds: 3 * 1000000000)
return "Name: \(name)"
}
}
这一视图和之前一样执行了3条处理,但这里显式地定义了任务,我们有了更多的控制权。比如,现在可以将任务赋值给变量,然后调用cancel()
方法取消任务。
cancel()
方法用于取消任务,便处理不会自动取消,我们必须使用isCancelled
属性监测任务是否被取消并自行停止任务,如下例所示。
示例9-4:取消任务
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, world!")
.padding()
}
.onAppear {
let myTask = Task(priority: .background) {
let imageName = await loadImage(name: "image1")
print(imageName)
}
Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { (timer) in
print("The time is up")
myTask.cancel()
}
}
}
func loadImage(name: String) async -> String {
try? await Task.sleep(nanoseconds: 3 * 1000000000)
if !Task.isCancelled {
return "Name: \(name)"
} else {
return "Task Cancelled"
}
}
}
本例中将前面的任务赋值给了一个常量,然后创建一个定时器在2秒后调用任务的cancel()
方法。在loadImage()
方法中,我们读取isCancelled
属性进行相对应的响应。如果取消了任务,返回Task Cancelled
,否则和之前一样返回名称。注意本例我们是在任务执行的处理内操作,因此使用了类型属性,而不是实例属性(我们从数据类型而不是实例中读取isCancelled
属性)。该属性根据当前任务的状态返回true
或false
。任务在完成后就被取消了。
任务可接收并返回值。Task
结构体包含一个value
属性提供对任务返回值的访问。当然,我们需等待任务完成才能读取值,如下例如下。
示例9-5:读取任务的返回值
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, world!")
.padding()
}
.onAppear {
Task(priority: .background) {
let imageName = await loadImage(name: "image1")
print(imageName)
}
}
}
func loadImage(name: String) async -> String {
let result = Task(priority: .background) { () -> String in
let imageData = await getMetadata()
return "Name: \(name) Size: \(imageData)"
}
let message = await result.value
return message
}
func getMetadata() async -> Int {
try? await Task.sleep(nanoseconds: 3 * 1000000000)
return 50000
}
}
因为我们需要等待任务完成才能使用值,所以定义了第二个任务。处理和之前一样启动,通过任务调用loadImage()
方法,但现在创建了第二个返回字符串的任务。该任务执行另一个异步方法,等待3秒、返回数字50000。在处理结束后,任务使用名称和返回的数字创建字符串。然后通过value
属性获取字符串,将其返回给原始任务,打印到控制台。
至此,我们使用了异步方法,但还可以定义异步属性。只需要使用async
关键字定义getter。
示例9-6:定义异步属性
struct ContentView: View {
var thumbnail: String {
get async {
try? await Task.sleep(nanoseconds: 3 * 1000000000)
return "mythumbnail"
}
}
var body: some View {
VStack {
Text("Hello, world!")
.padding()
}
.onAppear {
Task(priority: .background) {
let imageName = await thumbnail
print(imageName)
}
}
}
}
这次把调用方法改成由任务读取属性。属性在挂起任务3秒后返回字符串。这里做挂起只是为了进行演示,但在这个属性中可以执行任意需要的任务,比如处理或下载数据。
错误
异步任务不一定都能成功,所以必须准备好处理返回的错误。如果在创建自己的任务,可以通过实现Error协议的枚举来定义错误,在第3章中进行过讲解(见示例3-189)。下例中定义了一个含两个错误的结构体,一个在未找到服务端元数据(noData
)时返回,另一个在图片不存在(noImage
)时返回。
示例9-7:响应错误
enum MyErrors: Error {
case noData, noImage
}
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, world!")
.padding()
}
.onAppear {
Task(priority: .background) {
do {
let imageName = try await loadImage(name: "image1")
print(imageName)
} catch MyErrors.noData {
print("Error: No Data Available")
} catch MyErrors.noImage {
print("Error: No Image Available")
}
}
}
}
func loadImage(name: String) async throws -> String {
try? await Task.sleep(nanoseconds: 3 * 1000000000)
let error = true
if error {
throw MyErrors.noImage
}
return "Name: \(name)"
}
}
上例中的loadImage()
在测试代码时总是会抛出noImage
错误。其中的任务通过do catch
语句检测错误并在控制台打印消息报告错误。注意在异步方法可能会抛出错误时,必须在async
后使用关键字throws
进行声明。