用了几年协程,你真的理解
suspend背后发生了什么吗?
一、一个简单的例子
我们先写一个最普通的 suspend 函数:
kotlin
// Test.kt
suspend fun fetchData() {
println("step 1")
delay(1000L)
println("step 2")
}
二、反编译
编译之后,可以用 javap 命令反编译 class 文件查看编译结果:
bash
javap -c -p TestKt.class
javap -c -p 'TestKt$fetchData$1.class'
编译器在编译期会生成一个匿名类(状态机)和一个静态方法(状态分发),如下:
java
// 匿名类(状态机):
final class TestKt$fetchData$1 extends ContinuationImpl {
Object result; // 上一个 suspend 调用的返回值
int label; // 当前执行到哪个挂起点(0、1)
public TestKt$fetchData$1(Continuation completion) {
super(completion);
}
@Override
public Object invokeSuspend(Object $result) {
this.result = $result;
this.label |= Integer.MIN_VALUE;
return TestKt.fetchData(this);
}
}
// 静态方法(状态分发):
public static final Object fetchData(Continuation<? super Unit> $completion) {
// 第一步:创建或复用状态机对象
TestKt$fetchData$1 sm;
if ($completion instanceof TestKt$fetchData$1) {
sm = (TestKt$fetchData$1) $completion;
if ((sm.label & Integer.MIN_VALUE) != 0) {
sm.label -= Integer.MIN_VALUE;
}
} else {
sm = new TestKt$fetchData$1($completion);
}
// 第二步:状态分发 ------ 本质上就是 switch-case
switch (sm.label) {
case 0: // 初始状态
ResultKt.throwOnFailure(sm.result);
System.out.println("step 1");
sm.label = 1;
if (delay(1000L, sm) == COROUTINE_SUSPENDED) return null;
break;
case 1: // 从 delay 恢复后
ResultKt.throwOnFailure(sm.result);
System.out.println("step 2");
return Unit.INSTANCE; // 返回结果
}
throw new IllegalStateException("call to 'resume' before 'invoke'");
}
三、执行过程
3.1 调用入口
假设我们在协程作用域中调用了 fetchData():
经过协程的一系列调用和转换(scope.launch { fetchData() } → ... → startCoroutineCancellable → resumeWith(Unit)),最终会调用到 TestKt$fetchData$1.invokeSuspend(Unit) 这个方法:
java
// 前面生成的匿名类的方法
@Override
public Object invokeSuspend(Object $result) {
this.result = $result;
this.label |= Integer.MIN_VALUE; // 标记"正在执行"
return TestKt.fetchData(this); // 调用状态分发器
}
fetchData(this) 中的 this 就是状态机本身 。
这时 label == 0,所以进入 tableswitch 的 case 0 分支:
java
case 0: // 初始进入 fetchData,sm.label == 0
ResultKt.throwOnFailure(sm.result); // 检查上次是否异常
System.out.println("step 1");
sm.label = 1; // ← 记录下一点:下次进来走 case 1
if (delay(1000L, sm) == COROUTINE_SUSPENDED) {
return null; // ← 挂起!函数返回了
}
break;
线程没有被阻塞,方法调用从 fetchData 函数 return null 了就结束了。
sm 传给了 delay,相当于传过去了一个回调函数。
delay 内部开了个定时器,1 秒后回调 sm.resumeWith(Result.success(Unit)) 恢复执行。
3.2 再次调用 invokeSuspend
1 秒后定时器触发:
kotlin
// delay 的实际行为(简化示意)
// delay 不阻塞线程,它往 Dispatcher 的定时器队列注册了一个回调
// 1 秒后 Dispatcher 触发该回调,恢复续体:
sm.resumeWith(Result.success(Unit))
// sm.resumeWith 内部执行了:
sm.invokeSuspend(Unit)
// sm.invokeSuspend(Unit) 内部执行了:
fetchData(sm)
此时 sm.label == 1,所以执行 label 1 的分支:
java
case 1: // 从 delay 恢复后
ResultKt.throwOnFailure(sm.result); // 检查 Unit,正常
System.out.println("step 2");
return Unit.INSTANCE; // 执行完毕,返回结果
函数执行完了,Unit.INSTANCE 会一层层向上传,协程结束。
四、简单总结
- 协程挂起的本质还是回调,只是提供了语法糖,可以写出同步风格的代码。
- 挂起就是给别的函数传入了一个回调方法,然后直接 return,不阻塞线程。
- 回调的时候根据
label来判断执行哪个 case 分支代码。