挂起的原理:CPS 状态机
协程的魔力在编译期发生。编译器将含有 suspend 关键字的函数通过 CPS (Continuation-Passing-Style) 变换,改写为带有状态机的普通 Java 类。
签名变换
suspend 不是魔法,它只是改变了函数的签名。
kotlin
suspend fun fetch(id: String): Data在字节码层面,它变成了:
java
// 1. 返回值变成了 Object (可能返回结果,也可能返回挂起标志)
// 2. 增加了一个 Continuation 参数 (类似 CallBack)
public Object fetch(String id, Continuation<Data> cont)状态机 (State Machine)
编译器会将协程体内的代码切割成多个片段。每遇到一个挂起点,就切一刀。
kotlin
suspend fun demo() {
print("A")
delay(1000) // 挂起点 1
print("B")
delay(1000) // 挂起点 2
print("C")
}伪代码模拟生成的逻辑:
java
class DemoStateMachine extends ContinuationImpl {
int label = 0; // 状态指针
void invokeSuspend(Object result) {
switch(label) {
case 0:
print("A");
label = 1;
// 如果 delay 返回 COROUTINE_SUSPENDED,则直接 return,释放线程
if (delay(1000, this) == COROUTINE_SUSPENDED) return;
// 否则(假如 delay 不挂起),直接流转到 case 1
break;
case 1:
print("B");
label = 2;
if (delay(1000, this) == COROUTINE_SUSPENDED) return;
break;
case 2:
print("C");
return;
}
}
}核心常量:COROUTINE_SUSPENDED
这是协程挂起的“信号弹”。
- 当一个挂起函数(如
withContext)决定挂起时,它返回COROUTINE_SUSPENDED。 - 上层状态机收到这个返回值,立即 return,从而释放当前的栈帧和线程。
- 当异步操作完成,框架调用
continuation.resume(),状态机重新再次被调用,根据 label 跳转到下一阶段。
现场保存:Locals Spilling
局部变量如何跨越挂起点存活? 答案是:溢出到堆上。 编译器会将跨越挂起点的局部变量生成为 Continuation 实现类的成员字段。
kotlin
suspend fun test() {
val a = 100 // a 在挂起点后还需要使用
delay(1000)
println(a)
}生成的类:
java
class TestContinuation {
int I$0; // 用于保存变量 a
// ... 在挂起前保存: this.I$0 = a;
// ... 在恢复后读取: int a = this.I$0;
}