背景
Kotlin协程,在使用已经很多了,对于其挂起和恢复的原理却没有深入地分析过。只了解到有一个CPS转换,将协程里的suspend方法,分割成了一个个的Continuation续体对象,然后通过回调的方式来进行恢复通知。
得空计划写一个简单的调用流程,反编译为Java代码,然后从入口处,一点点分析完整的挂起和恢复流程。测试环境为桌面端CMP项目内,版本是Kotlin2.1.0.
CPS转换
在Kotlin协程中,挂起函数的执行是通过 **Continuation Passing Style (CPS)转换** 来实现的。CPS转换是一种将函数式编程中的函数调用转换为可传递的 `Continuation` 对象的过程。这里的转换是Kotlin编译器实现的,在跨平台属性上,也保证了流程的一致性。
Kotlin协程通过将异步流程拆解为一系列 **挂起点** ,对含有 `suspend` 关键字的函数进行了 **CPS转换** ,即Continuation Passing Style转换,使其能够 **接收Continuation对象** 作为参数,并在异步操作完成后通过调用 **Continuation** 的恢复方法来继续执行协程。
在编译后的字节码中,协程的状态会被转换为状态机的形式,每个挂起点对应状态机的一个状态。当协程挂起时,它的执行状态会被保存在Continuation对象中,包括局部变量上下文和执行位置。
Continuation
`Continuation` (续体)是一个保存协程状态的对象,它记录了协程挂起的位置以及局部变量上下文,使得协程可以在任何时候从上次挂起的地方继续执行。Continuation是一个接口,它定义了 `resumeWith` 方法,用于恢复协程的执行。
interface Continuation<in T> {
val context: CoroutineContext
fun resumeWith(result: Result<T>)//result 为返回的结果
}
1. 续体是一个较为抽象的概念,简单来说它包装了协程在挂起之后应该继续执行的代码;在编译的过程中,一个完整的协程被分割切块成一个又一个续体。
1. 在suspend函数或者 await 函数的挂起结束以后,它会调用 continuation 参数的 resumeWith 函数,来恢复执行suspend函数或者await 函数后面的代码。
**CPS转换** 使得协程能够在不阻塞线程的情况下执行异步操作。当协程挂起时,线程可以被释放去执行其他任务,从而提高了系统的并发性能。此外,CPS转换使得协程的挂起和恢复操作对开发者来说是透明的,开发者可以像编写同步代码一样编写异步代码。
发生 CPS 变换的函数,返回值类型变成了 Any?,这是因为这个函数在发生变换后,除了要返回它本身的返回值,还要返回一个标记CoroutineSingletons.COROUTINE_SUSPENDED,为了适配各种可能性,CPS 转换后的函数返回值类型就只能是 Any?了。
源码分析
编写的Kotlin测试代码如下:
class MySimpleTest {
suspend fun stephenTest(): String {
delay(500L)
return "result From stephenTest"
}
}
fun callFromOutside() {
CoroutineScope(Dispatchers.IO).launch {
val result = MySimpleTest().stephenTest()
println(result)
}
}
在callFromOutside函数中,我们创建了一个协程作用域,并在其中启动了一个协程。该协程将调用stephenTest函数。而stephenTest函数是一个挂起函数,它会暂停该协程的执行,直到delay函数返回。
将这个片段反编译成java代码,删掉导包和元数据注解信息等,详细的分析过程直接见注释流程编号:
public final class MySimpleTest {
public static final int $stable;
@Nullable
// (8)stephenTest函数本来是无参的,现在有一个Continuation类型的参数
// 这个就是外部调用代码块封装成的实例,stephenTest方法执行完毕,需要继续往下执行的代码都在这个对象里面
public final Object stephenTest(@NotNull Continuation $completion) {
Continuation $continuation;
// (9)label20: 是一个Java中的标签(label),主要用于控制流程跳转。在这里它被用来实现协程的挂起和恢复机制
label20: {
if ($completion instanceof <undefinedtype>) {
$continuation = (<undefinedtype>)$completion;
// (10)用于检查当前协程是否处于挂起状态。Integer.MIN_VALUE 是一个特殊的标志位,用于标记协程是否被挂起。
// 它的值是10000000 00000000 00000000 00000000
// label首次传进来是1,即00000000 00000000 00000000 00000001,和Integer.MIN_VALUE按位与的结果为0,表示需要挂起,会走到11步,基于外部传入的 completion 对象创建一个新的ContinuationImpl对象
//=======================分割线====================
// (16)这里的label在15步被赋值成了10000000 00000000 00000000 00000001,按位与的结果是Integer.MIN_VALUE,即条件检查结果为真(即 != 0)
// 10000000 00000000 00000000 00000001减去Integer.MIN_VALUE,结果是1,即00000000 00000000 00000000 00000001
// 并将label20标签跳出循环,继续往下执行stephenTest的switch状态判断
if (($continuation.label & Integer.MIN_VALUE) != 0) {
$continuation.label -= Integer.MIN_VALUE;
break label20;
}
}
// (11)开始创建关于stephenTest代码块的ContinuationImpl对象,用于传递给下一个suspend函数
$continuation = new ContinuationImpl($completion) {
// $FF: synthetic field
Object result;
// 初始值为0
int label;
@Nullable
// (13)delay执行完,调用resumeWith,触发这个invokeSuspend方法
public final Object invokeSuspend(@NotNull Object $result) {
this.result = $result;
//(14)将label = 1和Integer.MIN_VALUE按位或,
// 运算的结果是 10000000 00000000 00000000 00000001(即 -2147483647)
this.label |= Integer.MIN_VALUE;
//(15)重入调用stephenTest函数,这次是传入 $continuation 自己作为参数。
return MySimpleTest.this.stephenTest((Continuation)this);
}
};
}
Object $result = $continuation.result;
Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
// (17) 本轮调用中,label值为1,检查无异常后,就会返回这个字符串
// "result From stephenTest"
switch ($continuation.label) {
case 0:
ResultKt.throwOnFailure($result);
$continuation.label = 1;
// (12)调用delay函数,之后就和外部调用的(3)-(7)步流程一样.
// 传入ContinuationImpl对象,delay函数内部会判断是否需要挂起,如果需要挂起,就return掉本轮stephenTest方法的调用
// 进入了delay内部执行,等500ms过后,调用外部传进来的ContinuationImpl对象的 resumeWith 函数回调
// 而resumeWith方法,必然会调用到这个ContinuationImpl 对象自己的invokeSuspend方法,就跳转到第13步了
if (DelayKt.delay(500L, $continuation) == var4) {
return var4;
}
break;
case 1:
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
return "result From stephenTest";
}
}
// CoroutineTestKt.java
public final class CoroutineTestKt {
public static final void callFromOutside() {
// (1)分析入口,从最外部的调用开始
BuildersKt.launch$default(CoroutineScopeKt.CoroutineScope((CoroutineContext)Dispatchers.getIO()), (CoroutineContext)null, (CoroutineStart)null, new Function2((Continuation)null) {
// (2)函数代码块里的任务,被封装在了继承自Continuation的一个匿名内部类对象中
// launch开始后,进入就会调用其invoke方法,并首次执行invokeSuspend方法,这时候label为0
int label;
// (18) 17步返回后,标志着 stephenTest 方法中 $continuation实例的invokeSuspend方法调用完毕
// 将调用completion的invokeSuspend方法
// (19)这时候外部的这个label值也已经为1了,就是继续往下执行了
public final Object invokeSuspend(Object $result) {
Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
Object var10000;
// (3)通过label来判断当前是到了哪一个状态
switch (this.label) {
case 0:
// (4)首先检查异常
ResultKt.throwOnFailure($result);
// (5)创建一个MySimpleTest对象,并调用其stephenTest方法
var10000 = new MySimpleTest();
// (6)将这个匿名内部类自己传进去,作为参数
Continuation var10001 = (Continuation)this;
// 将label状态设置为1,等下次再次调用invokeSuspend就会走switch的1的分支
this.label = 1;
var10000 = (MySimpleTest)var10000.stephenTest(var10001);
// (7) 如果 stephenTest 这个方法的返回值是COROUTINE_SUSPENDED,则表示该函数已暂停,我们也返回COROUTINE_SUSPENDED给调用者
// 通知这个函数是挂起函数,暂时不往下执行了
if (var10000 == var3) {
return var3;
}
// 转到MySimpleTest这个类分析 ->(8)
break;
case 1:
ResultKt.throwOnFailure($result);
var10000 = (MySimpleTest)$result;
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
// (20)挂起和恢复流程执行完毕,打印结果
String result = (String)var10000;
System.out.println(result);
return Unit.INSTANCE;
}
public final Continuation create(Object value, Continuation $completion) {
return (Continuation)(new <anonymous constructor>($completion));
}
public final Object invoke(CoroutineScope p1, Continuation p2) {
return ((<undefinedtype>)this.create(p1, p2)).invokeSuspend(Unit.INSTANCE);
}
// $FF: synthetic method
// $FF: bridge method
public Object invoke(Object p1, Object p2) {
return this.invoke((CoroutineScope)p1, (Continuation)p2);
}
}, 3, (Object)null);
}
}
以上就是我对协程挂起恢复的分析。