状态机到底长什么样?——从手写 state 到编译器生成的 Continuation
// 你希望这样写:
void loginAndFetch() {
String token = requestToken("user", "pass"); // ① 网络请求,慢
String data = fetchData(token); // ② 又一个网络请求
show(data); // ③ 展示
}
三行代码,清晰直观。但每一行都可能要等网络,如果按普通函数写,线程就卡住了。
class LoginAndFetchStateMachine {
int state = 0; // 当前跑到哪一步
String token; // 局部变量 token → 变成字段
String data; // 局部变量 data → 变成字段
void resume() {
switch (state) {
case 0:
// ↓ 开始 ①:发起网络请求
state = 1;
httpClient.sendAsync("POST", "/login", "user:pass",
result -> {
this.token = result; // 结果存进字段
this.resume(); // 回来继续
});
return; // ← 暂停!人走了,但 token, state 留在堆上
case 1:
// ↓ 网络请求回来了,接着跑 ②
state = 2;
httpClient.sendAsync("GET", "/data?token=" + this.token,
result -> {
this.data = result;
this.resume();
});
return; // ← 又暂停!
case 2:
// ↓ 都拿到了
show(this.data);
return;
}
}
}
你眼睛看到的:顺序三行。计算机实际跑的:三个 case,每次跑一个就出去了。
suspend fun loginAndFetch() {
val token = requestToken("user", "pass") // suspend 函数
val data = fetchData(token) // suspend 函数
show(data)
}
Continuation是 Kotlin 里状态机的官方叫法——「续体」,意思是"待续的执行体"。
// 编译器生成(伪代码,展示核心结构)
class LoginAndFetchContinuation implements Continuation {
int label = 0;
Object result; // 上一个 suspend 函数的返回值
Object token; // 局部变量 → 字段
Object data; // 局部变量 → 字段
@Override
Object invokeSuspend(Object result) {
this.result = result;
switch (label) {
case 0:
// val token = requestToken("user", "pass")
label = 1;
Object r1 = requestToken("user", "pass", this); // ← this = 续体!
if (r1 == COROUTINE_SUSPENDED) { // ← 如果返回了暂停标记
return COROUTINE_SUSPENDED; // 那就真暂停
}
// 没暂停,往下走(fall through)
case 1:
token = result; // requestToken 的结果(通过 result 传进来了)
label = 2;
Object r2 = fetchData(token, this);
if (r2 == COROUTINE_SUSPENDED) {
return COROUTINE_SUSPENDED;
}
case 2:
data = result;
show(data);
return Unit.INSTANCE; // 完成
}
}
}
Kotlin 标准库中 Continuation 的真实定义:
// Kotlin 标准库源码
public interface Continuation<in T> {
// 续体所在的协程上下文
public val context: CoroutineContext
// 恢复续体:传入上一个挂起函数的结果
public fun resumeWith(result: Result<T>)
}
每个 suspend 函数编译后都会多一个 Continuation参数,这个参数就是调用者的续体——「我跑完了把结果交给谁」。
// 你写的:
suspend fun requestToken(user: String, pass: String): String
// 编译后(伪代码):
fun requestToken(user: String, pass: String, continuation: Continuation): Any?
// ↑ 多了这个参数
// 返回 Any? 因为可能返回实际结果(String)或 COROUTINE_SUSPENDED
COROUTINE_SUSPENDED是一个单例标记对象,定义在 Kotlin 标准库中:
// Kotlin 标准库中的定义
@SinceKotlin("1.3")
public val COROUTINE_SUSPENDED: Any get() = CoroutineSingletons.COROUTINE_SUSPENDED
internal enum class CoroutineSingletons {
COROUTINE_SUSPENDED,
UNDECIDED,
RESUMED
}
suspend 函数返回两种东西:
数据已经到了(缓存命中 / 已经准备好了)
→ 返回真实数据,调用者直接继续往下走
→ 没有真正挂起
还没好,我先挂起来了,回头叫你
→ 返回这个标记,调用者一路 return 出去
→ 真挂起,线程去干别的
因为协程挂起后线程就离开了。如果局部变量在栈上,线程一走栈帧弹出,变量就没了。
只有放在堆上的对象里,线程离开后变量才能继续存活,等下次线程(可能是另一个线程)回来继续用。
这就是「闭包捕获」的升级版:编译器把所有跨挂起点的局部变量全部提升为类的成员字段。这就是为什么 Kotlin 协程里你完全不需要关心变量生命周期——编译器全替你处理好了。
如果你把 Kotlin 的 suspend 函数编译后反编译成 Java,你会看到:
// 反编译后的 Java 伪代码(简化)
public final class LoginAndFetchKt {
public static Object loginAndFetch(Continuation $continuation) {
// 类型转换,拿到状态机对象
LoginAndFetchContinuation cont = (LoginAndFetchContinuation) $continuation;
switch (cont.label) {
case 0:
// 第一个挂起点之前
cont.label = 1;
Object result = requestToken("user", "pass", cont);
if (result == Intrinsics.COROUTINE_SUSPENDED) {
return Intrinsics.COROUTINE_SUSPENDED;
}
// fall through
case 1:
// token 拿到,继续
cont.token = (String) cont.result;
cont.label = 2;
Object result2 = fetchData(cont.token, cont);
if (result2 == Intrinsics.COROUTINE_SUSPENDED) {
return Intrinsics.COROUTINE_SUSPENDED;
}
// fall through
case 2:
cont.data = (String) cont.result;
show(cont.data);
return Unit.INSTANCE;
}
}
// 编译器生成的状态机内部类
static final class LoginAndFetchContinuation extends ContinuationImpl {
Object token; // 局部变量"提升"为字段
Object data; // 局部变量"提升"为字段
int label;
}
}
关键观察:
Continuation参数Object(因为可能返回 COROUTINE_SUSPENDED 或实际值)协程 = 编译器把你的函数切成 N 个 case + 每个 suspend 调用都是挂起点,调用时把续体(this)传下去 + 返回 COROUTINE_SUSPENDED → 真挂起,线程走人 + 数据到了 → 回调调续体 → 从下一个 case 继续 + 所有局部变量变成类字段,活在堆上,不怕暂停。