从反编译代码彻底读懂 Kotlin 协程:suspend 挂起与恢复全链路推演
本文约 15 分钟阅读。建议先收藏,遇到
suspend原理问题再回来查。适合谁:会用
launch,但对反编译里的label37、invokeSuspend仍有疑惑的 Android 开发者。
目录
- 1. 一份让我怀疑人生的反编译代码
- 2. 业务源码:我们在分析什么
- 3. 协程解决了什么问题
- 4. 四层架构:谁负责什么
- 5. CPS 变换:suspend 编译后的第一件事
- 6. 状态机:为什么 return 了还能继续执行
- 7. Continuation:用 OkHttp 回调来理解
- 8. label37 逐行解读:首次调用 vs 恢复调用
- 9. 符号位技巧:编译器的 Sign Bit Trick
- 10. 第一次执行:从 case 0 到全线挂起
- 11. getUserInfo 深度拆解:withContext、var10001、delay
- 12. 挂起如何一层层传播
- 13. 恢复全链路:delay 到点后发生了什么
- 14. invokeSuspend 与 resumeWith:谁调谁
- 15. Continuation 嵌套:快递驿站模型
- 16. 第二次进入 testCoroutine:case 1 走读
- 17. 协程挂起 vs 线程阻塞
- 18. 业务字节码 vs 协程库:看得到什么
- 19. 常见误区 TOP 10
- 20. 调试技巧
- 21. 面试口语版 + 30 秒速记
- 22. 结语
1. 一份让我怀疑人生的反编译代码
在 Android Studio 里反编译 LoginViewModel,本想看 ProGuard 有没有混淆方法名,结果看到:
label37: {
if ($completion instanceof <undefinedtype>) {
$continuation = (<undefinedtype>)$completion;
if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) {
((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
break label37;
}
}
$continuation = new ContinuationImpl($completion) {
Object L$0;
Object result;
int label;
public Object invokeSuspend(Object $result) {
this.result = $result;
this.label |= Integer.MIN_VALUE;
return LoginViewModel.this.testCoroutine((Continuation) this);
}
};
}
switch (((<undefinedtype>)$continuation).label) {
case 0: ...
case 1: ...
}
三个问题立刻冒出来:
| 疑问 | 错误直觉 | 正确答案 |
|---|---|---|
label37 是什么? | 状态机的 label | Java 带标签代码块,和 int label 无关 |
invokeSuspend 谁调的? | JVM 底层 | 协程库 resumeWith 触发 |
返回 SUSPENDED 后为何不执行 println? | 代码丢了 | 挂起时故意 return,恢复后从 case 1 继续 |
本文就是把这三个问题,从反编译代码一路追到 Kotlin 编译器 + kotlinx-coroutines 的完整笔记。
2. 业务源码:我们在分析什么
反编译代码对应的 Kotlin:
class LoginViewModel : ViewModel() {
suspend fun testCoroutine() {
println("start")
val user = getUserInfo()
println(user)
val friendList = getFriendList(user)
println(friendList)
val feedList = getFeedList(friendList)
println(feedList)
}
suspend fun getUserInfo(): String {
withContext(Dispatchers.IO) { delay(1000) }
return "BoyCoder"
}
suspend fun getFriendList(user: String): String {
withContext(Dispatchers.IO) { delay(1000) }
return "Tom, Jack"
}
suspend fun getFeedList(friendList: String): String {
withContext(Dispatchers.IO) { delay(1000) }
return "{FeedList..}"
}
}
调用:
viewModelScope.launch {
testCoroutine()
}
三步串行,每步 delay(1s),总耗时约 3 秒。
本文主战场:
launch → testCoroutine → getUserInfo → withContext(IO) → delay(1000)
搞懂这一条,其他 suspend 函数都是同一套模板。
3. 协程解决了什么问题
回调:嵌套深,难读。
Thread.sleep:阻塞线程,线程贵(栈 ~1MB)。
协程:
viewModelScope.launch {
val user = getUserInfo() // 看起来像同步
println(user)
}
表面上顺序执行,实际上编译器把代码 切成 case 0、1、2…,挂起点先 return,异步完成后再从断点继续。
核心承诺:用同步写法做异步的事;挂起时不阻塞线程。
4. 四层架构:谁负责什么
第 1 层 Kotlin 源码 suspend fun、顺序代码
↓ kotlinc
第 2 层 编译产物(反编译可见) CPS + 状态机 + invokeSuspend
↓ 被协程库调用
第 3 层 kotlinx-coroutines launch、delay、resumeWith
↓ 使用线程池/Handler
第 4 层 JVM / OS 只负责执行 Runnable
后面很多「为什么 ViewModel 里看不到 xxx」的答案都在这里:
- 状态机、label → 第 2 层,反编译能看到
- resumeWith、delay 定时 → 第 3 层,在协程库源码里
- JVM 不会特殊调用 invokeSuspend
5. CPS 变换:suspend 编译后的第一件事
CPS(Continuation-Passing Style):把「接下来要做什么」作为 Continuation 参数传入。
// 源码
suspend fun getUserInfo(): String
// 编译后(概念等价)
fun getUserInfo(completion: Continuation): Any?
| 变化 | 原因 |
|---|---|
多 Continuation 参数 | 承载挂起后的剩余逻辑 |
返回 Any? | 可能是真结果,也可能是挂起标志 |
两种出口:
快路径:return "BoyCoder"
慢路径:return COROUTINE_SUSPENDED → 结果稍后通过 resumeWith 交付
COROUTINE_SUSPENDED 是单例哨兵,语义:「还没算完,别往下走,也别阻塞线程。」
6. 状态机:为什么 return 了还能继续执行
JVM 方法 return 后栈帧销毁,局部变量没了。协程要把 执行位置 + 局部变量 搬到堆上 → 有限状态机。
testCoroutine 概念上的伪代码:
when (label) {
0 -> { println("start"); label = 1; getUserInfo(cont) }
1 -> { val user = cont.result; println(user); label = 2; getFriendList(user, cont) }
2 -> { ... }
3 -> { ... }
}
反编译里的 switch (label) 就是这个 when。
状态机对象(ContinuationImpl)典型字段:
| 字段 | 作用 |
|---|---|
label | 当前状态,像程序计数器 |
result | 恢复时子调用的返回值 |
L$0 | spill:跨挂起点保留的引用(如 this) |
invokeSuspend | 恢复时的重入入口 |
7. Continuation:用 OkHttp 回调来理解
| OkHttp | 协程 |
|---|---|
Callback | Continuation |
onResponse(data) | resumeWith(Result.success(data)) |
| 回调里的代码 | 状态机 case 1、2 里的代码 |
Continuation = 回调对象;resumeWith = 触发回调;label + switch = 原来写在回调里的代码。
8. label37 逐行解读:首次调用 vs 恢复调用
label37: {
if ($completion instanceof testCoroutine专用ContinuationImpl) {
$continuation = $completion;
if ((label & MIN_VALUE) != 0) {
label -= MIN_VALUE;
break label37; // 复用,跳过 new
}
}
$continuation = new ContinuationImpl($completion) { ... };
}
常见误区:第一次 $completion 是 null。
错。 第一次是 launch 传的父 Continuation,有值,只是 类型不是 testCoroutine$1,所以 new。
| 场景 | instanceof | 行为 |
|---|---|---|
| 首次 | false | new ContinuationImpl |
| 恢复 | true | 复用,break label37 |
label37 是 Java 代码块标签,和 int label 不是一回事。
9. 符号位技巧:编译器的 Sign Bit Trick
恢复时:
this.label |= Integer.MIN_VALUE; // 0x80000000
入口:
if ((label & MIN_VALUE) != 0) {
label -= MIN_VALUE;
break label37; // 是恢复调用,复用状态机
}
用来区分 fresh call 和 resumption,避免恢复时又 new 一个状态机。
10. 第一次执行:从 case 0 到全线挂起
case 0:
println("start");
$continuation.L$0 = this;
$continuation.label = 1; // ⚠️ 挂起前先设好
var10000 = getUserInfo($continuation);
if (var10000 == SUSPENDED) {
return SUSPENDED; // println(user) 到不了
}
三个关键点:
- label = 1 在 getUserInfo 之前 — 恢复后直接 case 1
- 传 testCoroutine 的 continuation 给 getUserInfo — 子完成后 resume 父层
- SUSPENDED 就 return — 不阻塞线程
此时线程回到 Looper,可以处理 UI。没有线程被 block 1 秒。
11. getUserInfo 深度拆解:withContext、var10001、delay
suspend fun getUserInfo(): String {
withContext(Dispatchers.IO) { delay(1000) }
return "BoyCoder"
}
| 反编译符号 | 含义 |
|---|---|
var10000 | Dispatchers.IO |
var10001 | withContext { delay(1000) } 的 lambda |
var4 | COROUTINE_SUSPENDED |
case 0:
label = 1;
if (withContext(IO, var10001, cont) == SUSPENDED) {
return SUSPENDED;
}
var10001 内部也是 mini 状态机:
label=0 → delay(1000) → 挂起 → return SUSPENDED
label=1 → delay 完成 → return Unit
第一次 case 0:return SUSPENDED,走不到 return "BoyCoder"。
恢复后 case 1:break → return "BoyCoder"。
var10001 的 invokeSuspend:
| 次序 | 触发者 |
|---|---|
| 第 1 次 | withContext 执行 block |
| 第 2 次 | delay 完成后 resumeWith |
12. 挂起如何一层层传播
不是只有最上层处理 SUSPENDED,每一层都检查:
delay → SUSPENDED
→ lambda → SUSPENDED
→ withContext → SUSPENDED
→ getUserInfo → return SUSPENDED
→ testCoroutine → return SUSPENDED
→ 线程调用栈清空
下图建议导出 PNG 插入(掘金 Mermaid 支持不稳定):
case 0, label=1withContextdelay(1000)层层返回 SUSPENDEDprintln(user) 未执行testCoroutinegetUserInfowithContextdelay
13. 恢复全链路:delay 到点后发生了什么
Step 1:delay 到点(协程库,ViewModel 里看不到)
continuation.resume(Unit) // 内部 → resumeWith
Step 2:恢复 lambda(var10001)
resumeWith(Unit) → var10001.invokeSuspend → case 1 → Unit
Step 3:恢复 getUserInfo
withContext 完成 → getUserInfo.invokeSuspend → case 1 → return "BoyCoder"
Step 4:resume testCoroutine
resumeWith("BoyCoder") → testCoroutine.invokeSuspend
Step 5:再次进入 testCoroutine
invokeSuspend("BoyCoder") {
this.result = "BoyCoder";
return testCoroutine(this);
}
// → case 1 → println("BoyCoder") ✅
不是重新 launch,是同一个 ContinuationImpl 重入。
14. invokeSuspend 与 resumeWith:谁调谁
协程库:delay 到点 → resumeWith(result)
→ BaseContinuationImpl.resumeWith
→ invokeSuspend(result) // 编译器生成的
→ testCoroutine(this) // 重新进入状态机
JVM 没有协程原语,全是普通方法调用。
15. Continuation 嵌套:快递驿站模型
launch 根站
└── testCoroutine 站
└── getUserInfo 站
└── withContext 站
└── var10001 站
└── delay 站(1 秒后发货)
挂起 = 最内层向上报 hold
恢复 = 最内层向上传递结果,testCoroutine 收到 "BoyCoder" 继续派送
16. 第二次进入 testCoroutine:case 1 走读
你写的 Kotlin:
val user = getUserInfo()
println(user)
编译后等价于:
when (label) {
0 -> { label=1; getUserInfo(cont) }
1 -> { val user = cont.result; println(user) }
}
这是理解协程最关键的一跃。
| 步骤 | 发生什么 |
|---|---|
| resumeWith("BoyCoder") | 协程库 |
| invokeSuspend | result = "BoyCoder" |
| 再次 testCoroutine | prelude 复用 |
| case 1 | println("BoyCoder") |
| label=2 | getFriendList → 又一次挂起循环 |
17. 协程挂起 vs 线程阻塞
| Thread.sleep | 协程 delay | |
|---|---|---|
| 阻塞对象 | 线程 | 协程(状态机) |
| 线程 | 被 OS 挂起 | return 后去干别的 |
| 内存 | ~1MB 栈 | 几十~几百字节对象 |
18. 业务字节码 vs 协程库:看得到什么
反编译能看到:label 状态机、SUSPENDED 判断、invokeSuspend、L$0、label37 prelude
反编译看不到:resumeWith 实现、delay 定时、launch 创建根 Continuation
找不到 resumeWith 是 设计如此,不是反编译失败。
19. 常见误区 TOP 10
- 第一次
$completion是 null → 是 launch 传的父 Continuation - instanceof 任意 Continuation → 本函数专用 ContinuationImpl
- 恢复时 label++ → 挂起前设好,恢复后 switch 跳转
- JVM 调 invokeSuspend → 协程库 resumeWith
- 挂起 = 阻塞线程 → 挂起协程,线程释放
- resumeWith 在 ViewModel → 在 kotlinx-coroutines
- 恢复 = 重新 launch → 同一 ContinuationImpl 重入
- suspend 创建线程 → suspend 是编译器指令
- 只有最上层处理 SUSPENDED → 每一层都检查
- label37 = 状态机 label → Java 代码块标签
20. 调试技巧
断点:
println("start")→ 只 hit 1 次println(user)→ 约 1 秒后第二次进入才 hit
Call Stack 恢复时可能出现:
BaseContinuationImpl.resumeWith
LoginViewModel$testCoroutine$1.invokeSuspend
LoginViewModel.testCoroutine
最小复现:
suspend fun main() = coroutineScope {
launch {
println("A")
delay(1000)
println("B")
}
}
只有一个挂起点,最适合反编译对照。
21. 结语
label37→ 决定是否 new 状态机switch(label)→ 记住断点invokeSuspend→ 恢复重入入口COROUTINE_SUSPENDED→ 先撤,稍后从 case N 继续
协程没有黑魔法。编译器写状态机,标准库做定时和回调,JVM跑普通方法。
下次看到 LoginViewModel$testCoroutine$1.invokeSuspend——哦,老熟人。
参考资料
觉得有帮助的话,点赞 + 收藏,下次看反编译代码就不慌了。有问题欢迎评论区交流。