大师学SwiftUI第9章Part 1 - 异步并发之Task、Async、Await和错误

其它相关内容请见虚拟现实(VR)/增强现实(AR)&visionOS开发学习笔记

苹果系统借助现代处理器的多核可同步执行多条代码,提升同一时间内程序所能执行的任务。例如,一段代码从网上下载文件,另一段代码可以在屏幕上显示进度。此时,我们不能等待第一个执行完后再执行第二个,而必须要同步执行这两个任务。

要并行处理代码,系统将代码单元分组成任务。在Swift中,任务可以通过异步和并发编程实现。异步编程是一种编程模式,代码在完成任务前等待处理完成。这样系统可以在不同进程间共享计算资源。等待期间,系统可使用资源执行其它任务。而并发编程实现的代码可以利用多核同步执行多个任务。

图9-1:异步和并发编程

因很多应用可以同时运行,系统并不会对每个应用分配指定的核数。系统会创建一些执行线程,将任务分配给这些线程,然后根据可用资源决定哪个核执行哪些线程。在图9-1的示例中,左边是一个异步任务,从网上加载图片然后在屏幕上显示。在等待服务响应时,线程处于空闲状态可以执行其它任务,因此系统可以使用它执行更新进度条的任务。右图中创建了并发任务,因此在不同进程中同步执行。

任务

异步和并发的代码由任务定义。Swift标准库中包含有Task结构体用于创建和管理这些任务。下面是结构体的初始化方法:

  • Task (priority : TaskPriority?, operation : Closure):这个初始化方法创建并运行新任务。priority参数是一个辅助系统决定何时执行任务的结构体。这一结构体中包含类型属性定义标准优先级。当前有backgroundhighlowmediumuserInitiatedutilityoperation参数是一个闭包,内含任务执行的语句。

Task结构带有如下属性用于取消任务。

  • isCancelled:该属性返回一个表示任务是否被取消的布尔值。
  • cancel() :取消任务的方法。

还有一些类型属性和方法,可用于从当前任务获取信息或创建执行指定处理的任务。以下是最常用的。

  • currentPriority :该属性返回当前任务的优先级。这是一个TaskPriority结构体,有属性backgroundhighlowmediumuserInitiatedutility
  • isCancelled:该属性返回一个表示当前任务是否取消的布尔值。
  • sleep (nanoseconds : UInt64):本方法按照nanoseconds参数指定的时间挂起当前任务。

虽然可以在代码的任意地方创建Task结构体初始化异步任务,SwiftUI自带了如下的修饰符在视图出现时进行创建。

  • task (priority : TaskPriority, Closure):此修饰符在视图出现时执行第二个参数所指定的任务。priority参数是一个结构体,辅助系统决定何时执行任务。值有backgroundhighlowmediumuserInitiatedutility
  • task (id : Value, priority : TaskPriority, Closure):此修饰符在视图出现时执行第三个参数所指定的任务。id参数是用于标识任务的值。每当这个值发生改变时,任务就会重启。priority参数是一个结构体,辅助系统决定何时执行任务。值有backgroundhighlowmediumuserInitiatedutility

Async和Await

异步和并发任务在Swift 中通过asyncawait关键字定义。例如要创建异步任务,我们使用async标注方法,然后使用await等待该方法执行完成。这表示在另一异步方法内只能通过await关键字调用一个异步方法,创建一个无限循环。开启这一循环,我们使用task()修饰符在视图出现时初始化异步任务,如下所示。

示例9-1:初始化异步任务

swift 复制代码
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:运行多异步处理

swift 复制代码
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:显式定义任务

swift 复制代码
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:取消任务

swift 复制代码
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属性)。该属性根据当前任务的状态返回truefalse。任务在完成后就被取消了。

任务可接收并返回值。Task结构体包含一个value属性提供对任务返回值的访问。当然,我们需等待任务完成才能读取值,如下例如下。

示例9-5:读取任务的返回值

swift 复制代码
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:定义异步属性

swift 复制代码
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:响应错误

swift 复制代码
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进行声明。

代码请见:GitHub仓库

本文首发地址:AlanHou的个人博客,整理自2023年10月版《SwiftUI for Masterminds》

相关推荐
开心就好202511 小时前
iOS App 安全加固流程记录,代码、资源与安装包保护
后端·ios
开心就好202511 小时前
iOS App 性能测试工具怎么选?使用克魔助手(Keymob)结合 Instruments 完成
后端·ios
zhongjiahao1 天前
面试常问的 RunLoop,到底在Loop什么?
ios
wvy2 天前
iOS 26手势返回到根页面时TabBar的动效问题
ios
RickeyBoy3 天前
iOS 图片取色完全指南:从像素格式到工程实践
ios
aiopencode3 天前
使用 Ipa Guard 命令行版本将 IPA 混淆接入自动化流程
后端·ios
二流小码农3 天前
鸿蒙开发:路由组件升级,支持页面一键创建
android·ios·harmonyos
iceiceiceice4 天前
iOS PDF阅读器段评实现:如何从 PDFSelection 精准还原一个自然段
前端·人工智能·ios
TT_Close4 天前
【Flutter×鸿蒙】FVM 不认鸿蒙 SDK?4步手动塞进去
flutter·swift·harmonyos
张江4 天前
Swift Concurrency学习
swift