前言
在反编译后我们知道挂起函数的内部执行流程就是传入一个continuation参数的状态机,即当一个函数定义为挂起函数时,编译器在编译后,会在参数最后传入一个Continuation< T >类型的参数。
这个Continuation到底表示什么,本篇文章我们就来探讨一下。
正文
Koltin协程库
其实Kotlin的协程框架代码非常多,但是它有一个层次关系,大致关系图如下:
我们会发现最下面的叫做协程基础元素,其中就有我们熟悉的CoroutineContext以及Continuation,如果仔细去看里面的代码,会发现它们居然在kotlin标准库中;而中间层则是我们更熟悉的、经常用的一些类,比如Job、Flow,它们是存放在kotlinx.coroutines库中,我们需要单独依赖;这种关系说明了一件什么事情呢
也就是基础元素它更像是砖块,而不同的库就是利用这些基础元素封装出的,但是目前还是Kotlin的协程库封装的最好。最后就是因为Kotlin是跨平台实现,所以有特定平台的实现,比如JVM中使用线程池,在JS中使用的是JS线程。
可以发现这个Continuation是基础元素,我们来看看这个基础元素要如何使用。
实现挂起函数
在前面学习中我们知道挂起函数必须由其他挂起函数调用或者协程调用,但是我们一直没有实现一个挂起函数,那下面我们就可以使用suspendCoroutine{}高阶函数来实现一个挂起函数,代码如下:
fun main() = runBlocking {
println("start")
val result = getLengthSuspend("Kotlin")
println(result)
println("end")
}
suspend fun getLengthSuspend(text: String): Int = suspendCoroutine {
continuation ->
thread {
//模拟耗时
Thread.sleep(3000)
continuation.resume(text.length)
}
}
这里我们定义的挂起函数getLengthSuspend在其内部没有调用其他挂起函数,但是也不会提醒suspend关键字是多余的,说明它就是一个挂起函数,上面代码执行结果如下:
这里最最无法理解的就是以continuation.resume这样异步的方式传出结果以后,挂起函数就能接收到结果呢?
这里就要结合上一篇文章所说的挂起函数原理了,runBlocking启动的协程,也可以看成一个挂起函数,然后会有一个continuation传递给getLengthSuspend()函数,而当getLengthSuspend中的线程执行完后调用resume方法,又会触发runBlocking的挂起函数中的invokeSuspend方法,从而继续执行后面的操作。
所以这里没有什么神奇的地方,结合前一篇文章的CPS和状态机以及continuation传递就非常好理解,其中continuation.resume会触发调用点函数的continuation的invokeSuspend方法进入下一个状态机状态。
suspendCoroutine{}函数
当然这里实现挂起函数的重点是suspendCoroutine{}函数,除了这个还有一个函数是suspendCancellableCoroutine{}函数,这2个高阶函数也是Kotlin协程库的基础元素。
我们直接看一下suspendCoroutine函数的定义:
public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
return suspendCoroutineUninterceptedOrReturn { c: Continuation<T> ->
val safe = SafeContinuation(c.intercepted())
block(safe)
safe.getOrThrow()
}
}
可以发现这里block函数的参数就是一个continuation,这个continuation就是从其他挂起函数传递进来的。而这里就是Continuation的第一个功能,它可以创建挂起函数,同时把挂起函数的值往外面传递(resume方法)。
这里代码其实还是比较容易阅读,除了suspendCoroutineUninterceptedOrReturn外,我们来看一下,它的参数也是一个continuation,我们可知:
- SafeContinuation就是把原来的Continuation给包裹了一遍。
- 把safe传递给block执行,也就是相当于调用lambda中的逻辑。
- safe.getOrThrow()就是取出block(safe)的运行结果。
那这个suspendCoroutineUninterceptedOrReturn函数是啥呢,直接看源码如下:
public suspend inline fun <T> suspendCoroutineUninterceptedOrReturn(crossinline block: (Continuation<T>) -> Any?): T {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
throw NotImplementedError("Implementation of suspendCoroutineUninterceptedOrReturn is intrinsic")
}
实际上这里理解这句话的关键是"intrinsic",这里其实指的是编译器领域的一个术语,叫做"内建",即这个函数是由Kotlin编译器来实现的。
这里就不探究Kotlin编译器当中的逻辑了,但是我们可以写个demo来看一下这个内建函数的功能与作用。
我们发现这个suspendCoroutineUninterceptedOrReturn的函数类型是(Continuation< T >) -> Any?,这个是不是特别熟悉,它就是上篇文章所说的CPS后的函数的函数类型,而这里的Any?就可以直接返回这个挂起函数是否真的挂起。
比如下面代码:
fun main() = runBlocking {
val result = testNoSuspendCoroutine()
println(result)
}
private suspend fun testNoSuspendCoroutine() = suspendCoroutineUninterceptedOrReturn<String> {
continuation ->
return@suspendCoroutineUninterceptedOrReturn "Hello!"
}
这里的代码中没有调用continuation.resume(),而是直接返回"Hello",所以它不是真正的挂起函数,将上面方法进行反编译后:
private static final Object testNoSuspendCoroutine(Continuation $completion) {
int var2 = false;
if ("Hello!" == IntrinsicsKt.getCOROUTINE_SUSPENDED()) {
DebugProbesKt.probeCoroutineSuspended($completion);
}
return "Hello!";
}
也可以发现这并不是一个真正的挂起函数。
这时我们写一个真正的挂起函数:
fun main() = runBlocking {
val result = testSuspendCoroutine()
println(result)
}
private suspend fun testSuspendCoroutine() = suspendCoroutineUninterceptedOrReturn<String> {
continuation ->
thread {
Thread.sleep(1000L)
continuation.resume("Hello!")
}
return@suspendCoroutineUninterceptedOrReturn kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
}
这里真正调用了continuation.resume方法,而且返回的是COROUTINE_SUSPEND,这时再进行反编译:
private static final Object testSuspendCoroutine(Continuation $completion) {
int var2 = false;
// 1
ThreadsKt.thread$default(false, false, (ClassLoader)null, (String)null, 0, (Function0)(new CoroutineBasicElementsKt$testSuspendCoroutine$2$1($completion)), 31, (Object)null);
// 2
Object var10000 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
if (var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED()) {
DebugProbesKt.probeCoroutineSuspended($completion);
}
// 3
return var10000;
}
final class CoroutineBasicElementsKt$testSuspendCoroutine$2$1 extends Lambda implements Function0 {
final Continuation $it;
public Object invoke() {
this.invoke();
return Unit.INSTANCE;
}
public final void invoke() {
// 4
Thread.sleep(1000L);
Continuation var1 = this.$it;
String var2 = "Hello!";
Companion var3 = Result.Companion;
var1.resumeWith(Result.constructor-impl(var2));
}
CoroutineBasicElementsKt$testSuspendCoroutine$2$1(Continuation var1) {
super(0);
this.$it = var1;
}
}
这里注释1和4创建了一个新的线程,执行了thread{}中的代码,然后注释2将var10000赋值为COROUTINE_SUSPEND这个挂起标志位,注释3返回挂起标志位,根据上一篇文章所说的,代表这个函数会真的被挂起。
这里既然有了suspendCoroutineUninterceptedOrReturn函数了,为什么还要提供suspendCoroutine{}和suspendCancellableCoroutine{}函数呢,原因非常简单,因为后面2个函数只需要无脑调用continuation.resume即可往挂起函数外返回数据了,而前面这个函数需要考虑其返回值,不了解挂起原理的人很容易搞错。
其他问题
关于Continuation还有一个作用其实我们在上一篇文章说过了,它就是可以接收挂起函数的返回值,这里我们在Java调用Kotlin挂起函数时经常用到,就把它看成是一个CallBack即可。
总结
本篇文章先是大概说了Kotlin协程框架库中的一些内容,主要分为基础元素和中间层的高级概念,以及跨平台的实现。
然后说了使用suspendCoroutine{}和suspendCalcellableCoroutine{}这2个高阶函数来实现一个挂起函数,最终实现都是suspendCoroutineUninterceptedOrReturn{}这个高阶函数,它是编译器实现的内建函数。
而在这过程中,Continuation这个基础元素非常重要,它不仅可以创建挂起函数,通过resume方法往协程外传递数据,还可以通过new其匿名内部类来获取一个挂起函数的返回结果。
理解挂起函数的原理非常重要,对后面协程的原理知识分析起了决定性作用,continuation在挂起函数之间传递,调用resume方法会进入调用者函数的状态机以继续后续操作。