从反编译代码彻底读懂 Kotlin 协程:suspend 挂起与恢复全链路推演

5 阅读2分钟

从反编译代码彻底读懂 Kotlin 协程:suspend 挂起与恢复全链路推演

本文约 15 分钟阅读。建议先收藏,遇到 suspend 原理问题再回来查。

适合谁:会用 launch,但对反编译里的 label37invokeSuspend 仍有疑惑的 Android 开发者。


目录


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 是什么?状态机的 labelJava 带标签代码块,和 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$0spill:跨挂起点保留的引用(如 this
invokeSuspend恢复时的重入入口

7. Continuation:用 OkHttp 回调来理解

OkHttp协程
CallbackContinuation
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行为
首次falsenew 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) 到不了

}

三个关键点:

  1. label = 1 在 getUserInfo 之前 — 恢复后直接 case 1
  2. 传 testCoroutine 的 continuation 给 getUserInfo — 子完成后 resume 父层
  3. SUSPENDED 就 return — 不阻塞线程

此时线程回到 Looper,可以处理 UI。没有线程被 block 1 秒。


11. getUserInfo 深度拆解:withContext、var10001、delay

suspend fun getUserInfo(): String {

withContext(Dispatchers.IO) { delay(1000) }

return "BoyCoder"

}

反编译符号含义
var10000Dispatchers.IO
var10001withContext { delay(1000) } 的 lambda
var4COROUTINE_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")协程库
invokeSuspendresult = "BoyCoder"
再次 testCoroutineprelude 复用
case 1println("BoyCoder")
label=2getFriendList → 又一次挂起循环

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

  1. 第一次 $completion 是 null → 是 launch 传的父 Continuation
  2. instanceof 任意 Continuation → 本函数专用 ContinuationImpl
  3. 恢复时 label++ → 挂起前设好,恢复后 switch 跳转
  4. JVM 调 invokeSuspend → 协程库 resumeWith
  5. 挂起 = 阻塞线程 → 挂起协程,线程释放
  6. resumeWith 在 ViewModel → 在 kotlinx-coroutines
  7. 恢复 = 重新 launch → 同一 ContinuationImpl 重入
  8. suspend 创建线程 → suspend 是编译器指令
  9. 只有最上层处理 SUSPENDED → 每一层都检查
  10. 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——哦,老熟人。


参考资料


觉得有帮助的话,点赞 + 收藏,下次看反编译代码就不慌了。有问题欢迎评论区交流。