Swift UI 深入理解async和await
Swift UI 深入理解async和await

Swift UI 深入理解async和await

针对Swift UI 在异步函数async调用过程中,需要用到的async、await以及task做一个详细的介绍。并通过一个代码样例来讲解在网络调用过程中,如何使用async和await。以便让更多人的了解async和await的使用方式和流程。

async和await介绍

async 和 await 是 Swift 中用于处理异步编程的关键字,它们允许你编写非阻塞的代码,处理需要等待完成的任务,比如网络请求、文件读写等。通过使用 async 和 await,你可以避免使用复杂的回调或闭包,代码变得更加直观和易于理解。

async 是什么?

async 用于声明一个异步函数,表示这个函数包含一些可能需要较长时间完成的操作(例如网络请求、文件操作等)。调用异步函数时,程序不会被阻塞,其他代码可以继续运行。

函数被标记为 async 后,意味着它可能会暂停执行并等待某些任务完成。

当调用该函数时,你需要用 await 来等待它的执行结果。

func fetchData() async {
    // 这个函数可能需要等待一些任务完成
    print("Fetching data...")
}

await 是什么?

await 用于等待异步函数执行完成并获取结果。它会暂停当前任务,直到异步函数返回结果后再继续执行。await 关键字告诉编译器,当前这行代码需要异步等待执行结果。

await 只能在 async 函数中使用,因为它需要有一个异步上下文。

await fetchData()  // 等待 fetchData() 完成

async和await的关系

在调用异步函数时,使用async声明函数是异步的。

在调用该函数时,使用await来暂停当前执行,并等待异步函数完成。

模拟网络请求的场景

// 声明一个异步函数
func fetchData() async -> String {
    // 模拟网络请求
    return "Data from server"
}

// 在另一个函数中调用
func processData() async {
    let data = await fetchData() // 等待 fetchData 完成
    print(data) // 打印获取的数据
}

在这个例子中:

fetchData 函数是异步的,因为它可能需要等待一些外部操作(例如网络请求)完成。用 async 关键字标记它。

在 processData 函数中,我们使用 await 关键字来等待 fetchData 的执行结果,表示当前函数会暂停,直到数据获取完成。

为什么需要async和await?

在处理需要长时间等待的操作(比如网络请求、数据库访问或文件读写)时,如果不使用 async/await,你可能会使用回调、闭包或其他形式的异步机制。这种方式会让代码变得复杂和难以维护(这被称为“回调地狱”)。

async 和 await 提供了一种更加简洁和可读的方式来编写异步代码,让代码看起来像同步代码,尽管它仍然是异步的。

跟异步函数相反的就是同步函数,也就是我们常见的函数类型,不需要async或await,操作按顺序执行。

func greet() -> String {
    return "Hello, World!"
}

最后,特别注意的是async 关键字仅用于声明函数,不能用于调用函数。

当调用异步函数时,需要用 await 来等待它的完成,而不是再使用 async。因为 async 的作用是标识函数需要异步执行,而 await 则是告诉程序需要暂停并等待这个异步函数完成。

扩展理解

在学习async和await时,你会发现async声明的异步函数表示它可能会暂停执行并等待某些任务完成。而await会告诉编译器在这里暂停执行,等待异步操作完成后继续。

一个是可暂停的一步操作,另一个是暂停等待某个异步操作,这里可能会让人感到困惑和不解。而实际上这并不冲突,而是相互配合的机制。

async 使函数具备“暂停”的能力:当你声明一个函数为 async 时,你允许它在内部执行一些异步任务时,可以暂停自己等待,不用同步阻塞其他代码。这让函数可以更加高效地处理需要等待的操作,比如网络请求、文件读取等。

await 使调用方愿意“等待”这个暂停:当你在调用 async 函数时,使用 await 表示你愿意在此处暂停并等待这个异步任务的结果,而不是马上执行后面的代码。

简而言之,async 和 await 并不是冲突的,而是相互协作的机制:

async 函数表示它可以被“暂停”和恢复,这使得它在执行时不会一直占用资源,可以在需要时等待其他任务完成。

await 告诉程序在这个点上暂停,等到异步操作完成,再继续向下执行。

举个现实中的例子:

想象一个人(程序)要做很多事情(任务),比如洗衣服、做饭、写作业。

1、同步方式(没有 async/await):

