【iOS】Swift的async/await 笔记一:异步函数初体验

5,031 阅读5分钟

前言

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等处理),大大提高可读性和维护性。

异步函数的注意点:

  1. 如果是在另一个异步函数或者子Task内调用该异步函数:执行完之后还是在被await分配到的线程中(子线程)
  2. 如果是在父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)")
    }
}

打印结果,可以看到是不同的线程:

951641199108_.pic.jpg

个人猜想的原理可能是这样:

971641202329_.pic.jpg

看上去跟以前的闭包回调差不多,只不过现在代码可以”同步“调用,并且不需要再关心线程的调度问题了(手动回去主线程之类)。

异步函数的异常捕获

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")
    }
}

打印结果:

961641201885_.pic.jpg

结语

这只是本人的小白式理解,如果有错会马上修改!

后续更新其他小白式理解和使用async letactorTaskGroup等案例的笔记,本篇到此为止。

案例代码:AwaitDemo