使用 Swift 实现简单易懂的异步编程 async/await
使用过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仓库,如有需要,可自行下载。
如果觉得有收获请按如下方式给个
爱心三连
:????:点个赞鼓励一下
。????:收藏文章,方便回看哦!
。????:评论交流,互相进步!
。
上一篇: 两种变换方法 for VARIANT类型
推荐阅读
-
使用 Swift 实现简单易懂的异步编程 async/await
-
异步编程RxJava-介绍-前言 前段时间写了一篇对协程的一些理解,里面提到了不管是协程还是callback,本质上其实提供的是一种异步无阻塞的编程模式;并且介绍了java中对异步无阻赛这种编程模式的支持,主要提到了Future和CompletableFuture;之后有同学在下面留言提到了RxJava,刚好最近在看微服务设计这本书,里面提到了响应式扩展(Reactive extensions,Rx),而RxJava是Rx在JVM上的实现,所有打算对RxJava进一步了解。 RxJava简介 RxJava的官网地址:https://github.com/ReactiveX/RxJava, 其中对RxJava进行了一句话描述:RxJava – Reactive Extensions for the JVM – a library for composing asynchronous and event-based programs using observable sequences for the Java VM. 大意就是:一个在Java VM上使用可观测的序列来组成异步的、基于事件的程序的库。 更详细的说明在Netflix技术博客的一篇文章中描述了RxJava的主要特点: 1.易于并发从而更好的利用服务器的能力。 2.易于有条件的异步执行。 3.一种更好的方式来避免回调地狱。 4.一种响应式方法。 与CompletableFuture对比 之前提到CompletableFuture真正的实现了异步的编程模式,一个比较常见的使用场景: CompletableFuture<Integer> future = CompletableFuture.supplyAsync(耗时函数); Future<Integer> f = future.whenComplete((v, e) -> { System.out.println(v); System.out.println(e); }); System.out.println("other..."); 下面用一个简单的例子来看一下RxJava是如何实现异步的编程模式: Observable<Long> observable = Observable.just(1, 2) .subscribeOn(Schedulers.io).map(new Func1<Integer, Long> { @Override public Long call(Integer t) { try { Thread.sleep(1000); //耗时的操作 } catch (InterruptedException e) { e.printStackTrace; } return (long) (t * 2); } }); observable.subscribe(new Subscriber<Long> { @Override public void onCompleted { System.out.println("onCompleted"); } @Override public void onError(Throwable e) { System.out.println("error" + e); } @Override public void onNext(Long result) { System.out.println("result = " + result); } }); System.out.println("other..."); Func1中以异步的方式执行了一个耗时的操作,Subscriber(观察者)被订阅到Observable(被观察者)中,当耗时操作执行完会回调Subscriber中的onNext方法。 其中的异步方式是在subscribeOn(Schedulers.io)中指定的,Schedulers.io可以理解为每次执行耗时操作都启动一个新的线程。 结构上其实和CompletableFuture很像,都是异步的执行一个耗时的操作,然后在有结果的时候主动告诉我结果。那我们还需要RxJava干嘛,不知道你有没有注意,上面的例子中其实提供2条数据流[1,2],并且处理完任何一个都会主动告诉我,当然这只是它其中的一项功能,RxJava还有很多好用的功能,在下面的内容会进行介绍。 异步观察者模式 上面这段代码有没有发现特别像设计模式中的:观察者模式;首先提供一个被观察者Observable,然后把观察者Subscriber添加到了被观察者列表中; RxJava中一共提供了四种角色:Observable、Observer、Subscriber、Subjects Observables和Subjects是两个被观察者,Observers和Subscribers是观察者; 当然我们也可以查看一下源码,看一下jdk中的Observer和RxJava的Observer jdk中的Observer: public interface Observer { void update(Observable o, Object arg); } RxJava的Observer: public interface Observer<T> { void onCompleted; void onError(Throwable e); void onNext(T t); } 同时可以发现Subscriber是implements Observer的: public abstract class Subscriber<T> implements Observer<T>, Subscription 可以发现RxJava中在Observer中引入了2个新的方法:onCompleted和onError onCompleted:即通知观察者Observable没有更多的数据,事件队列完结 onError:在事件处理过程中出异常时,onError会被触发,同时队列自动终止,不允许再有事件发出。 正是因为RxJava提供了同步和异步两种方式进行事件的处理,个人觉得异步的方式更能体现RxJava的价值,所以这里给他命名为异步观察者模式。 好了,下面正式介绍RxJava的那些灵活的操作符,这里仅仅是简单的介绍和简单的实例,具体用在什么场景下,会在以后的文章中介绍 Maven引入
-
什么是 Python 中的 coroutine?如何使用 async 和 await 关键字实现一个 coroutine?如何用 Python 实现简单的异步 RESTful API 客户端?