这个人把衣服放到洗衣机里,然后一直等着洗衣机洗完,什么都不做。洗完后再去做饭,再做作业。

整个过程顺序执行,期间不能干别的事情。

2、异步方式(async):

这个人把衣服放到洗衣机里(async 操作),然后“暂停”洗衣的任务,去做饭。

等到做饭需要等待煮沸水时,他又“暂停”做饭,开始写作业。

“暂停” 就是 async,表示任务允许等待其他任务,不会阻塞。

3、await 的角色:

当洗衣机结束洗衣后,这个人用 await 去“等待”并完成衣服晾晒。

同样,饭煮好后,他用 await 来“等待”并完成吃饭。

通过 async 和 await,他在多个任务之间切换,从而更加高效,而不会因为某个任务卡住自己。

此外,这里的await是一个异步的“暂停”,在async/await中,暂停并不意味着完全停止,而是程序会“挂起”当前任务,然后让出控制权,让其他任务可以继续执行。当await的任务完成时,程序会恢复到这个等待的任务,继续执行。

总结

await 不是监听器,它是真正的暂停,只是这种暂停不会卡住程序,而是允许其他任务继续执行。

高效的异步等待:await 的机制让程序更加高效,因为它不会因为等待某个任务完成而浪费时间。

多任务并行:async/await 的设计使得程序可以在“等待”的时候处理其他任务,比如上述例子中,可以在等待洗衣机工作时去做饭。

简单来说,async 使得任务可以被“挂起”,await 使得程序可以在需要时等待某个任务完成而不影响其他任务的执行。这样可以更灵活地处理多任务并发问题。

task修饰符

task是什么?

task 是 SwiftUI 中的一个修饰符,专门用于启动异步任务。它的特点是会在视图加载时自动触发,并在异步任务完成后更新视图。task 常用于需要在视图首次加载时执行网络请求、数据加载或其他需要异步处理的操作。

虽然不能在普通的同步代码中直接使用 await,但在 task 中是可以的,因为 task 提供了一个异步的上下文,类似于在函数中使用 async,因此在 task 中你可以直接使用 await。虽然 task 并没有在函数声明上显式使用 async,它隐式地提供了这个功能。你可以理解为 task 在视图中创建了一个小的异步环境。

var body: some View {
    List(results, id: \.trackId) { item in
        Text(item.trackName)
    }
    .task {
        await loadData()
    }
}

在这个例子中,task 会在视图加载时运行 performAsyncOperation 函数,等待它的执行结果并进行处理。你可以将它理解为在视图的生命周期中,创建了一个 async 任务。

task总结

task可以在视图中局部创建一个异步任务,允许使用 await 来调用异步操作, 无需将 View 的整个 body 声明为 async。

task 提供了一个简便的方式在 SwiftUI 中执行异步任务。

await 可以在 task 中使用,因为 task 创建了一个隐式的异步环境。

因此,await 可以直接在 .task 中使用,是因为 .task 本质上创建了一个异步上下文,允许异步任务的执行。

实战演习

我们将通过代码调取itunes中Taylor Swift的曲目列表,并展示在我们的应用当中。

import SwiftUI

struct Response: Codable {
    var results: [Result]
}

struct Result: Codable {
    var trackId: Int
    var trackName: String
    var collectionName: String
}

struct CupcakeCorner: View {
    @State private var results = [Result]()
    
    func loadData() async {
        guard let url = URL(string: "https://itunes.apple.com/search?term=taylor+swift&entity=song") else {
            print("Invalid URL")
            return
        }
        do {
            let (data, _) = try await URLSession.shared.data(from: url)

            if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
                results = decodedResponse.results
            }
        } catch {
            print("Invalid data")
        }
    }
    
    var body: some View {
        List(results, id: \.trackId) { item in
            Text(item.trackName)
                .font(.headline)
            Text(item.collectionName)
        }
        .task {
            await loadData()
        }
    }
}

#Preview {
    CupcakeCorner()
}

在该代码中,我们定义了名为Response和Result的两个结构体,在CupcakeCorner结构体中设置了一个名为loadData的异步函数,在调取这个异步函数时,我们使用的代码为:

.task {
    await loadData()
}

其中,loadData使用async标记为异步函数,采用前面学到的task生成一个局部的异步任务,最后通过await调用我们的异步函数。

