async / await
将函数标记为async
会告诉Swift
编译器该函数是异步执行的,是可以挂起的。await
关键字标记了这些挂起点。当一个函数在await
调用时被挂起时,它所执行的线程可以用来执行其他工作。当等待的工作完成时,运行时可以恢复函数的执行。
比如下面这段代码:
func fetchServerData() async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: URL(string: "https://xxxxx")!)
return data
}
在函数fetchServerData()
中使用了async
修饰,从而告诉编译器这是个异步方法,可以挂起。在方法里面请求的时候使用了await
,因此在网络请求的时候,不会立即执行return
,此时的函数被挂起了,也可以认为被暂停了,只有请求返回结果后,运行时再继续执行该方法剩余部分。
还有一点需要注意的是被async
标记的函数/方法只能在异步上下文中调用,例如Task
或者其他异步函数中调用。
Task {
let data = try await viewModel.fetchServerData()
}
async
:修饰的方法或函数是异步的。只能在异步上下文中调用,需要和await搭配使用。
await
:在调用async
修饰的方法时,需要在前面使用await
,表示代码在等待异步的方法或函数返回时可能会暂停执行。使用它可以暂停执行,直到异步方法返回结果。
除了用在函数或方法中,async/await
还可以用在计算属性中:
var myProperty: String {
get async {
return ""
}
}
上面return了一个空字符串,实际中,可能需要一些耗时的操作后才能返回。
那么在调用的地方调用函数方法一样,如下:
Task {
let myProperty = await viewModel.myProperty
}
上一篇文章也就说如果有多个情况相互依赖,如果我们采用逃逸闭包(@escaping closure)的方法,会嵌套很多层,即使使用的throws抛出异常,代码也是比较难维护了,遇到这样的代码,除非是自己写的,否则一般都不太情愿去维护。
现在我们模拟一个场景,延迟2秒模拟网络数据返回的情况,我们嵌套几层看看
func addTitle() {
// 1
let title1 = "title1 \n\(Thread.current)"
dataArray.append(title1)
// 2
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
let title2 = "title2 \n\(Thread.current)"
self.dataArray.append(title2)
// 3
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
let title3 = "title3 \n\(Thread.current)"
DispatchQueue.main.async {
self.dataArray.append(title3)
// 4
let title4 = "title4 \n\(Thread.current)"
self.dataArray.append(title4)
}
}
}
}
上面代码每个2秒向数组添加一个元素并显示在界面上,为了不阻塞主线程,我们用了很多异步操作,采用了DispatchQueue.main.asyncAfter
和DispatchQueue.global().asyncAfter
等方法,并且最终还要回到主线程刷新UI。比如在第三步如果用到了DispatchQueue.global().asyncAfter
,那么还要调用DispatchQueue.main.async
回到主线程,整体来说代码嵌套比较多。这里还特意打印了不同时期的线程情况,如下:
现在改成用async
和await
组合再试试:
func addAuthor() async {
let author1 = "author1 \n\(Thread.current)"
await MainActor.run {
self.dataArray.append(author1)
}
try? await Task.sleep(nanoseconds: 2_000_000_000)
let author2 = "author2 \n\(Thread.current)"
await MainActor.run {
self.dataArray.append(author2)
}
try? await Task.sleep(nanoseconds: 2_000_000_000)
let author3 = "author3 \n\(Thread.current)"
await MainActor.run {
self.dataArray.append(author3)
}
try? await Task.sleep(nanoseconds: 2_000_000_000)
let author4 = "author4 \n\(Thread.current)"
await MainActor.run {
self.dataArray.append(author4)
}
}
采用async
和await
组合代码就好看多了,至少是从上往下看,而不是从外往里看了。代码也是从上至下一步一步执行的。
这里面采用了try? await Task.sleep(nanoseconds: 2_000_000_000)
模拟2秒延迟的情况。
addAuthor()
方法使用了async
修饰,表示这个方法是一个异步方法,里面会有await
将方法挂起来。因为是异步方法,所以每次在向dataArray添加数据刷新UI的时候,都需要回到主线程,之前回到主线程一般采用的是DispatchQueue.main.async
,这次采用了MainActor
,后面文章会涉及到这个,这里就不阐述了,苹果推出了MainActor
,也意味着想让我们少用DispatchQueue.main.async
这类代码,或者说给开发者更多了选择。
addAuthor()
是异步的,那是不是方法里面都是在同一个异步线程呢?看看打印输出就知晓了。
通过运行打印可以看出,这里面用到了好几个线程,每次遇到延迟代码后都会切换一个线程。
所以笔者认为async
修饰的方法提供了一个异步环境,但是处理里面的事件不一定是一个线程在处理,当遇到await
等待的时候,方法被挂起了,线程也去处理别的事情了,当方法有返回数据的时候,系统再找一个空闲的异步线程接着处理方法的返回值值及后续的事件。
小结一下
async/await
的组合能够帮助发开着写出更容易理解,更加健壮并且易维护的异步编程代码,它的出现将会替代以前的异步编程逻辑,对开发者更加友好。
async
和await
是组合使用的,调用被async
修饰的方法的时候,需要在异步上下文中执行,并且在调用方法的前面加上await
关键字。
如果需要提供一个上下文的环境,我们可以采用Task
,其闭包内提供了一个异步环境。
另外不管怎么调用,最终都要回归到UI上,切记要调用主线程代码。
如果上述文章内容有不当或错误之处,还请不吝赐教。
最后,希望能够帮助到有需要的朋友,如果觉得有帮助,还望点个赞,添加个关注,笔者也会不断地努力,写出更多更好用的文章。