WWDC21 推出了 async/await
,特意去百度了一下:
微软在发布 VS2012 的同时推出了C# 5.0,其中包含了async和await。
Swift 也在进化,WWDC21 推出的 swift 5.5 也终于加入了 async/await
特性。下面是对 Meet async/await in Swift 的整理。如果有不恰当的地方,欢迎评论区交流。
如上图,列表中每一行左侧的小图标是从服务端下载下来并渲染到 UI 界面上来的。一般情况下远端图片下载渲染分以下四个步骤:
1、通过图片的地址 URL 地址创建一个 URLRequest 的对象;
2、以上一步创建的 URLRequest 对象为参,通过 URLSession 创建一个 task 任务用于请求远端的图片数据;
3、等待,异步获取 UIImage 的 data 数据, 然后将 data 通过 UIImage 的 initWithData 方法转化成 image 对象;
4、将该 image 对象渲染到 UI 界面;
一个简单的异步获取数据并在 completion handler 中处理返回的数据,每位 iOS 开发者对这个过程并不陌生。其整的实现代码如下
很熟悉的味道吧!
二十行代码左右,通常我们都会去这么实现或采用类似的实现方式。所谓习惯成自然,也就没觉着这样有什么不好。但可不可以更简洁点呢?
上面的写法不简洁吗?
是的!
相比 async/await
的实现方式,上面的代码有以下几个问题:
1、为了向通知外界返回的结果,completion
回调写了 5 次,而且很容易在两个guard
的 else
执行语句中漏写;
2、不直观,函数体若按上到下的顺序执行比较符合人们的阅读习惯,而且很清楚的表达了代码的意图。但这里图片的下载是异步的, dataTask
的尾随闭包里面的代码其实是后执行的;
3、代码多,容易乱,不够简洁;
现在 Swift 5.5 引入 async/await
特性。下面来使用 async/await
语法来实现上述功能。
分析一下这个实现:
先来看函数定义这一行,fetchThumbnail
参数的右括号后面紧跟了一个 async
关键字,表示该函数为 async
函数。async
后面则跟了个 throw
关键字,表示该函数可能会抛出错误。
函数体中第二行在获取图片时使用了 await
关键字,表示这是一个需要等待的操作,因为图片的下载因网络好坏需要不确定的时间。此时函数执行到这里会暂停(suspend),但不会阻塞程序的执行,系统会接管该函数所在的线程去继续执行其它的任务。待图片下载完成之后,系统又会回到这个暂停的地方,该函数会 resume 从而继续执行下面的函数语句。
接下来是处理返回数据。若返回的数据的 statusCode 不等于 200 ,说明请求不成功,使用 throw
抛出错误。反之将返回的数据 data 转化为 UIImage
对象。在数据转换过程中如果发生异常,同样由 throw
抛出错误,否则就返回处理好的 thumnail
对象供渲染使用。
至此, async/await
版的代码完成了上面传统的 completion handler
回调函数完成的图片下载。代码量简洁,逻辑更清晰,代码可读性幅提高!都忍不住想来一句:good job !
回过头再去仔细看一下这个函数实现。倒数第二行乍一看上去不是一个异步函数,但仍然使用了 await
关键字,这不科学!其实不然,这里的 thmbnail
其实是一个 async
类型的属性(property)。
注意:仅 read-only 类型的 property 才可以是标记为 async。
不仅 function
和 property
,initializer
也可以标记为 async。那么还有没有其它的呢?有,下面有请 AyncSequence 登场!
AsyncSequence 是一个可以异步获取其元素的一个序列。由于其每一个元素的获取都是异步的,所以依次获取其中的每一个元素都是 awaitable
的,需要使用 await
关键字进行标识。
综上,有很多地方你可以使用 await
关键字,await
也表示一个 async
标识的函数执行到这里时候会暂停 suspend
。那这里的暂停 suspend
意味着什么呢?
通常情况下,当我们调用一个函数的时候,函数体的执行在执行完之前会一直占用当前的线程,直到函数体执行完毕之后当前线程才能去执行其它的任务。而 async 函数就不一样了,它具有 暂停 suspend
的能力。当暂停时,它就会让出当前线程给系统,由系统去决定当前线程去执行其它任务。而且 async
函数可以暂停 suspend
多次。当标识为 await
的任务完成之后,系统又会重新回到原来暂停的地方,原来的函数被 resume
接着执行函数体下面的代码。需要注意的是,被 resume
的函数所处的线程可能与之前 suspend
时所处的线程不是同一个线程。
非 async
函数的执行流程
async
函数的执行流程
理论和示例都看过了,问题来了:怎样将现有的代码切换为 async/await
呢?
先从现代软件编码的一个重点代码测试开始。我们想像测试同步代码一样方便的去测试异步代码,于是 async/await
就派上用场了!
下面是一个通常情况下的测试异步代码的一个示例
使用 async/await
特性之后
不仅如此,当前很多的系统库 API 也都支持了 async/await
这一特性。
比如
代理方法中也有不少 completion handler
语法,当然我们没有忘记。同样代理方法你也可以选择使用 async/await
特性的 API。
到这里,你所看到的所有异步函数都是 Swift 帮你实现的,你只要使用就行了。但总有一些情形需要你自己去搞定异步函数。于是就有了 continuation 。借助 continuation,你可以灵活的自己去决定异步函数 resume 的时机和位置。
如上,借助 withCheckedThrowingContinuation
你可以向上面这样使用 continuations 。
使用 continuation 需要注意两点:
1、continuation 不能被丢弃,必须要执行相关操作以唤醒之前 suspend 的异步函数;
2、continuation 的 resume
函数在一个分支条件上只能调用一次;
Thanks for reading!