1) 大图景:CPS 改写 + 堆上栈(stackless)
-
思想 :suspend 会被编译器做 Continuation-Passing Style (CPS) 转换------
原本"直接返回"的函数,被改写成"把结果回调给一个 Continuation"。
-
结果 :每个含挂起点的 suspend 函数/λ 会被"状态机化":
-
局部变量、下一步要从哪行继续(label)都放进生成的对象里(堆上保存"栈帧")。
-
真正"继续执行"靠调用 Continuation.resumeWith(result),像是把程序计数器跳回 switch(label) 的某一分支。
-
Continuation(简化):
kotlin
interface Continuation<in T> {
val context: CoroutineContext
fun resumeWith(result: Result<T>)
}
2) 签名改写:隐式多了一个 Continuation 参数
示例(源代码):
kotlin
suspend fun foo(a: Int): String {
delay(100)
return a.toString()
}
编译后(概念化伪代码):
javascript
Object foo(int a, Continuation<? super String> cont) {
// 可能立即返回值,也可能返回 COROUTINE_SUSPENDED
}
- 返回类型被抹成 Object,用来承载 真实结果 或 COROUTINE_SUSPENDED。
- 没有挂起点 时,仍会多一个 Continuation 形参,但实现通常是直返(不建状态机)。
3) 生成的"状态机类":ContinuationImpl/SuspendLambda
两种常见形态:
-
挂起 λ(suspend {}) :生成类继承 SuspendLambda,实现 FunctionN,核心逻辑在 invokeSuspend()。
-
挂起函数 :编译器会为它生成一个内部 ContinuationImpl 子类,封装局部变量、label、异常等;继续执行时也走 invokeSuspend()。
典型骨架(伪代码):
typescript
final class Foo$1 extends ContinuationImpl {
int label; // 状态机的程序计数器
Object L$0, L$1; // 若干个槽保存局部变量/参数
Object result; // 上一次 resumeWith 传入的结果/异常
Foo$1(Continuation parent) { super(parent); }
public Object invokeSuspend(Object param) {
this.result = param;
switch (label) {
case 0: {
// 初次进入
// 保存局部,设置下一状态
label = 1;
Object r = delay(100, this);
if (r == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED;
// fallthrough 到状态1逻辑
}
case 1: {
// 从 delay 返回后继续
int a = (int) L$0;
return a + ""; // 返回最终结果
}
default:
throw new IllegalStateException("call to 'resume' before 'invoke'");
}
}
}
要点
- 每遇到一个挂起点 ,就把需要的活跃局部写入 L$* 槽,设置 label = 下一状态,然后调用下游挂起函数。
- 如果下游返回 COROUTINE_SUSPENDED,就立刻把 COROUTINE_SUSPENDED 原样向上返回;真正的恢复发生在以后某个 resumeWith。
4) 挂起点如何"切片"
以 delay(100) 为例(简化):
ini
label = 1
L$0 = a
val r = delay(100, this)
if (r == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
// 否则继续向下执行(说明立刻完成,没有真的挂起)
恢复时序:
- 定时器到点 → 调度器拿到原 Continuation → 调 resumeWith(Result.success(Unit));
- 跳回 invokeSuspend(),result = Unit,switch(label) 命中 case 1;
- 从 L$0 里把 a 读回,继续执行。
5)COROUTINE_SUSPENDED与resumeWith
-
COROUTINE_SUSPENDED 是个哨兵对象:表示"我挂起了,别往下跑了,等我 resumeWith 再说"。
-
resumeWith(Result) 里封装成功值 或异常:
- 成功:Result.success(value) → 下一次 invokeSuspend(param=value);
- 失败:Result.failure(e) → 下一次 invokeSuspend(param=Failure(e)),随后 throw 出来或在 try/catch 中处理。
6) 调度器/拦截器:ContinuationInterceptor
-
Continuation 在恢复前会经 intercepted() 包一层(例如 DispatchedContinuation),由 ContinuationInterceptor (如 Dispatchers.Main/IO)控制在哪个线程恢复。
-
构建器 launch/async 会创建一个起始 Continuation (如 ScopeCoroutine/StandaloneCoroutine),并把 block 的 Continuation 链接起来形成"堆上调用栈"。
启动路径(概念):
scss
block.startCoroutine(receiver, completion) // 或
block.startCoroutineUninterceptedOrReturn(completion) // 内联优化路径
7) 取消/异常如何穿透状态机
-
取消本质是向上游 抛出 CancellationException ,所有挂起点都是可取消的"安全点"。
-
你的 try/finally 会被拆成状态机的多个 label,从而保证 finally 一定运行(无论正常返回、异常还是取消)。
示例(保证释放):
kotlin
suspend fun use() {
val r = acquire()
try {
delay(1000) // 这里若取消,会在恢复路径里跳到 finally
} finally {
r.close()
}
}
8) inline/无挂起点/尾调用 等细节
- inline suspend 函数 :主体会内联到调用处;若内部仍有挂起点,状态机依旧存在,但"归属"到调用处的那一套 Continuation。
- 无挂起点的 suspend :虽然签名多了 Continuation,但不会生成状态机,通常直接计算后返回结果。
- 尾部挂起优化:编译器/标准库会尽量通过 startCoroutineUninterceptedOrReturn 走更少包装,减少切换成本。
- label | Int.MIN_VALUE 位标记 :反编译常见到把最高位 OR 上,表示"这是恢复路径"(debug/probe 用),理解为恢复态标识即可。
9) 与 Flow/Channel 的衔接(心智模型)
- suspend 是基本指令;Flow 运算符本身也是一堆 suspend 挂起点串起来的状态机。
- Channel 在 send/receive 处会挂起;因此它们都依赖上述"保存现场→返回 COROUTINE_SUSPENDED→稍后 resumeWith"的流程。
10) 一个完整示例(源代码 → 伪反编译)
源:
kotlin
suspend fun twoDelays(a: Int): String {
delay(10)
val x = a + 1
delay(20)
return "v=$x"
}
伪反编译(高度简化):
typescript
Object twoDelays(int a, Continuation<? super String> cont) {
class SM extends ContinuationImpl {
int label;
Object result;
int L$0; // 保存 a
int L$1; // 保存 x
SM(Continuation parent) { super(parent); }
public Object invokeSuspend(Object param) {
result = param;
switch (label) {
case 0: {
label = 1;
L$0 = a;
if (delay(10, this) == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED;
}
case 1: {
int x = L$0 + 1;
L$1 = x;
label = 2;
if (delay(20, this) == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED;
}
case 2: {
return "v=" + L$1;
}
}
throw new IllegalStateException();
}
}
SM sm = (cont instanceof SM) ? (SM) cont : new SM(cont);
return sm.invokeSuspend(Unit.INSTANCE);
}
观察点:两个挂起点 → 三段代码块 → 两次设置 label 与保存局部。
11) 实战调试/读懂状态机的小贴士
- 反编译看 invokeSuspend 的 switch(label):每个 case 即一个"继续点"。
- 查看是否存在 COROUTINE_SUSPENDED 的快速返回;它就是"真挂起"的判定。
- L$* 槽里是被拆散的局部;label 是当下所在阶段。
- 若 finally 没执行,大概率是你在挂起函数外侧做了资源管理(应把清理放到 suspend/withContext 封闭的作用域里)。
一句话总结
suspend = 把"同步栈"拆成"堆上状态机 + Continuation 回调链" :
进入挂起点前存档(局部变量 + 下一步的 label),返回 COROUTINE_SUSPENDED;
等到外界 resumeWith,再按 label 读档,在正确的线程由调度器继续执行。