1.什么是挂起函数

挂起函数是Kotlin协程中特殊的函数,当执行到这个函数时,协程内部的代码会被暂停执行,并让出当前线程的资源,以便让线程执行其他的任务。
此时挂起函数会在其他线程执行(存在挂起点的情况下),等待耗时操作完成,然后再根据调度器的安排,切换回原来的协程,在对应的线程中继续执行。
由于挂起函数挂起的是协程体,所以挂起函数一定是直接或者间接在协程中被调用

2.在哪儿挂起(挂起点)

在执行到能够实际执行挂起操作的函数时,才会挂起协程,这个函数也叫挂起点。
通常是Kotlin协程库提供的函数,如delay(),withContext(),channnel.receive(),flow.colllect();或者是协程支持的第三方库的挂起函数,如Retrofit 的suspend接口方法,Room 的suspend DAO方法等。
在执行了挂起函数后,是协程被挂起,并释放协程占用的线程;但是挂起函数是会继续执行的,调度器会安排另外的线程给这个挂起函数使用。
比如我们通常使用withContext(Dispatch.IO){},就会给挂起函数安排IO类型的线程单独执行,而这个新IO线程可能会阻塞,但它不会影响原来调用协程的线程。

3.常用的挂起函数

3.1 delay()

delay()会暂停当前执行的协程指定时间,通过挂起协程的方式,不会阻塞当前的线程。在到时间后,协程调度器会恢复这个协程,继续执行delay()之后的代码。
delay()是一个可以取消的挂起函数,当调用的协程在delay期间被取消掉,delay()会立即抛出异常,从而使协程停止执行后续的代码。

3.2 suspendCancellableCoroutine

当调用suspendCancellableCoroutine方法时,会立即挂起当前协程。
需要使用内部传入的continuation提供的resume()方法来恢复协程;或者使用resumeWithException()方法来抛出异常并恢复协程。
suspendCancellableCoroutine允许在lambda表达式中通过continuation的invokeOnCancellation { … }方法来注册一个回调,当协程被取消时,回调会被执行。通常可以在这里做清除资源、移除监听、停止动画等操作。
suspendCancellableCoroutine适用于传统的基于回调接口的异步请求,并且通常是只回调一次的请求。
suspendCancellableCoroutine不会自动切换线程,它会在调用它的协程所在的线程执行lambda表达式。所以如果是在lambda表达式中执行了网络请求等耗时的操作,请确保表达式是在Dispatchers.IO上下文中执行。
resume()或者resumeWithException()是线程安全的,会把结果或者异常传递回挂起的协程,并让协程在原来的调度器上恢复执行。

3.3 withContext

withContext用于在不引入新的协程前提下,切换当前协程的上下文(通常是切换调度器CoroutineDispatcher),挂起当前上下文的协程,并在指定的上下文中执行完lambda表达式后,将结果返回原来的上下文环境中。
withContext通常是来做线程的切换,是以同步风格编写异步代码并管理其上下文的关键工具。

3.4 withTimeout

withTimeout用于在指定时间内执行一段block代码块,如果未能在超时时间内完成,则会抛出异常,否则返回block块的结果。
withTimeout是一个挂起函数,调用它的协程会被挂起,直到结果返回或者超时取消,才会回到原来的协程上下文环境继续执行。
withTimeout是通过取消其block块的协程作用域来工作的,所以block块中必须是可取消的,比如调用了delay(),或者自动检测isActive
withTimeout不会切换线程,所以通常我们还要配合withcontext切换线程,如下:

suspend fun doWorkWithTimeout() {
        try {
            val result = withTimeout(5000L) { // 5秒超时
                withContext(Dispatchers.IO) { // 将耗时IO操作切换到IO线程池
                    println("Work starting on ${Thread.currentThread().name}")
                    delay(6000L) // 模拟耗时IO,这个会超时
                    "Work Done"
                }
            }
            println("Result: $result")
        } catch (e: TimeoutCancellationException) {
            println("Work timed out on ${Thread.currentThread().name}")
        }
    }

3.5 Job.join()

Job.join是一个挂起点,它会挂起当前执行的协程,而去执行目标Job中的任务,等Job执行完毕或者取消之后,才会恢复到原来的协程继续执行。
Job.join不会返回结果。通过launch{…}的方式获取到Job,再调用join方法。

3.6 joinAll(…)

joinAll会等待多个Job完成生命周期后,再继续执行原来的协程。
它内部也是调用的Job.join,但是joinAll不保证多个Job的完成顺序。

3.7 Deferred.await()

跟Job.join类似,会等待目标协程执行完毕,再执行原有的协程。
Deferred.await会返回block块最后的结果。通过async{…}的方式获取到Deferred,再调用await方法。

3.8 Flow.collect()

collect是一个终端操作符,在调用了collect之后,才会启动Flow的数据收集过程。
collect是一个挂起函数,会挂起当前协程,直到Flow发射完毕并消费完成。

3.9 Channel.send(element: E)

send会尝试往Channel发送一个数据,如果此时Channel没有缓冲或者缓冲区满了,此时会挂起协程,直到另外一个消费的协程调用了该Channel的receive()方法。

3.10 Channel.receive()

与Channel.send方法对应,会尝试从Channel中获取一个数据,如果此时Channel没有任何数据可供接收,会挂起协程,直到有新的元素通过send()发送到Channel中。

3.11 callbackFlow { … }

callbackFlow是一个Flow构建器,专门将基于回调式API转换成响应式编程风格,使用较多。
callbackFlow对应的lambda表达式是数据生产者,通过send()或则trysend()(更常用)发送数据。
callbackFow虽然不是挂起函数,但是其lambda表达式末尾需要调用awaitClose方法,这是一个挂起函数,保证Flow能够一直持续发送数据。在Flow取消或者生命周期完结的时候,会调用awaitClose方法内部的lambda表达式,通常用于清除注册的监听。

4.自定义挂起函数

一个自定义的挂起函数要实现真正的挂起行为(即非阻塞地暂停执行并释放线程),它必须直接或间接地调用 Kotlin 协程框架提供的、能够真正执行挂起操作的基础挂起函数
如下:

suspend fun uploadFile(file: File): Boolean {
    return withContext(Dispatchers.IO) {
        // ...
        true // 或 false 表示成功/失败
    }
}
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