如何在Swift中以Async/await方式使用 URLSession

542 阅读8分钟

如何在Swift中以Async/await方式使用 URLSession

hudson 译 原文

传统上,当我们想要发出网络请求时,我们必须使用基于闭包的URLSession APIs来异步执行请求,以便我们的应用程序可以在等待完成时做出响应。随着Swift 5.5的发布,情况不再如此,我们现在有了另一个替代方案,那就是使用async/await。

在本文中,我想向您展示如何使用async/await关键字进行网络请求。除此之外,我还将在async/await和基于闭包的API之间进行快速比较,以便您可以更好地了解使用async/await的好处。

本文确实要求您对async/await有基本的了解。因此,如果您不熟悉Swift并发,我强烈建议您首先阅读我的博客文章 Swift并发入门

说了这么多,让我们直接开始吧!

先决条件

在本文中,我们将使用Apple iTunes API从Taylor Swift获取专辑集。以下是API的URL:

Https://itunes.apple.com/search?Term=taylor+swift&entity=album

此API端点将为我们提供以下JSON响应: 在这里插入图片描述

出于演示目的,我们将获取专辑的名称和价格,并在一个集合视图列表中显示它们。以下是我们进行JSON解码所需的模型对象:

struct ITunesResult: Codable {
    let results: [Album]
}

struct Album: Codable, Hashable {
    let collectionId: Int
    let collectionName: String
    let collectionPrice: Double
}

请注意,我们正在使Album结构符合Hashable协议,以便我们可以将其用作集合视图可区分数据源的项目标识符类型。

有了这些准备,让我们进入网络请求代码。

传统方式

在Swift 5.5之前,为了发出网络请求,我们必须使用基于闭包的URLSessiondataTask(with:completionHandler:)方法来触发在后台异步运行的请求。 一旦网络请求完成,完成处理程序将返回网络请求的结果。

为了简单起见,让我们为此定义一个AlbumsFetcher结构:

struct AlbumsFetcher {
    
    enum AlbumsFetcherError: Error {
        case invalidURL
        case missingData
    }
    
    static func fetchAlbums(completion: @escaping (Result<[Album], Error>) -> Void) {
        
        // Create URL
        guard let url = URL(string: ”https://itunes.apple.com/search?term=taylor+swift&entity=album“) else {
            completion(.failure(AlbumsFetcherError.invalidURL))
            return
        }
        
        // Create URL session data task
        URLSession.shared.dataTask(with: url) { data, _, error in

            if let error = error {
                completion(.failure(error))
                return
            }
            
            guard let data = data else {
                completion(.failure(AlbumsFetcherError.missingData))
                return
            }
            
            do {
                // Parse the JSON data
                let iTunesResult = try JSONDecoder().decode(ITunesResult.self, from: data)
                completion(.success(iTunesResult.results))
            } catch {
                completion(.failure(error))
            }
            
        }.resume()
    }
}

如果您之前处理过发出网络请求的代码,您应该熟悉上述fetchAlbums(completion:)函数。我们首先启动一个数据任务来发出网络请求。请求完成后,我们会检查错误并解析响应JSON。

调用fetchAlbums(completion:)函数也非常简单:

AlbumsFetcher.fetchAlbums { [unowned self] result in
    
    switch result {
    case .success(let albums):

        // Update UI using main thread
        DispatchQueue.main.async {
            
            // Update collection view content
            updateCollectionViewSnapshot(albums)
        }
        
    case .failure(let error):
        print(”Request failed with error: \(error)“)
    }
}

需要注意的一点是,updateCollectionViewSnapshot(_:)函数是一个辅助函数,它根据albums数组更新我们的列表。因此,在调用它之前,我们需要调度回主线程。

在了解了传统的方式后,在下一节中,让我们看看如何使用新的async/await关键字实现同样的事情。

Swift的并发方式

为了将我们基于闭包的fetchAlbums(completion:)函数转换为新的async/await风格,我们可以采取两种完全不同的方法。

第一种方法是使用CheckedContinuation(在Swift 5.5中引入)将fetchAlbums(completion:) 函数与异步上下文桥接起来,而第二种方法是将基于闭包的URLSession替换为URLSession的异步变体。

现在,让我们首先关注CheckedContinuation方法。

CheckedContinuation

CheckedContinuation是Swift 5.5中的一种新机制,可帮助开发人员在同步和异步代码之间进行桥接。我们可以使用函数withCheckedThrowingContinuation(function:_:)或函数withCheckedContinuation(function:_:)方法创建CheckedContinuation

在我们的案例中,由于fetchAlbums(completion:) 函数的完成处理程序将返回错误,我们将使用该方法的“抛出”变体来创建一个 CheckedContinuation。如下:

static func fetchAlbumWithContinuation() async throws -> [Album] {
    
    // Bridge between synchronous and asynchronous code using continuation
    let albums: [Album] = try await withCheckedThrowingContinuation({ continuation in
        
        // Async task execute the `fetchAlbums(completion:)` function
        fetchAlbums { result in
            
            switch result {
            case .success(let albums):
                // Resume with fetched albums
                continuation.resume(returning: albums)
                
            case .failure(let error):
                // Resume with error
                continuation.resume(throwing: error)
            }
        }
    })
    
    return albums
}

如您所见,withCheckedThrowingContinuation(function:_:)方法接受接受延续参数的闭包。它创建一个异步任务,执行fetchAlbums(completion:) 函数,以异步触发网络请求。