下面的扩展知识涉及URL(string:)、URLSession等数据调取方式,如果没有兴趣的朋友,可以在这里停止并退出该页面。

扩展知识(可选)

在上面的实战演习代码中,我们还需要着重分析一下loadData异步函数是如何运行的。

func loadData() async {
    guard let url = URL(string: "https://itunes.apple.com/search?term=taylor+swift&entity=song") else {
        print("Invalid URL")
        return
    }
    do {
        let (data, _) = try await URLSession.shared.data(from: url)

        if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
            results = decodedResponse.results
        }
    } catch {
        print("Invalid data")
    }
}

代码分析:

1、URL(string:)对象

URL(string: "https://itunes.apple.com/search?term=taylor+swift&entity=song")

URL(string:)表示构建一个URL对象,这里的URL是一个字符串,表示 iTunes API 的搜索请求地址。URL(string:) 会将字符串转换为 URL 对象,如果字符串格式无效,它会返回 nil。

除了URL(string:),URL类还有多种构造方法和功能。

2、URL(components:):

var components = URLComponents()
components.scheme = "https"
components.host = "itunes.apple.com"
components.path = "/search"
components.queryItems = [URLQueryItem(name: "term", value: "taylor+swift")]
let url = components.url

通过 URLComponents 创建一个 URL。你可以分段构建 URL(如设置 scheme、host、path 等)。

3、URLSession.shared.data(from:url)

let (data, _) = try await URLSession.shared.data(from: url)

URLSession 是 Swift 中用于处理网络请求的核心类,允许我们从网络中获取数据、上传数据、下载文件等。它提供了一种异步操作的方式,可以轻松进行 HTTP 请求。

接下来详细讲解 URLSession 及其相关概念,包括 URLSession.shared、data(from:) 等内容。

1)URLSession 简介

URLSession 是用来管理网络请求和响应的类。它支持多种网络任务,包括:

  • 数据任务(Data Task):从服务器获取数据或发送数据到服务器。
  • 下载任务(Download Task):从服务器下载文件。
  • 上传任务(Upload Task):将文件上传到服务器。

URLSession 主要用于异步网络请求,这意味着程序不会因为等待网络响应而卡住,可以提高应用的响应速度和用户体验。

2)URLSession.shared

URLSession.shared 是 URLSession 提供的一个共享的、单例的会话对象。它是最常用的,会话(Session)管理网络请求的生命周期,可以复用这个共享会话来简化代码。

特点:

  • 简化使用:由于它是共享的单例实例,你可以直接使用,不需要手动创建新的会话对象。
  • 适合简单的网络请求:例如加载 JSON 数据,下载文件等。
  • 无额外的配置:shared 会话对象不能自定义配置,只能使用默认的会话配置。
let url = URL(string: "https://example.com/data.json")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
    // 处理返回的数据
}
task.resume()

在这里,URLSession.shared 提供了一个默认的会话对象来执行网络请求。

3)data(from:) 方法

data(from:) 是 URLSession 中的一种方法,用于发起简单的数据任务请求。这是一个异步方法,会向指定的 URL 发送请求,并返回从该 URL 获取的二进制数据。

在 Swift 中,可以直接使用 await 来等待 data(from:) 的结果。下面详细解释这个方法:

  • 功能:data(from:) 会向指定的 URL 发送请求,并在数据返回时,将结果作为元组 (Data, URLResponse) 返回。
  • 异步执行:由于网络请求通常需要一些时间,因此这是一个 async 方法,你需要使用 await 来等待它完成。
  • 使用场景:用于获取简单的网络数据,如下载 JSON 数据或文本。
let url = URL(string: "https://api.example.com/items")!
do {
    let (data, response) = try await URLSession.shared.data(from: url)
    // 使用 data 和 response 进行处理
} catch {
    print("请求失败: \(error)")
}

在这里,data(from:) 发送了一个 GET 请求到 url,并等待响应。

  • data 是从服务器接收到的原始数据。
  • response 包含了响应的元数据(如状态码、HTTP 头信息)。

4)dataTask(with:)方法

在URLSession.shared的样例代码中,我们还发现存在dataTask(with)的请求方法。

dataTask(with:) 是最基础的请求方法。它与 data(from:) 的不同之处在于:

  • dataTask(with:):你需要手动调用 .resume() 来启动任务,而且它使用的是回调闭包的形式。比较适合老版本的代码。
  • data(from:):现代的 async/await 风格,无需手动启动,直接等待执行结果。

