
前言
大乘大圆满,你已渡过死锁天劫,掌握了协程同步原语与 ThreadLocal 迁移之道。你的协程修为,已站在 Kotlin 开发者的顶峰。
然而,真正的飞升,需要直面天道雷劫------那些隐藏在字节码与编译器源码中的终极真相。你是否曾困惑:
- 1、
suspend关键字究竟是如何让函数'挂起'的?编译器在我背后做了什么手脚?- 2、
CPS(续体传递风格)变换到底是如何将我的顺序代码切成状态机的?- 3、
Continuation的label分支是如何生成的?invokeSuspend的返回值COROUTINE_SUSPENDED究竟从何而来?- 4、协程的启动、挂起、恢复在字节码层面是怎样的一条完整链路?
这些问题的答案,只有潜入 Kotlin 编译器的源码,直视 CPS 变换的完整流程,才能彻底领悟。本讲是渡劫境的初阶修炼,也是整个修仙系列的倒数第二讲。你将:
- 亲眼见证一个简单的
suspend函数,在编译后被切分成怎样的状态机字节码。 - 逐行解析
Continuation接口与BaseContinuationImpl的resumeWith实现。 - 追踪
COROUTINE_SUSPENDED的来龙去脉,理解它为何是挂起的"信使"。 - 通过
JVM字节码(Smali/Java伪代码)还原状态机的switch-case全貌。
准备好迎接飞升前的最后雷劫了吗?我们开始。
操千曲 而后晓声,观千剑 而后识器。虐它千百遍 方能通晓其真意。
CPS 变换的本质:从顺序代码到状态机
什么是 CPS?
CPS(Continuation Passing Style,续体传递风格) 是一种编程范式:函数不通过return返回值,而是将结果传递给一个额外的回调参数(Continuation)。
Kotlin 协程的编译器会对每一个 suspend 函数进行 CPS 变换。变换后的函数签名多出一个 Continuation 参数,且返回值类型变为 Any?(可能返回真实结果,也可能返回 COROUTINE_SUSPENDED)。