在上述代码中,您应该注意一些重要方面:

  1. withCheckedThrowingContinuation(function:_:)方法被标记为async,因此我们必须使用await关键字调用它。除此之外,由于我们使用它的“抛出”变体,我们也需要使用try关键字(就像调用正常抛出函数一样)。

  2. 在整个异步任务中,我们必须在每个执行路径上精确调用一次恢复方法。不止一次从延续中恢复是未定义的行为,而永不恢复会使异步任务无限期地处于暂停状态,我们称之为延续泄漏。

  3. withCheckedThrowingContinuation(function:_:)方法的返回类型必须与 resume(returning:) 方法的参数数据类型匹配,即[Album]

现在,让我们把重点转移到调用点。假设我们在视图控制器中调用fetchAlbumWithContinuation()函数,我们可以这样调用它:

// Start an async task
Task {

    do {
        
        let albums = try await AlbumsFetcher.fetchAlbumWithContinuation()
        
        // Update collection view content
        updateCollectionViewSnapshot(albums)
        
    } catch {
        print(”Request failed with error: \(error)“)
    } 

}

像往常一样,我们必须创建一个异步任务,这样我们才能在异步上下文中等待并执行fetchAlbumWithContinuation()函数。由于我们不再使用完成处理程序,我们现在可以使用do-catch语句处理函数抛出的错误。

此外,请注意,在调用updateCollectionViewSnapshot()之前调度到主线程不再需要,因为我们正在调用视图控制器中的fetchAlbumWithContinuation()函数是一个MainActor

注意:

如果您不熟悉MainActor,您可以查看我之前的文章Swift 并发入门

异步URLSession

在Swift 5.5中,除了发布asyncawait关键字外,苹果还更新了许多自己的SDK,以支持这两个关键字,其中一个是URLSession

苹果在URLSession中添加了一个新的data(url:)方法,相当于我们之前使用的dataTask(with:completionHandler:)方法。这是一个抛出错误的异步方法,返回数据和URLResponse的元组。以下是如何使用它来发出网络请求:

static func fetchAlbumWithAsyncURLSession() async throws -> [Album] {

    guard let url = URL(string: ”https://itunes.apple.com/search?term=taylor+swift&entity=album“) else {
        throw AlbumsFetcherError.invalidURL
    }

    // Use the async variant of URLSession to fetch data
    // Code might suspend here
    let (data, _) = try await URLSession.shared.data(from: url)

    // Parse the JSON data
    let iTunesResult = try JSONDecoder().decode(ITunesResult.self, from: data)
    return iTunesResult.results
}

上面的代码几乎是不言自明的,但是,请注意,URLSession.data(from:)方法是一种异步方法,因此代码在等待返回时可能会暂停。这也是为什么我们需要使用await关键字来调用它。

以下是fetchAlbumWithAsyncURLSession()的调用点,这与调用fetchAlbumWithContinuation()基本相同:

// Start an async task
Task {
    
    do {
        
        let albums = try await AlbumsFetcher.fetchAlbumWithAsyncURLSession()
        
        // Update collection view content
        updateCollectionViewSnapshot(albums)
        
    } catch {
        print(”Request failed with error: \(error)“)
    }
    
}

在这个阶段,你可能会问:如果URLSession的API已经支持 async/await,使用CheckedContinuation有什么意义?好吧,你绝对是对的!我们绝对应该使用任何API的async/await变体,如果它们可用的话。

CheckedContinuation主要用于桥接任何尚未支持 async/await语法的异步API。假设您正在使用不支持async/await语法的第三方网络库(如Alamofire),那么在等待第三方库更新期间,您可以使用CheckedContinuation逐步迁移现有代码以支持async/await。

Async/await与闭包

自从苹果在WWDC21中引入async/await以来,几位初级开发人员一直问我:为什么每个人都如此炒作async/await,而我们已经可以通过使用闭包和调度队列来做同样的事情?

在本节中,让我们尝试通过快速浏览使用异步/等待的一些好处来回答这个问题:

  1. 在使用闭包时,我们可能会忘记调用完成处理程序,没有办法防止这种情况发生。使用async/await时,如果我们没有从异步函数返回,我们将收到编译错误。

  2. 使用闭包时,不可能使用do-catch语句来处理错误,因为闭包不支持这一点。另一方面,我们可以使用do-catch语句处理异步函数抛出的错误,就像处理普通函数抛出的错误一样。

  3. 通过使用async/await,我们不再需要担心忘记调度回主线程,因为它已经由MainActor处理。

  4. async/await提供了更大的线程爆炸安全性,同时提高了我们代码的性能。您可以查看此WWDC视频以了解更多信息。

  5. 使用async/await语法编写的异步代码都是线性代码。需要按顺序执行的操作都一个接一个地列出。这使得我们的代码(实现和调用)更短、更干净、更容易推理。您可以使用以下图像进行快速比较。

网络请求比较 网络请求实现比较

网络请求调用点比较 网络请调用点比较

小结

使用async/await进行网络请求非常简单,我们只需使用它就能获得大量好处。然而,值得注意的是,async/await仅在iOS 15及更高版本中可用。因此,如果您的项目仍然需要支持旧版本的iOS,您可能需要等待一段时间才能更新现有的异步代码以使用async/await。

如果您想亲自尝试,这里是完整的示例代码

感谢您的阅读。👨🏻‍💻