对于使用过 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 并行执行。这些特性显著提升了代码的可读性和可维护性。