前言
在文章 挂起函数原理解析中,我们把挂起函数经过CPS转换后,通过多出的Continuation变量,以及巧妙的状态机模型,来实现挂起函数的调用。
在文章 # 协程(16) | 优雅地实现一个挂起函数中,我们通过suspendCancellableCoroutine{}高阶函数里面暴露的Continuation接口对象,调用其resume方法来实现挂起函数,往往用于实现函数内部的逻辑的。
所以这里的关键就是Continuation接口和suspendCancellableCoroutine{}方法,在之前文章我们分析过该接口的作用,我们以它抽象挂起函数挂起和恢复的角度来看的,这篇文章我们先来看看Continuation的作用。
正文
话不多说,我们先来看看这个Continuation到底有什么用。
Continuation的作用
其实我们在前面就使用过Continuation的第一种用法了,就比如下面代码,我们使用扩展函数的方式,让我们的程序支持了挂起函数:
/**
* 把原来的[CallBack]形式的代码,改成协程样式的,即消除回调,使用挂起函数来完成,以同步的方式来
* 完成异步的代码调用。
*
* 这里的[suspendCancellableCoroutine] 翻译过来就是挂起可取消的协程,因为我们需要结果,所以
* 需要在合适的时机恢复,而恢复就是通过[Continuation]的[resumeWith]方法来完成。
* */
suspend fun <T : Any> KtCall<T>.await(): T =
suspendCancellableCoroutine { continuation ->
//开始网络请求
val c = call(object : CallBack<T> {
override fun onSuccess(data: T) {
//这里扩展函数也是奇葩,容易重名
continuation.resume(data)
}
override fun onFail(throwable: Throwable) {
continuation.resumeWithException(throwable)
}
})
//当收到cancel信号时
continuation.invokeOnCancellation {
c.cancel()
}
}
这个是在我们之前文章中介绍过的例子,可以把Callback消除,使用挂起函数来实现以同步的方式写异步的代码。在这里的核心就是通过suspendCancellableCoroutine{}高阶函数所暴露的continuation,向外部传递数据。
这个例子可能有点复杂,我们写个更简单的例子:
fun main() = runBlocking {
val length = getLengthSuspend("Hello")
println(length)
}
/**
* 使用[suspendCancellableCoroutine]实现挂起函数,模拟耗时后返回文本的长度
* @param text 测试文本
* */
suspend fun getLengthSuspend(text: String): Int =
suspendCancellableCoroutine { continuation ->
thread {
//模拟耗时
Thread.sleep(2000)
continuation.resume(text.length)
}
}
在这个例子中,你或许有点疑惑,为什么挂起函数中以continuation.resume的方式异步传出的结果,在main()函数调用时,就可以收到结果呢?
这时我们利用挂起函数原理那节的知识,对上面main()函数的调用进行改写:
fun main(){
val func = ::getLengthSuspend as (String,Continuation<Int>) -> Any?
func("Hello",object : Continuation<Int>{
override val context: CoroutineContext
get() = EmptyCoroutineContext
override fun resumeWith(result: Result<Int>) {
println(result.getOrNull())
}
})
//防止程序退出
Thread.sleep(5000)
}
其实这里的Continuation就类似与Callback,所以回到了之前刚学习挂起函数的一个观点:挂起函数的本质还是Callback。
综上所述,Continuation的作用就相当于Callback,它既可以用于实现挂起函数,向挂起函数外传递结果;也可以用于调用挂起函数,我们可以创建Continuation的匿名内部类,来接收挂起函数返回的结果。
suspendCoroutineUnintercepedtOrReturn函数解析
既然知道了Continuation的作用,就相当于是一个Callback,那挂起函数的核心就落到了我们常用的suspendCoroutine{}和suspendCancellableCoroutine{}这2个高阶函数身上了,经过简单查看源码,这2个函数的核心都是调用了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编译器来实现的。
这里我们会发现,这个类所在的方法是Intrinsic.kt,并且上面函数抛出的异常也说该方法是intrinsic的,这个英语单词的意思是固有、本质的意思,但是在这里表示的是编译器领域的一个术语,可以理解为内建,即suspendCoroutineUninterceptedOrReturn{}函数是一个编译器内建函数,它是由Kotlin编译器来实现的。
关于这个函数的具体实现,我们就不继续往下深究了,但是我们发现它的参数block可以接收一个Lambda,同时这个block的函数类型是(Continuation<T>) -> Any?,这个Any?是否有一点熟悉,在挂起函数原理那一节中我们通过CPS转换后得到的状态机代码,就是通过挂起函数的返回值来切换状态机的。
当时我们说的这个返回值类型之所以是Any?,是因为当挂起函数是伪挂起函数时,返回函数值;当是挂起函数时,返回固定值,表示它已经被挂起。
其实上面block这个lambda的函数类型返回值,也是这个意思,我们来验证一下,测试代码如下:
/**
* 测试实现伪挂起函数,这里值得注意的是[suspendCoroutineUninterceptedOrReturn]的函数类型
* [block]是通过[crossinline]修饰的,通过该关键字修饰的高阶函数类型,在里面是不能直接使用[return]的,
* 必须要return特定的作用域。
* */
private suspend fun testNoSuspendCoroutine() =
suspendCoroutineUninterceptedOrReturn<String> { continuation ->
return@suspendCoroutineUninterceptedOrReturn "Hello"
}
fun main() = runBlocking {
val length = testNoSuspendCoroutine()
println(length)
}
在这里我们没有通过continuation.resume()方法来设置返回值,而是直接return了结果Hello,关于上面注释中说的crossinline关键字,可以查看文章:# Kotlin的inline、noinline、crossinline全面分析1。
上面代码执行结果可以直接得到Hello,我们可以直接把上面代码进行反编译,如下:
private static final Object testNoSuspendCoroutine(Continuation $completion) {
int var2 = false;
if ("Hello" == IntrinsicsKt.getCOROUTINE_SUSPENDED()) {
DebugProbesKt.probeCoroutineSuspended($completion);
}
return "Hello";
}
看到这里你是不是恍然大悟了,我们前面的suspendCoroutineUninterceptOrReturn{}方法没了,转而是普通的函数,在这里面加入了判断是否挂起的逻辑,然后直接返回结果。
这个方法结合 # 协程(15) | 挂起函数原理解析中说的状态机模型,当挂起函数调用这个方法时,发现是非挂起,就可以直接返回结果了。
这个时候,我们来写一个真正的挂起函数:
/**
* 这里真正使用[Continuation]来往挂起函数外传递了值
* 同时,函数返回值范围了挂起标志位
* */
private suspend fun testSuspendCoroutine() =
suspendCoroutineUninterceptedOrReturn<String> { continuation ->
thread {
Thread.sleep(1000)
continuation.resume("Hello")
}
return@suspendCoroutineUninterceptedOrReturn kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
}
fun main() = runBlocking {
val length = testSuspendCoroutine()
println(length)
}
这里,我们使用continuation向外部传递了函数返回值,同时在return时返回了代表挂起函数被挂起的标志位。
我们依旧把上面代码进行反编译,得到如下代码:
private static final Object testSuspendCoroutine(Continuation $completion) {
int var2 = false;
//注释1
ThreadsKt.thread$default(false, false, (ClassLoader)null, (String)null, 0, (Function0)(new KtContinuationKt$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 KtContinuationKt$testSuspendCoroutine$2$1 extends Lambda implements Function0 {
// $FF: synthetic field
final Continuation $continuation;
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
this.invoke();
return Unit.INSTANCE;
}
public final void invoke() {
//注释4
Thread.sleep(1000L);
Continuation var1 = this.$continuation;
String var2 = "Hello";
Companion var10001 = Result.Companion;
//注释5
var1.resumeWith(Result.constructor-impl(var2));
}
KtContinuationKt$testSuspendCoroutine$2$1(Continuation var1) {
super(0);
this.$continuation = var1;
}
}
上面代码不难理解,分析如下:
- 注释2和注释3,会把
var10000赋值为挂起标志位,然后直接return该值,调用它的挂起函数就会知道该函数已经被挂起,即会等待进入新一轮状态机。 - 注释1、4、5就是开启了一个线程,在线程经过1000ms后,通过
continuation的resumeWith方法把值传递到函数外。
这里再结合状态机的模型,外部挂起函数调用该函数时,就会发现该函数是真挂起了,会传递唯一的continuation进去,等待resumeWith回调,从而进入下一个状态机分支。
到这里,我们也可以总结一下suspendCoroutineUninterceptedOrReturn{}这个高阶函数的作用了:它可以将挂起函数中的Continuation以参数的形式暴露出来,在它的lambda中,我们可以直接返回结果,这时是一个伪挂起函数;也可以使用返回COROUTINE_SUSPENDED这个挂起标志位,然后使用resume()传递结果。
最重要的是,里面的状态机逻辑是Kotlin编译器帮我们实现的,经过反编译后我们可以发现该函数不见了,取而代之的是上一节文章所说的状态机原理。
suspendCoroutine{}和suspendCancellableCoroutine{}高阶函数
通过本篇文章前面的学习,我们基本就非常清晰地明白挂起函数的原理了,但是我们经常使用的用来实现挂起函数的高阶函数却是suspendCoroutine{}和suspendCancellableCoroutine{},我们来看看这2个函数的源码:
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()
}
}
public suspend inline fun <T> suspendCancellableCoroutine(
crossinline block: (CancellableContinuation<T>) -> Unit
): T =
suspendCoroutineUninterceptedOrReturn { uCont ->
val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)
cancellable.initCancellability()
block(cancellable)
cancellable.getResult()
}
分析如下:
- 首先这2个方法都是使用
suspendCoroutineUninterceptedOrReturn{}来实现的,只是对代码执行加了一些额外判断,比如suspendCoroutine{}中调用block(safe)加了安全判断,在suspendCancellableCoroutine{}中调用block(cancellable)来看一响应取消。 - 其次就是这俩个方法的返回值,都是
T,这就可以极大地减少我们使用的成本。毕竟如果直接使用suspendCoroutineUninterceptedOrReturn{}的话,需要开发者知道挂起函数的状态机原理,指定返回特定的返回值。
总结
学习完本篇文章,再结合之前挂起函数的原理,我相信肯定会有一种恍然大悟的感觉。简单概况就是Continuation本质就是Callback,既可以用于实现挂起函数,对外传递返回值;也可以实现其接口,接收挂起函数的返回值。
而suspendCoroutineUninterceptedOrReturn是编译器内建函数,是我们能接触的创建挂起函数最底层的函数了,该函数主要就是实现了之前文章所介绍的状态机逻辑。