前面我们讲挂起函数的时候,讲过:suspend函数经过编译之后会多出来一个Continuation<T>
类型的参数。本篇文章将详细探讨一下,Continuation是干啥的。
主要作用有2个:
- 实现挂起函数:在挂起函数的内部使用Continuation这个callback传递数据给外部
- 调用挂起函数时的传参:在调用挂起函数的时候,需要传递一个Continuation,用来接收挂起函数执行结果
实现挂起函数时,一般会需要用到suspendCoroutine
或者suspendCancellableCoroutine
,它们用法差不多,只是suspendCancellableCoroutine
提供了一个CancellableContinuation,可以手动取消。谷歌建议我们尽可能使用suspendCancellableCoroutine
,因为协程的取消是可控的。
因为用法差别不大,我这里使用suspendCoroutine
来举例子,实现挂起函数:
fun main(): Unit = runBlocking {
val name = getUserName()
println(name)
}
suspend fun getUserName() = suspendCoroutine { continuation ->
thread {
Thread.sleep(2000L)
continuation.resume("云天明")
}
}
我在suspendCoroutine内部开了个线程,等待2秒后将结果通过continuation回调出去。而在runBlocking中,它等着getUserName执行完,拿到结果之后才开始执行后面的println语句。在挂起函数getUserName的内部,continuation的作用就是将挂起函数的执行结果传递出去。
思考一个问题,它是怎么拿到结果的?我明明是将结果通过continuation回调出去的,为啥外面却可以直接拿到结果了,而没有实现类似callback的东西。看过Kotlin挂起函数原理这篇文章的同学可能已经知道我要说什么了,实际上,在调用getUserName函数的地方,是传入了一个continuation的。在getUserName内部通过resume返回结果时,会回调传入的continuation的invokeSuspend方法,接着继续执行runBlocking后面的逻辑,也就是继续状态机的执行。
这里实现挂起函数的重点是suspendCoroutine,它和suspendCancellableCoroutine
都是Kotlin协程的基础元素,在Kotlin库中。它们2个原理差不多,我们分析一下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()
}
}
在内部,主要是调用了一下suspendCoroutineUninterceptedOrReturn这个高阶函数来实现主要逻辑,其他部分都是很明显的:
- 将Continuation包装了一下,包成SafeContinuation
- 调用传入的Lambda,并且将safe作为参数传进去
- 获取Continuation的值,或者抛出异常
我们重点关注一下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")
}
???卧槽?这个方法内居然没有源码,而是直接抛出异常,但是我们明明执行到它了啊,并没有抛异常。为啥?实际上,我们仔细看一下抛出的异常,它想表达的是这个方法是由Kotlin编译器来实现的。所以,我们只需要写个demo,来看下编译之后长什么样子,就行了。注意,这里的block,也就是传入的函数类型是(Continuation<T>) -> Any?
,这刚好就是上节课我们发现的suspend函数经过CPS转换之后的样子,Any?表示可能会直接返回数据,或者返回COROUTINE_SUSPENDED
。
既然suspendCoroutineUninterceptedOrReturn需要的是(Continuation<T>) -> Any?
,那我们在代码中使用该一下suspendCoroutineUninterceptedOrReturn,并且给它一个(Continuation<T>) -> Any?
,然后再反编译,不就拿到了它编译之后长什么样子了么,就像下面这样:
fun main(): Unit = runBlocking {
val name = getUserName()
println(name)
}
//直接写suspendCoroutineUninterceptedOrReturn居然不会提示导包,需要手动导包:import kotlin.coroutines.intrinsics.*
// 这里<String>可以不用写,可以推导出来,咱学东西时还是把这个写上,知道有这个东西
suspend fun getUserName() = suspendCoroutineUninterceptedOrReturn<String> { continuation ->
return@suspendCoroutineUninterceptedOrReturn "云天明"
}
注意,此处我并没有使用Continuation.resume来将结果回调出去,而是直接通过return将结果返回出去,所以它现在不是一个真正的挂起函数。
反编译一下,看看长什么样子:
public static final Object getUserName(@NotNull Continuation $completion) {
int var2 = false;
if ("云天明" == IntrinsicsKt.getCOROUTINE_SUSPENDED()) {
DebugProbesKt.probeCoroutineSuspended($completion);
}
return "云天明";
}
反编译之后,可以看到,suspendCoroutineUninterceptedOrReturn编译出来的东西非常少,仅仅是将云天明
返回出去。在return出去之前,它拿去和COROUTINE_SUSPENDED
进行比较,可以看出它是想判断一下该函数是不是真正的挂起函数,如果是挂起函数的话,是会返回COROUTINE_SUSPENDED
的。下面我们就来写一个真正的挂起函数:
fun main(): Unit = runBlocking {
val name = getUserName()
println(name)
}
suspend fun getUserName() = suspendCoroutineUninterceptedOrReturn<String> { continuation ->
thread {
Thread.sleep(2000L)
continuation.resume("云天明")
}
return@suspendCoroutineUninterceptedOrReturn COROUTINE_SUSPENDED
}
真正的挂起函数应该立刻返回COROUTINE_SUSPENDED
,而后再慢慢的去计算,计算完了再通过continuation返回真实的结果。来看下反编译之后长什么样子:
public static final Object getUserName(@NotNull Continuation $completion) {
int var2 = false;
//创建线程
ThreadsKt.thread$default(false, false, (ClassLoader)null, (String)null, 0, (Function0)(new SuspendTestKt$getUserName$2$1($completion)), 31, (Object)null);
Object var10000 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
if (var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED()) {
DebugProbesKt.probeCoroutineSuspended($completion);
}
//直接返回COROUTINE_SUSPENDED,表示该函数已挂起
return var10000;
}
//这个类是一个匿名内部类,创建来是为了传给thread,创建线程之后就会执行这个Lambda
final class SuspendTestKt$getUserName$2$1 extends Lambda implements Function0<Unit> {
final Continuation<String> $continuation;
SuspendTestKt$getUserName$2$1(Continuation<? super String> continuation) {
super(0);
this.$continuation = continuation;
}
public final void invoke() {
//这块代码是在新开的一个线程中执行的
Thread.sleep(2000);
Continuation<String> continuation = this.$continuation;
Result.Companion companion = Result.Companion;
//sleep 2秒之后将结果返回出去,通过continuation
continuation.resumeWith(Result.m23constructorimpl("云天明"));
}
}
可以看到,这就是我们写的代码,就多了个continuation参数,其他什么也没变。我们首先是创建了一个线程,然后将continuation参数传入线程的Lambda中,随后马上就返回了COROUTINE_SUSPENDED
,表示该函数已挂起。线程执行完成之后,通过continuation.resume将结果回调出去。
好了,现在我们知道suspendCoroutineUninterceptedOrReturn有什么用处了,它其实就是把Continuation这个参数暴露出来,让开发者可以使用(getUserName内部)。如果我们要实现真正的挂起函数的话,则需要返回COROUTINE_SUSPENDED
才行。
是不是有一种感觉,这suspendCoroutineUninterceptedOrReturn听着咋感觉有点像suspendCoroutine和suspendCancellableCoroutine呢。 没错,suspendCoroutine和suspendCancellableCoroutine其实就是包装了suspendCoroutineUninterceptedOrReturn,功能都是一样的。
- Continuation可以它简单的看作Callback。实现挂起函数的时候,可以通过continuation向外部回调数据。调用挂起函数时,会传一个Continuation过去,用来接收数据,且执行后面的逻辑。
- suspendCoroutine和suspendCancellableCoroutine其实就是包装了suspendCoroutineUninterceptedOrReturn。suspendCoroutineUninterceptedOrReturn会暴露一个Continuation出来,方便实现挂起(return
COROUTINE_SUSPENDED
)和 通过Continuation.resume向外传递数据。suspendCoroutineUninterceptedOrReturn包装成suspendCoroutine和suspendCancellableCoroutine是为了方便开发者使用,降低上手门槛。