Swift Async/Await 异步编程实践指南

Viewed 0

对于使用过 ES6 或 Dart 的开发者来说,async/await 异步编程模式应该很熟悉。在 iOS 开发中,随着 Xcode 13 和 Swift 5.5 的更新,Swift 也引入了 async/await 特性,使得异步编程变得更加简洁和高效。本文将结合实践经验,总结在 Swift 中使用 async/await 进行异步编程的一些关键点。

使用回调的问题

在 iOS 开发中,异步操作通常通过完成处理器(回调)来返回结果。例如,以下代码展示了多层嵌套的回调:

func processImageData2a(completionBlock: (_ result: Image?, _ error: Error?) -> Void) {
    loadWebResource("dataprofile.txt") { dataResource, error in
        guard let dataResource = dataResource else {
            completionBlock(nil, error)
            return
        }
        loadWebResource("imagedata.dat") { imageResource, error in
            guard let imageResource = imageResource else {
                completionBlock(nil, error)
                return
            }
            decodeImage(dataResource, imageResource) { imageTmp, error in
                guard let imageTmp = imageTmp else {
                    completionBlock(nil, error)
                    return
                }
                dewarpAndCleanupImage(imageTmp) { imageResult, error in
                    guard let imageResult = imageResult else {
                        completionBlock(nil, error)
                        return
                    }
                    completionBlock(imageResult)
                }
            }
        }
    }
}

这种写法存在几个问题:方法嵌套过深,可读性差且容易出错;在 guard 语句中容易忘记调用回调;代码量大,功能不直观。为了解决这些问题,Xcode 13 之后可以使用 async/await 进行改进。

async-await

异步串行

使用 async/await 改造后,代码变得清晰:

func loadWebResource(_ path: String) async throws -> Resource
func decodeImage(_ r1: Resource, _ r2: Resource) async throws -> Image
func dewarpAndCleanupImage(_ i : Image) async throws -> Image

func processImageData() async throws -> Image {
  let dataResource  = try await loadWebResource("dataprofile.txt")
  let imageResource = try await loadWebResource("imagedata.dat")
  let imageTmp      = try await decodeImage(dataResource, imageResource)
  let imageResult   = try await dewarpAndCleanupImage(imageTmp)
  return imageResult
}

代码量显著减少,逻辑更清晰,可读性增强。为了理解 async/await 的工作流程,考虑以下示例:

override func viewDidLoad() {
   super.viewDidLoad()
   Task {
       let image = try await downloadImage(imageNumber: 1)
       let metadata = try await downloadMetadata(for: 1)
       let detailImage = DetailedImage(image: image, metadata: metadata)
       self.showImage(detailImage)
  }
  setupUI()
  doOtherThing()
}
func setupUI(){
    print("初始化UI开始")
    sleep(1)
    print("初始化UI完成")
}
func doOtherThing(){
    print("其他事开始")
    print("其他事结束")
}

@MainActor
func showImage(_ detailImage: DetailedImage){
    print("刷新UI")
    self.imageButton.setImage(detailImage.image, for: .normal)
}

func downloadImage(imageNumber: Int) async throws -> UIImage {
        try Task.checkCancellation()
        print("downloadImage----- begin \(Thread.current)")
        let imageUrl = URL(string: "http://r1on82fmy.hn-bkt.clouddn.com/await\(imageNumber).jpeg")!
        let imageRequest = URLRequest(url: imageUrl)
        let (data, imageResponse) = try await URLSession.shared.data(for: imageRequest)
        print("downloadImage----- end ")
        guard let image = UIImage(data: data), (imageResponse as? HTTPURLResponse)?.statusCode == 200 else {
            throw ImageDownloadError.badImage
        }
            return image
}
func downloadMetadata(for id: Int) async throws -> ImageMetadata {
    try Task.checkCancellation()
    print("downloadMetadata --- begin \(Thread.current)")
    let metadataUrl = URL(string: "http://r1ongpxur.hn-bkt.clouddn.com/imagemeta\(id).json")!
    let metadataRequest = URLRequest(url: metadataUrl)
    let (data, metadataResponse) = try await URLSession.shared.data(for: metadataRequest)
    print("downloadMetadata --- end  \(Thread.current)")
    guard (metadataResponse as? HTTPURLResponse)?.statusCode == 200 else {
        throw ImageDownloadError.invalidMetadata
    }
    return try JSONDecoder().decode(ImageMetadata.self, from: data)
}

struct ImageMetadata: Codable {
    let name: String
    let firstAppearance: String
    let year: Int
}

struct DetailedImage {
    let image: UIImage
    let metadata: ImageMetadata
}

enum ImageDownloadError: Error {
    case badImage
    case invalidMetadata
}

在 viewDidLoad 中,Task 开启了一个异步环境,先下载图像,然后下载元数据,完成后在主线程刷新 UI。同时,主线程初始化 UI 并执行其他任务。这里引入了 Task 和 MainActor:Task 用于在同步和异步线程之间桥接,开辟异步环境;@MainActor 确保 showImage 方法在主线程执行。

