Kotlin 编译器是如何把 suspend 函数变成状态机的?

Kotlin 编译器是如何把 suspend 函数变成状态机的?

上一篇我们知道了,一个 suspend 函数最终会被编译成一个普通函数:

java 复制代码
Object getUser(int id, Continuation<? super User> continuation)

但新的问题又来了。

如果一个函数中有多个挂起点,它恢复的时候为什么知道该从哪里继续执行?

例如下面这个函数:

kotlin 复制代码
suspend fun getUserInfo(id: Int): User {
    val user = getUser(id)
    val avatar = getAvatar(id)
    val token = getToken(id)

    return user.copy(
        avatar = avatar,
        token = token
    )
}

它一共有三个挂起点:

scss 复制代码
① getUser()

② getAvatar()

③ getToken()

假设第一次执行到 getAvatar() 时挂起了,那么一秒以后恢复时,它怎么知道:

"我已经执行完 getUser() 了,应该从 getAvatar() 后面继续,而不是重新从第一行开始执行。"

答案就是今天的主角------状态机(State Machine) 。


看看反编译后的代码

把上面的代码反编译以后(省略无关代码),会看到这样一段:

ini 复制代码
switch (continuation.label) {
    case 0:
        ...
        continuation.label = 1;
        if (getUser(id, continuation) == COROUTINE_SUSPENDED) {
            return COROUTINE_SUSPENDED;
        }

    case 1:
        ...
        continuation.label = 2;
        if (getAvatar(id, continuation) == COROUTINE_SUSPENDED) {
            return COROUTINE_SUSPENDED;
        }

    case 2:
        ...
        continuation.label = 3;
        if (getToken(id, continuation) == COROUTINE_SUSPENDED) {
            return COROUTINE_SUSPENDED;
        }

    case 3:
        ...
        return user.copy(...);
}

第一次看到这段代码的时候,我最大的疑问就是:

这不是一个 switch 吗?

后来才意识到:

整个 suspend 函数已经被编译器改造成了一台状态机。


什么叫状态机?

其实状态机没有那么神秘。

可以把上面的代码画成这样:

scss 复制代码
┌─────────┐
│ state 0 │
└────┬────┘
     │
     ▼
 getUser()
     │
     ▼
┌─────────┐
│ state 1 │
└────┬────┘
     │
     ▼
getAvatar()
     │
     ▼
┌─────────┐
│ state 2 │
└────┬────┘
     │
     ▼
 getToken()
     │
     ▼
┌─────────┐
│ state 3 │
└────┬────┘
     │
     ▼
 return

每经过一个挂起点,就进入下一个状态。

恢复的时候,只需要知道当前状态是多少,就知道该从哪里继续执行。


label 就是当前状态

反编译代码里有一个成员变量:

arduino 复制代码
int label;

它就是状态机当前所在的位置。

第一次进入函数时:

ini 复制代码
label = 0

于是:

scss 复制代码
switch(label)

进入:

arduino 复制代码
case 0

执行:

ini 复制代码
val user = getUser(id)

但是在调用之前,编译器会先偷偷做一件事:

ini 复制代码
continuation.label = 1;

也就是说:

如果这里发生挂起,那么下一次恢复时,就直接进入 case 1。

所以真正的执行顺序其实变成了:

arduino 复制代码
case 0

↓

label = 1

↓

调用 getUser()

↓

如果挂起

↓

return COROUTINE_SUSPENDED

为什么要提前修改 label?

这里是我第一次看反编译代码时最疑惑的地方。

按正常思维,应该是:

ini 复制代码
getUser()

↓

执行成功

↓

label = 1

为什么 Kotlin 偏偏反过来?

原因很简单。

如果 getUser() 真的挂起了,那么:

ini 复制代码
label = 1

这一行将永远不会执行。

因为函数已经提前返回了:

kotlin 复制代码
return COROUTINE_SUSPENDED;

因此,编译器必须在调用 suspend 函数之前,就把下一步状态保存好。

这样恢复的时候,才能知道应该继续执行哪里。


为什么恢复以后不会重新执行 getUser()?

很多人第一次看状态机都会有这个疑问。

恢复时,整个函数不是重新调用了吗?

例如:

bash 复制代码
return getUserInfo(id, continuation);

如果重新进入函数,那岂不是又会执行:

ini 复制代码
val user = getUser(id)

实际上不会。

因为这时候:

ini 复制代码
label = 1

所以:

scss 复制代码
switch(label)

直接跳到了:

arduino 复制代码
case 1

于是:

scss 复制代码
getUser()

这一段代码根本不会再次执行。

恢复后继续执行的是:

ini 复制代码
val avatar = getAvatar(id)

这就是状态机真正发挥作用的地方。


那 user 去哪里了?

还有一个问题。

既然整个函数重新执行了一遍,那之前已经得到的:

ini 复制代码
val user = ...

去哪了?

答案是:

局部变量已经不再保存在栈上,而是保存在 Continuation 对象里面。

反编译代码中经常会看到这样的成员:

javascript 复制代码
Object L$0;
Object L$1;

int I$0;
int I$1;

它们分别表示:

  • L:引用类型(Object)
  • I:int 类型

例如:

ini 复制代码
val user = getUser(id)

恢复之前,编译器会先保存:

ini 复制代码
continuation.L$0 = user;

恢复以后:

ini 复制代码
user = (User) continuation.L$0;

于是整个函数看起来就像从来没有中断过一样。


小结

理解状态机以后,会发现协程其实没有想象中那么神秘。

编译器只是把一个顺序执行的函数,拆成了多个状态。

每遇到一个挂起点,就提前记录好下一步要执行的位置:

scss 复制代码
case 0

↓

getUser()

↓

case 1

↓

getAvatar()

↓

case 2

↓

getToken()

↓

case 3

↓

return

而 label,就是这台状态机的"当前位置"。

下一篇,我们继续讨论一个我当时研究反编译代码时困扰了很久的问题:

Continuation 到底是谁创建的?为什么每个 suspend 函数里都会出现一个 $continuation?

相关推荐
事圆则缓38 分钟前
Kotlin 入门与面试:从空安全、扩展函数到协程
安全·面试·kotlin
传奇开心果编程2 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程3 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程4 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程4 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
传奇开心果编程4 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer
熊猫钓鱼>_>21 小时前
Kotlin Multiplatform for OpenHarmony 实战:为 Reaktive 实现响应式原语适配(完整版 · 含摘要目录与技术图表)
华为·kotlin·大模型·ai编程·harmonyos·适配·reaktive
李游Leo1 天前
HarmonyOS 7 InsightBoard 多形态适配实录 06:ArkUI × 多设备回归:布局抖动、监听释放与全形态验收【鸿蒙心迹】
回归·kotlin·harmonyos
点燃大海1 天前
手机给平板当键盘?
kotlin
李游Leo1 天前
HarmonyOS 7 QuickDock 闪控窗开发实录 06:floatView × 回归验收:25轮场景回归、资源基线与发布前收口【鸿蒙心迹】
回归·kotlin·harmonyos