传统 dataTask 的用法:

let url = URL(string: "https://example.com/data.json")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
    if let data = data {
        print("收到的数据: \(data)")
    } else if let error = error {
        print("请求失败: \(error)")
    }
}
task.resume()

5)data 和 _

let (data, _) = try await URLSession.shared.data(from: url)

我们使用的是 URLSession.shared.data(from:) 方法,该方法返回一个 元组 (Data, URLResponse)。

  1. data:这是从服务器返回的 实际数据,类型是 Data。通常,你会将这个数据解码为 JSON、文本、图片等格式,以便在应用中使用。
  2. _:这是元组中的第二个值,表示 URLResponse 对象。这个对象包含了关于服务器响应的信息,比如 HTTP 状态码、响应头等。

在代码中使用 _(下划线) 表示你不需要处理或使用这个值。这是 Swift 的一种语法,用来忽略不需要的返回值。这样可以让代码更简洁,不用声明不必要的变量。

如果你需要检查 URLResponse 的信息,可以这样写:

let (data, response) = try await URLSession.shared.data(from: url)
if let httpResponse = response as? HTTPURLResponse {
    print("Status code: \(httpResponse.statusCode)")
}

6)task.resume()方法

task.resume() 是用于启动或恢复一个 URLSession 任务的方法。理解这个方法的关键在于知道 URLSession 的任务(如 dataTask, downloadTask, uploadTask)的生命周期。

(1)为什么需要 task.resume()?

当你使用 URLSession 创建了一个网络任务(如 dataTask(with:)),这个任务不会自动开始。即使你已经定义好了任务和处理的回调,任务还处于暂停状态。为了让这个任务实际运行,你必须调用 .resume() 方法。

换句话说,task.resume() 是告诉系统:“现在开始执行这个任务。”

(2)任务的生命周期

URLSession 的任务在创建后会有以下几种状态:

  1. 未开始(Suspended):默认状态,任务创建但未开始执行。
  2. 运行中(Running):调用 .resume() 之后,任务会进入运行状态。
  3. 完成(Completed):任务执行完成(无论是正常完成还是因为错误终止)。
  4. 暂停(Suspended):可以手动暂停任务,比如下载任务可以暂停和恢复。

task.resume() 将任务从未开始(Suspended)状态启动到运行中(Running)状态。一旦任务开始,它会执行网络请求,等待响应,调用你的回调处理数据。

(3)一个网络请求任务的代码样例

let url = URL(string: "https://example.com/data.json")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
    if let data = data {
        print("Received data: \(data)")
    } else if let error = error {
        print("Request failed: \(error)")
    }
}
// 任务尚未开始
task.resume()  // 任务启动,开始请求数据

在上面的例子中:

  1. 任务创建:调用 URLSession.shared.dataTask(with:) 创建了一个 task,但此时任务还未启动。
  2. 任务启动:调用 task.resume(),任务正式开始,请求 URL 中的数据。

(4)暂停和恢复任务

URLSession 的任务(如下载任务)可以暂停和恢复。除了 resume(),还有 suspend() 方法:

  1. task.suspend():暂停任务。
  2. task.resume():如果任务已暂停,会恢复任务继续执行。如果任务未开始,则启动任务。
task.suspend() // 暂停任务
task.resume()  // 恢复任务

总结

  1. task.resume() 是用于启动或恢复 URLSession 任务的方法。
  2. dataTask 和其他网络任务创建后不会自动开始,必须调用 .resume() 才能启动。
  3. 通过 suspend() 和 resume(),你可以控制任务的执行状态。

调用 .resume() 是让网络任务实际运行的必要步骤。没有这一步,任务永远处于“待命”状态,不会去请求数据,也不会触发回调。

URLSession.shared.data(from:)

然而使用URLSession.shared.data(from:) 时,不需要调用 task.resume(),因为这是一个 async 方法,它会在你调用时自动开始执行任务。

总结

  1. data(from:):自动启动任务,不需要 resume()。适合 async/await 风格编程。
  2. dataTask(with:):手动启动任务,需要 resume()。传统的回调方式。

     3、如果不了解do-catch用法,可以看我的另一篇深入理解try的文章。关于JSONDecoder的用法,也可以看一下这篇swift UI解构JSON文件

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注