前言
Swift5.5有一个最大的新特性,那就是支持async/await了,类似于Flutter的future。
该特性不仅能大大简化代码的复杂度,而且能够更好的避免很多异步处理带来的错误,由此带来了新的概念:异步函数和结构化并发。
想要深入了解异步函数和结构化并发的概念可以去看喵神的文章:
本文主要是基于上面两篇文章介绍的async/await的小白式理解和使用的案例的第一章。
案例代码:AwaitDemo
PS:在Swift5.6之后,异步函数的执行通过日志打印不会再显示子线程了,因此本文的子线程打印仅供参考(实际上确实是在异步执行)。
异步函数初体验
在函数的返回箭头前面加上async关键字就可以把一个函数声明为异步函数。
根据以往的认知,iOS的函数都是同步函数,也就从开头到结束都是在一个线程内同步完成的;而异步函数可能会放弃当前线程,整个函数可以在不同的线程内执行。
- 🌰 这是随机获取一个网络段子的异步函数:
func getHitokoto() async -> String {
// ------ 此处是:调用该函数时的线程,可能是主线程也可能是子线程 ------
let (data, _) = try? await URLSession.shared.data(from: URL(string: "https://v1.hitokoto.cn")!)
// ====== 等到服务器响应再继续下面代码 ======
// ------ 此处是:因`await`而被底层机制分配到其他合适的线程 ------
///【在`异步函数`中调用async函数,执行完之后还仍旧处于那个async函数所在的线程】
var str = "null"
if let json = try? JSONSerialization.jsonObject(with: data!, options: .mutableContainers),
let dic = json as? [String: Any],
let hitokoto = dic["hitokoto"] as? String
{
str = hitokoto
}
return str
}
使用:
// 网络请求数据
func loadData() async -> String {
let str1 = await getHitokoto()
let str2 = await getHitokoto()
return str1 + "," + str2
}
// 刷新界面
func updateTitle() {
Task {
let title = await loadData()
// 此时并不会立马执行下一句代码,会等到拿到数据再继续,但并不会卡住主线程。
/**
* 放弃线程的能力,意味着异步方法可以被【暂停】,这个线程可以被用来执行其他代码。
* 如果这个线程是【主线程】的话,函数在此处【暂停】,主线程则去处理其他事情,因此界面不会卡顿。
*/
titleLabel.text = title
}
}
相比以前,现在代码可以以“同步”的形式实现异步处理的功能,不需要再使用回调闭包来进行刷新(各种闭包的嵌套、weakSelf等处理),大大提高可读性和维护性。
异步函数的注意点:
- 如果是在
另一个异步函数或者子Task内调用该异步函数:执行完之后还是在被await分配到的线程中(子线程) - 如果是在
父Task内调用该异步函数:执行完之后会回到Task所在的那个线程(貌似都是主线程)
也就是说,最好不要在被async修饰的函数内执行需要主线程执行(UI刷新之类)的代码。
- 🌰 对
loadData进行加工,打印每一个异步函数执行完之后的线程:
func loadData(_ tag: Int) async throws -> String {
JPrint("\(tag) - loadData_1 \(Thread.current)") // 调用函数的那个线程(可能是主线程也可能是子线程,主要看系统的分配)
// ------- 此时是调用函数时的那个线程 -------
let str1 = await getHitokoto() // await - 代表了函数在此处会放弃当前线程,将被底层机制分配到其他合适的线程,也就是函数此处被【暂停】了
// 来到这里时,说明上面await代码执行完了,函数继续执行,只不过此时不再是执行await代码前的那个线程了。
// 因为该函数本身就是异步函数,所以此时还是在上面await代码内被await分配到的线程中(子线程)
// ------- 此时是被await分配到的线程 -------
JPrint("\(tag) - loadData_2 \(str1), \(Thread.current)") // 第一个`getHitokoto`内分配的子线程
let str2 = await getHitokoto()
JPrint("\(tag) - loadData_3 \(str2), \(Thread.current)") // 第二个`getHitokoto`内分配的子线程
// PS:有空闲的线程则会用旧的线程
return "\(tag) - \(str1) - \(str2)"
}
@objc func btn1DidClick() {
Task {
JPrint("begin \(Thread.current)")
let str1 = try await loadData(0)
JPrint("str1 \(str1) \(Thread.current)") // 回到Task所在的那个线程(主线程)
JPrint("end \(Thread.current)")
}
}
打印结果,可以看到是不同的线程:
个人猜想的原理可能是这样:
看上去跟以前的闭包回调差不多,只不过现在代码可以”同步“调用,并且不需要再关心线程的调度问题了(手动回去主线程之类)。
异步函数的异常捕获
在async后面加上throws修饰,内部使用throw抛出,外部使用do{}catch{}捕获。
func loadData() async throws -> String {
let str = await getHitokoto()
if str.count == 0 {
throw NSError(domain: "数据为空", code: 999)
}
return str
}
@objc func btn1DidClick() {
Task {
do {
let str = try await loadData()
JPrint("result: \(str)")
} catch {
JPrint("error: \(error)")
}
}
}
关于Task
如果想在【没有被async修饰的函数】内使用异步函数,就得套在 Task{} 内使用。而异步函数的内部使用其他异步函数就不需要再套一个 Task{} 了,因为异步函数内部就已经在一个任务执行的上下文环境内运行的。
- 🌰 Task的注意点:
@objc func btn1DidClick() {
Task {
JPrint("Task1 begin")
// Task3是Task1所在的{}执行完再执行的,也就是”Task2 begin“之后
Task {
JPrint("Task3 begin")
let str = await getHitokoto()
JPrint("result3: \(str)")
JPrint("Task3 end")
}
let str = await getHitokoto()
JPrint("result1: \(str)")
// Task4等到Task1结束后才执行的,也就是”Task1 end“之后
Task {
JPrint("Task4 begin")
let str = await getHitokoto()
JPrint("result4: \(str)")
JPrint("Task4 end")
}
JPrint("Task1 end")
}
// Task2跟Task1是`并发`执行的
Task {
JPrint("Task2 begin")
let str = await getHitokoto()
JPrint("result2: \(str)")
JPrint("Task2 end")
}
}
打印结果:
结语
这只是本人的小白式理解,如果有错会马上修改!
后续更新其他小白式理解和使用async let、actor、TaskGroup等案例的笔记,本篇到此为止。
案例代码:AwaitDemo