一个 suspend 函数,为什么会变成一个普通函数?

一个 suspend 函数,为什么会变成一个普通函数?

最近在研究 Kotlin 协程源码的时候,我做了一件以前一直没有认真做过的事情------把一个 suspend 函数反编译成 Java。

结果发现,原来一个看起来很普通的 suspend 函数,经过 Kotlin 编译器之后,已经完全变成了另外一种样子。

例如下面这个函数:

kotlin 复制代码
suspend fun getUser(id: Int): User {
    delay(1000)
    return User(
        id = id,
        name = "User$id",
        email = "user$id@example.com"
    )
}

如果反编译成 Java,可以看到函数签名已经变成了这样(省略了一些无关代码):

less 复制代码
@Nullable
public static final Object getUser(
        int id,
        @NotNull Continuation<? super User> continuation
) {
    ...
}

看到这里,我的第一个疑问就是:

明明返回值是 User,为什么变成了 Object?

Continuation 又是什么?为什么多了一个参数?

后来把协程源码一路追下去,才发现这两个变化其实就是 Kotlin 协程实现的核心。


suspend 并不是 JVM 的能力

很多人刚接触协程的时候,会觉得 suspend 是 Kotlin 发明的一种新的函数类型。

实际上并不是。

JVM 根本不知道什么叫 suspend。

对于 JVM 来说,它认识的还是普通的方法,例如:

java 复制代码
Object getUser(int id, Continuation continuation)

也就是说,所谓的 suspend,只是 Kotlin 编译器提供的一层语法糖。

真正运行的时候,所有 suspend 函数都会被编译器转换成普通函数。


为什么返回值变成了 Object?

最开始我一直觉得,既然函数返回的是 User,为什么不直接写成:

java 复制代码
User getUser(int id, Continuation continuation)

后来想明白以后,发现其实不能这样设计。

因为 suspend 函数有两种完全不同的执行结果。

第一种,没有发生挂起。

例如:

kotlin 复制代码
suspend fun getUser(id: Int): User {
    return User(...)
}

这种情况下,函数可以直接返回 User。

但是另一种情况:

kotlin 复制代码
suspend fun getUser(id: Int): User {
    delay(1000)
    return User(...)
}

执行到 delay() 的时候,函数必须立即退出,把线程让出来。

这时候它根本没有 User 可以返回。

于是 Kotlin 定义了一个特殊对象:

复制代码
COROUTINE_SUSPENDED

它表示:

"我不是执行结束了,而是挂起了。"

因此,一个 suspend 函数实际上可能返回两种东西:

sql 复制代码
User

或者

复制代码
COROUTINE_SUSPENDED

由于 Java 没有联合类型(Union Type),编译器只能把返回值统一改成:

css 复制代码
Object

这样既可以返回真正的结果,也可以返回 COROUTINE_SUSPENDED。


Continuation 是干什么的?

再来看第二个变化。

函数多了一个参数:

swift 复制代码
Continuation<? super User> continuation

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

为什么一个普通函数需要传一个 Continuation?

后来才意识到,这个对象其实就是协程恢复执行所需要的一切信息。

可以把它理解成:

suspend 函数执行到一半时的"现场"。

例如:

kotlin 复制代码
suspend fun getUser(id: Int): User {
    val realId = id + 1

    delay(1000)

    return User(realId, ...)
}

如果 delay() 挂起了一秒钟,那么一秒以后恢复的时候,需要知道很多事情:

  • 当前执行到了哪一行?
  • realId 的值是多少?
  • id 的值是多少?
  • 后面应该继续执行哪段代码?

这些信息都保存在 Continuation 中。

后面我们会看到,Kotlin 编译器实际上会为每一个 suspend 函数生成一个对应的 ContinuationImpl,专门负责保存这些状态。


为什么普通函数不需要 Continuation?

普通函数只有一种执行方式:

kotlin 复制代码
开始
↓

一直执行

↓

return

执行结束以后,调用栈自然就销毁了。

局部变量也跟着释放。

但是 suspend 函数不一样。

它可以变成:

kotlin 复制代码
开始

↓

执行一半

↓

挂起

↓

几秒以后恢复

↓

继续执行

↓

return

问题来了。

函数早就已经退出了。

调用栈也没有了。

那恢复的时候怎么知道之前执行到哪里了?

答案就是:

编译器把原本保存在调用栈里的信息,全部搬到了 Continuation 对象里面。

也正因为如此,协程才能做到"挂起以后还能继续执行"。


为什么 Java 也能调用 suspend 函数?

知道 suspend 最终只是一个普通函数以后,就会发现:

理论上 Java 当然也可以调用。

例如:

scss 复制代码
getUser(1, continuation);

只是这个 Continuation 需要自己实现。

还要处理恢复、异常、结果传递等一系列逻辑。

所以虽然可以调用,但基本没有人会这么写。

通常还是交给 Kotlin 编译器自动生成这些代码。


小结

很多文章都会说:

suspend 不会阻塞线程。

这句话没有错,但它并没有解释 suspend 是怎么做到的。

真正理解协程的第一步,其实应该是接受一个事实:

suspend 函数在 JVM 上并不是一种新的函数,它最终仍然会被编译成一个普通函数。

只不过编译器悄悄帮我们做了两件事情:

  • 把返回值改成了 Object,用于同时表示"执行完成"和"协程挂起"。
  • 增加了一个 Continuation 参数,用来保存协程恢复所需要的全部状态。

理解了这两点,再去看后面的反编译代码、状态机、ContinuationImpl,很多之前看起来神秘的地方都会变得顺理成章。

下一篇,我们继续聊一个更有意思的话题:

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

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