使用过ES6
或者Dart
开发的朋友应该对使用async await
进行异步编程比较熟悉,在iOS
中,随着Xcode 13
和Swift 5.5
的更新,在Swift
中,也可以使用async await
来进行异步编程了,在这篇文章中,我结合自己的在工作中实践的经验,来总结下自己的一些开发心得。
使用回调的问题
在iOS
开发中,进行异步
操作,我们通常通过Complete Handler(回调)
的方式,返回异步处理的结果。我们来看如下代码:
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 // 2
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)
}
}
}
}
}
这种写法看起来很糟糕:
- 1,方法之间嵌套太深,可读性差,容易出错。
- 2,在
guard let
中,在return
之前容易忘记handler回调
。 - 3,代码量比较大,不容易直观的看出这段的功能。
有没有更好的方式来避免这些问题出现呢?在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
![asyn-let.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c4dd77092c8f498a9993a38e81cfcddd~tplv-k3u1fbpfcp-watermark.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()
// if Task.isCancelled {
// throw ImageDownloadError.invalidMetadata
// }
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()
// if Task.isCancelled {
// throw ImageDownloadError.invalidMetadata
// }\
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
中:
- 1,开启一个
Task
,先下载image
,然后在下载imageMetadata
,下载完成之后,回到主线程刷新UI
。 - 2,在主线程初始化UI和做一些其他事情。
这里有两个新概念:Task
和MainActor
,使用Task
的原因是在同步线程和异步线程之间,我们需要一个桥接
,我们需要告诉系统
开辟一个异步环境
,否则编译器会报 'async' call in a function that does not support concurrency
的错误。 另外Task
表示开启一个任务。@MainActor
表示让showImage
方法在主线程执行。
回到示例代码本身,此时的代码执行顺序是这样的
使用 async-await
并不会阻塞主线程
,在同一个Task
中,遇到await
,后面的任务将会被挂起
,等到await
任务执行完后,会回到被挂起
的地方继续执行。这样就做到了 异步串行
。
异步并行(async-let)
我们回头看下上面的例子,下载图片和下载图片的metadata是可以并行
执行的。我们可以使用 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()
// 执行结果
初始化UI开始
>>>>>>>>>> 1 <NSThread: 0x6000005db840>{number = 6, name = (null)}
>>>>>>>> 2 <NSThread: 0x6000005db840>{number = 6, name = (null)}
downloadImage----- begin <NSThread: 0x6000005a8240>{number = 3, name = (null)}
downloadMetadata --- begin <NSThread: 0x6000005a8240>{number = 3, name = (null)}
downloadImage----- end
downloadMetadata --- end <NSThread: 0x6000005acf80>{number = 5, name = (null)}
>>>>>>>> 3 <NSThread: 0x6000005acf80>{number = 5, name = (null)}
初始化UI完成
其他事开始
其他事结束
刷新UI
此时的运行顺序是这样的
使用 asyn let
修饰后,该函数会并发
执行,所以async let
又称为并发绑定
。这里需要注意的是 使用 async let
修饰 image
,downloadImage
会被挂起
,该线程继续执行其他任务, 直到遇到 try await image
,downloadImage
才会执行,这也是为什么 print2
在downloadImage
之前执行的原因了。
在这里,我们在一个Task
内,异步并发
的执行任务,系统
会给我们维护一个任务树
downloadImage
和downloadMetadata
是该Task(任务)
的子任务
如果有一个子任务
抛出异常,该Task(任务)
,将会抛出异常。
Group Task
想一下,如果我们要同时下载多张图片,我们该怎么处理呢,我们先尝试使用如下方式: 通过遍历数组,开启多个Task
,并image添加到数组中。编译器不允许我们这样做,抛出了Mutation of capture var xxxx in concurrenly-excuting code
的错误,为什么呢?多个任务同时引用了可变变量detailImages
, 如果有两个任务同时向detailImages
里面写入数据,会造成数据竞争(data races)
,这样很不安全。
我们可以通过将每一个Task
放到任务组(data task)
中,来解决这个问题,新增如下方法
func downloadMultipleImagesWithMetadata(imageNumbers: [Int]) async throws -> [DetailedImage]{
var imagesMetadata: [DetailedImage] = []
try await withThrowingTaskGroup(of: DetailedImage.self) { group in
for imageNumber in imageNumbers {
// 向Taskgroup中添加
group.addTask(priority: .medium) {
async let image = self.downloadImageAndMetadata(imageNumber: imageNumber)
return try await image
}
}
//等Task组里面的任务都执行完
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("图片下载失败")
}
}
运行结果如下
downloadImage----- begin <NSThread: 0x600001998c80>{number = 5, name = (null)}
downloadMetadata --- begin <NSThread: 0x600001998c80>{number = 5, name = (null)}
downloadImage----- begin <NSThread: 0x600001998c80>{number = 5, name = (null)}
downloadMetadata --- begin <NSThread: 0x600001998c80>{number = 5, name = (null)}
downloadImage----- begin <NSThread: 0x600001998c80>{number = 5, name = (null)}
downloadMetadata --- begin <NSThread: 0x600001998c80>{number = 5, name = (null)}
downloadImage----- begin <NSThread: 0x600001998c80>{number = 5, name = (null)}
downloadMetadata --- begin <NSThread: 0x600001998c80>{number = 5, name = (null)}
downloadImage----- end
downloadMetadata --- end <NSThread: 0x60000198cf40>{number = 7, name = (null)}
downloadImage----- end
downloadMetadata --- end <NSThread: 0x60000198cf40>{number = 7, name = (null)}
downloadImage----- end
downloadMetadata --- end <NSThread: 0x60000198cf40>{number = 7, name = (null)}
downloadImage----- end
downloadMetadata --- end <NSThread: 0x60000198cf40>{number = 7, name = (null)}
可以看到,多个任务是并行执行的,并且在某一个任务中,也是并行执行的
。
withThrowingTaskGroup
会创建一个任务组
,来存放任务。 使用 for await
等待线程里面的任务全部执行完毕后,将全部数据返回,这样就解决了多个Task并行,引起的数据竞争
问题。 此时的任务树
结构如下
如果有一个任务抛出异常,那个整个任务组
将会抛出异常。
异步属性
可以通过async await
异步获取属性值,该属性只能是只读属性
。
extension UIImage {
// only read-only properties can be async
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的回调
在一些第三库
中,或者自己写的方法中,有许多都是基于handler回调
,我们需要自己去改造,例如如下回调:
//MARK: call back based
func requestUserAgeBaseCallBack(_ completeHandler: @escaping (Int)->() ){
NetworkManager<Int>.netWorkRequest("url") { response, error in
completeHandler(response?.data ?? 0)
}
}
可以选中该函数,按下command + shift + A
,选中 Add Async Alternative
,Xcode
会自动帮我们生成async
替换方法
转换结果如下:
//MARK: call back based
@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
来进行异步编程。
- 1,使用
async await
,进行异步串行
执行。 - 2,使用
async let
,在同一个Task(任务)
内,进行异步并行
执行。 - 3,使用
group task
和for await
,让多个Task
并行执行。
在最后一节,我们对基于handler
和基于Delegate
的代码进行了 async await
改造。
本文涉及的代码,我已上传我的Git仓库,如有需要,可自行下载。
如果觉得有收获请按如下方式给个
爱心三连
:👍:点个赞鼓励一下
。🌟:收藏文章,方便回看哦!
。💬:评论交流,互相进步!
。
今天的文章Swift并发编程async/await分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/13948.html