一个极简挂起函数的蜕变
考虑这个最简单的挂起函数:
kotlin
suspend fun simple(): String {
delay(1000)
return "Done"
}
经过 Kotlin 编译器(kotlinc)处理后,生成的 JVM 字节码等价于以下 Java 伪代码:
java
public final Object simple(Continuation<? super String> $completion) {
// 1. 检查是否已有状态机,没有则创建
SimpleStateMachine sm = $completion instanceof SimpleStateMachine
? (SimpleStateMachine) $completion
: new SimpleStateMachine($completion);
// 2. 状态机执行入口
Object result = sm.invokeSuspend();
// 3. 如果返回 COROUTINE_SUSPENDED,直接返回给调用者
return result;
}
final class SimpleStateMachine extends ContinuationImpl {
int label = 0;
Object invokeSuspend() {
switch (label) {
case 0:
label = 1;
Object delayResult = DelayKt.delay(1000, this);
if (delayResult == COROUTINE_SUSPENDED) {
return COROUTINE_SUSPENDED;
}
// 如果没有挂起(例如时间已到),继续执行
case 1:
return "Done";
}
}
}
关键点:
label变量记录了当前执行到哪个挂起点。- 每次从
invokeSuspend返回时,要么返回真实结果,要么返回COROUTINE_SUSPENDED。 - 当挂起函数返回
COROUTINE_SUSPENDED时,调用方(调度器)知道协程已挂起,线程可以释放。
Continuation 接口与 BaseContinuationImpl 的恢复魔法
Continuation 接口
kotlin
public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}
context:当前协程的上下文。resumeWith:当协程应该恢复时被调用,传入成功或失败的结果。
BaseContinuationImpl 的核心逻辑
BaseContinuationImpl 是所有状态机的基类。它的 resumeWith 方法实现了恢复循环:
kotlin
internal abstract class BaseContinuationImpl(
val completion: Continuation<Any?>?
) : Continuation<Any?> {
protected abstract fun invokeSuspend(result: Result<Any?>): Any?
override fun resumeWith(result: Result<Any?>) {
var current = this
var param = result
while (true) {
val outcome = current.invokeSuspend(param)
if (outcome == COROUTINE_SUSPENDED) return // 挂起,退出循环
// 正常完成,将结果传递给 completion
current = current.completion as? BaseContinuationImpl ?: run {
completion?.resumeWith(outcome as Result<Any?>)
return
}
param = Result.success(outcome)
}
}
}
这个恢复循环 是协程能够"挂起-恢复-再挂起-再恢复"的核心引擎。每次 invokeSuspend 返回非 COROUTINE_SUSPENDED 时,它会自动将结果传递给父 Continuation,从而实现自动传播完成。
COROUTINE_SUSPENDED:挂起的信使
它的定义与作用
kotlin
public val COROUTINE_SUSPENDED: Any = Any()
COROUTINE_SUSPENDED 只是一个普通的单例对象。它的唯一作用是作为一个特殊标记,告诉协程调度器:"我已经挂起了,线程你可以拿去用了。"
在字节码中的体现
当 delay 这样的挂起函数决定真正挂起时,它会:
- 注册一个回调(如向
CoroutineScheduler提交延时任务)。 - 返回
COROUTINE_SUSPENDED。
状态机的 invokeSuspend 检测到这个返回值后,会立刻将其返回给 resumeWith 循环,resumeWith 看到 COROUTINE_SUSPENDED 后直接 return,从而退出恢复循环,线程被释放。
实战:用 javap 反编译挂起函数
让我们亲手反编译一个挂起函数,验证上述理论。
准备代码
kotlin
// Test.kt
import kotlinx.coroutines.delay
suspend fun test(): String {
delay(100)
return "OK"
}
编译与反编译
bash
kotlinc Test.kt -include-runtime -d Test.jar
javap -c -p TestKt.class
你将看到类似如下的字节码(简化并注释):
java
public final class TestKt {
public static final java.lang.Object test(kotlin.coroutines.Continuation);
Code:
0: aload_0
1: instanceof #8 // class TestKt$test$1
4: ifeq 15
7: aload_0
8: checkcast #8 // class TestKt$test$1
11: astore_1
12: goto 25
15: new #8 // class TestKt$test$1
18: dup
19: aload_0
20: invokespecial #12 // Method TestKt$test$1."<init>":(Lkotlin/coroutines/Continuation;)V
23: astore_1
24: ...
// 状态机的 invokeSuspend 中会有 tableswitch 指令
}
状态机类反编译(伪代码)
java
final class TestKt$test$1 extends ContinuationImpl {
int label;
Object invokeSuspend(Object result) {
switch (label) {
case 0:
label = 1;
Object r = DelayKt.delay(100, this);
if (r == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED;
case 1:
return "OK";
}
}
}

多个挂起点的状态机
当一个函数中有多个挂起点时,状态机的 label 会有更多分支。
kotlin
suspend fun multi(): String {
delay(100) // label 0 -> 1
val a = "A"
delay(200) // label 1 -> 2
val b = "B"
return a + b
}
编译后的状态机伪代码:
java
Object invokeSuspend(Object result) {
String a, b;
switch (label) {
case 0:
label = 1;
Object r1 = DelayKt.delay(100, this);
if (r1 == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED;
case 1:
a = "A";
label = 2;
Object r2 = DelayKt.delay(200, this);
if (r2 == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED;
case 2:
b = "B";
return a + b;
}
}
常见误区与避坑指南
误区 1:认为 suspend 函数一定会挂起
suspend 只是允许 挂起,是否真正挂起取决于被调用的挂起函数是否返回 COROUTINE_SUSPENDED。例如 delay(0) 可能直接返回而不挂起。
误区 2:手动调用 invokeSuspend 或 resumeWith
这些是编译器生成的方法,绝对不要手动调用。协程的恢复由调度器自动管理。
误区 3:在状态机中保存大量数据导致内存膨胀
每个挂起点之间存活的局部变量都会被状态机保存为字段。如果某个大对象在多个挂起点之间不需要,应将其作用域缩小,或手动置为 null。
最佳实践
- 理解
CPS变换:这有助于你写出更高效的挂起函数。 - 避免过深的挂起调用链:每个挂起点都有状态机开销,但通常可忽略。
- 使用
-Xdebug编译时保留参数名:方便反编译时阅读。 - 在性能敏感处,考虑使用
suspendCancellableCoroutine直接操作Continuation:减少状态机层数。
总结与下回预告
恭喜,你已成功渡过 CPS 变换的雷劫,看透了挂起函数的字节码真身!渡劫境初阶修炼完成!
本讲核心收获:
- CPS 变换将
suspend函数转换为带有Continuation参数的状态机。 - 状态机通过
label变量和switch-case实现挂起与恢复。 COROUTINE_SUSPENDED是挂起信号,触发线程释放。BaseContinuationImpl.resumeWith的恢复循环是协程自动传播完成的核心。- 通过
javap可亲手验证状态机的字节码。
在下一讲------渡劫境·中阶 ,也是本系列的倒数第二讲------中,我们将深入协程调度器的终极定制:如何用 CoroutineDispatcher 实现优先级调度 、限流令牌桶 、以及线程亲和性调度。届时你将真正拥有改造协程底层的能力。
【当前境界修为面板】
| 当前境界 | 修炼技能 | 修炼进度 | 修炼心得 |
|---|---|---|---|
| 渡劫境 · 初阶 | 1、CPS 变换真解 2、状态机字节码透视镜 3、COROUTINE_SUSPENDED 信使诀 |
当前进度 :35% 修为 :350/1000 下一突破 :[渡劫境 · 中阶] (需领悟:自定义 CoroutineDispatcher 实现优先级调度、限流令牌桶、线程亲和性) |
每个suspend函数编译后都是一台精密状态机。COROUTINE_SUSPENDED是释放线程的暗号。 |
【本讲思考题】
-
表象题 :以下挂起函数编译后,状态机的
label最大值为多少?kotlinsuspend fun example() { delay(100) delay(200) delay(300) } -
场景题:你发现某个挂起函数在调用后似乎"卡住了",没有返回也没有抛异常。你怀疑是某个挂起点永远没有恢复。如何通过查看字节码来定位问题?
-
原理题 :
BaseContinuationImpl.resumeWith的恢复循环中,为什么要在outcome != COROUTINE_SUSPENDED时继续循环,而不是直接结束?这种设计解决了什么问题?
道友,飞升前的最后一重雷劫已在眼前。掌握了调度器的终极定制,你将成为协程世界的造物主。渡劫境·中阶见。
欢迎一键四连 (
关注+点赞+收藏+评论)