使用 async/await 不会阻塞主线程。在同一个 Task 中,遇到 await 时,后续任务会被挂起,等待 await 任务完成后恢复执行,从而实现异步串行。

异步并行(async-let)

下载图像和元数据可以并行执行,使用 async-let 实现:

func downloadImageAndMetadata(imageNumber: Int) async throws -> DetailedImage {
        print(">>>>>>>>>> 1 \(Thread.current)")
        async let image =  downloadImage(imageNumber: imageNumber)
        async let metadata =  downloadMetadata(for: imageNumber)
        print(">>>>>>>> 2 \(Thread.current)")
        let detailImage = DetailedImage(image: try await image, metadata: try await metadata)
        print(">>>>>>>> 3 \(Thread.current)")
        return detailImage
}

在 ViewDidLoad 中调用:

Task {
        let detailImage = try await downloadImageAndMetadata(imageNumber: 1)
        self.showImage(detailImage)
}
setupUI()
doOtherThing()

执行顺序显示,async let 修饰的函数会并发执行,称为并发绑定。使用 async let 后,downloadImage 会被挂起,线程继续执行其他任务,直到遇到 try await image 时才执行 downloadImage。系统会维护一个任务树,其中 downloadImage 和 downloadMetadata 是 Task 的子任务;如果子任务抛出异常,整个 Task 会抛出异常。

Group Task

如果需要同时下载多张图片,直接开启多个 Task 会导致数据竞争问题。可以通过任务组解决:

func downloadMultipleImagesWithMetadata(imageNumbers: [Int]) async throws -> [DetailedImage]{
    var imagesMetadata: [DetailedImage] = []
    try await withThrowingTaskGroup(of: DetailedImage.self) { group in
        for imageNumber in imageNumbers {
            group.addTask(priority: .medium) {
                async let image = self.downloadImageAndMetadata(imageNumber: imageNumber)
                return try await image
            }
        }
        for try await imageDetail in group {
            imagesMetadata.append(imageDetail)
        }
    }
    return imagesMetadata
}

在 viewDidLoad 中调用:

Task {
    do {
        let images = try await downloadMultipleImagesWithMetadata(imageNumbers: [1,2,3,4])
    } catch ImageDownloadError.badImage {
        print("图片下载失败")
    }
}

运行结果显示多个任务并行执行,且每个任务内部也并行。withThrowingTaskGroup 创建任务组存放任务,使用 for await 等待所有任务完成后返回数据,避免数据竞争。如果有一个任务抛出异常,整个任务组会抛出异常。

异步属性

可以通过 async await 异步获取只读属性值:

extension UIImage {
    var thumbnail: UIImage? {
        get async {
            let size = CGSize(width: 40, height: 40)
            return await self.byPreparingThumbnail(ofSize: size)
        }
    }
}

如何接入async await

使用系统async await API

系统提供了许多 async API,如 URLSession,可以直接使用:

let (data, metadataResponse) = try await URLSession.shared.data(for: metadataRequest)

改造基于handler的回调

对于基于回调的函数,可以手动改造或使用 Xcode 自动生成。例如:

@available(*, deprecated, message: "Prefer async alternative instead")
func requestUserAgeBaseCallBack(_ completeHandler: @escaping (Int)->() ){
    Task {
        let result = await requestUserAgeBaseCallBack()
        completeHandler(result)
    }
}

func requestUserAgeBaseCallBack() async -> Int {
    return await withCheckedContinuation { continuation in
        NetworkManager<Int>.netWorkRequest("url") { response, error in
            continuation.resume(returning: response?.data ?? 0)
        }
    }
}

使用 withCheckedContinuation 包装回调,实现异步版本。

改造基于delegate的回调

通过改造 UIImagePickerControllerDelegate 示例:

class ImagePickerDelegate: NSObject, UINavigationControllerDelegate & UIImagePickerControllerDelegate {
    var contination: CheckedContinuation<UIImage?, Never>?

    @MainActor
    func chooseImageFromPhotoLibrary() async throws -> UIImage?{
        let vc = UIImagePickerController()
        vc.sourceType = .photoLibrary
        vc.delegate = self
        print(">>>>>>>> 图片选择 \(Thread.current)")
        BasicTool.currentViewController()?.present(vc, animated: true, completion: nil)
        return await withCheckedContinuation({ continuation in
            self.contination = continuation
        })
    }

    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        self.contination?.resume(returning: nil)
        picker.dismiss(animated: true, completion: nil)
    }

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage
        self.contination?.resume(returning: image)
        picker.dismiss(animated: true, completion: nil)
    }
}

使用时:

Task {
    let pickerDelegate = ImagePickerDelegate()
    let image = try? await pickerDelegate.chooseImageFromPhotoLibrary()
    sender.setImage(image, for: .normal)
}

通过 CheckedContinuation 实例完成 delegate 的异步改造。

总结

本文从回调的不便性入手,介绍了 Swift 5.5 的新特性 async/await 进行异步编程。关键点包括:使用 async/await 实现异步串行执行;使用 async let 在同一个 Task 内实现异步并行执行;使用 group task 和 for await 让多个 Task 并行执行。这些特性显著提升了代码的可读性和可维护性。

0 Answers