Continuation 到底是谁创建的?

Continuation 到底是谁创建的?

最近在研究 Kotlin 协程反编译代码的时候,有一段代码让我困惑了很久。

几乎每一个 suspend 函数都会出现类似下面这样的代码(省略了一些无关内容):

ini 复制代码
Continuation $continuation;

label20: {
    if ($completion instanceof GetUserContinuation) {
        $continuation = (GetUserContinuation) $completion;

        if (($continuation.label & Integer.MIN_VALUE) != 0) {
            $continuation.label -= Integer.MIN_VALUE;
            break label20;
        }
    }

    $continuation = new GetUserContinuation($completion);
}

第一次看到的时候,我一直有两个疑问。

第一个:

为什么这里要判断 $completion 的类型?

第二个,也是让我困惑最久的:

$continuation 到底是谁创建的?

如果你和我一样,只盯着这段代码看,很容易越看越迷糊。

真正理解它,需要先弄清楚一个问题:

一个 suspend 函数,到底会执行几次?


suspend 函数其实会"进入"两次

还是上一篇文章里的例子:

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

    delay(1000)

    return User(realId, "User$realId", "user$id@example.com")
}

如果 delay() 没有发生挂起,那这个函数只会执行一次。

但是现实情况是:

scss 复制代码
第一次进入

↓

执行到 delay()

↓

挂起

↓

一秒以后恢复

↓

再次进入 getUser()

注意这里的"再次进入"。

不是继续执行。

而是重新调用了一次整个函数

只是由于 label 的存在,它会直接跳到对应的 case

上一篇文章我们已经知道了这一点。

现在新的问题来了。

第一次进入和恢复进入,使用的是同一个 Continuation 吗?

答案是:

是。

而上面的那段代码,就是为了区分这两种情况。


第一次进入函数

假设有这样一段代码:

ini 复制代码
runBlocking {
    val user = getUser(1)
}

编译以后,调用大致变成:

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

这里传进来的 continuation 并不是 getUser 自己的。

它属于调用者,也就是 runBlocking

于是第一次进入函数的时候:

php 复制代码
if ($completion instanceof GetUserContinuation)

结果是:

arduino 复制代码
false

因为:

复制代码
runBlocking 的 Continuation

≠

getUser 的 Continuation

于是编译器会创建一个新的对象:

ini 复制代码
$continuation = new GetUserContinuation($completion);

这就是 getUser 自己的状态机。

所以第一次调用的时候:

scss 复制代码
runBlocking

↓

runBlockingContinuation

↓

new GetUserContinuation(...)

到这里,第一个疑问就解决了。

每一个 suspend 函数第一次执行时,都会创建属于自己的 Continuation。


恢复的时候为什么没有重新创建?

继续执行:

scss 复制代码
delay(1000)

发生挂起。

delay 会保存刚刚创建好的 GetUserContinuation

一秒以后:

scss 复制代码
Timer 到期

↓

resumeWith()

↓

invokeSuspend()

这时候会执行生成的代码:

kotlin 复制代码
public Object invokeSuspend(Object result) {
    this.result = result;
    this.label |= Integer.MIN_VALUE;

    return getUser(0, this);
}

注意最后一句:

kotlin 复制代码
getUser(0, this)

这里传进去的,不再是 runBlocking 的 Continuation。

而是:

kotlin 复制代码
this

↓

GetUserContinuation

于是再次进入函数:

php 复制代码
if ($completion instanceof GetUserContinuation)

结果变成:

arduino 复制代码
true

于是:

ini 复制代码
$continuation = (GetUserContinuation)$completion;

直接复用之前创建好的对象。

不会再执行:

scss 复制代码
new GetUserContinuation(...)

因为恢复的时候,之前保存的局部变量、label 等状态都还在这个对象里面。

如果重新创建一个对象,这些状态就全部丢失了。


为什么每个 suspend 函数都要创建自己的 Continuation?

看到这里,我又产生了一个新的疑问。

既然 runBlocking 已经有一个 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
    )
}

这里实际上有四个 suspend 函数:

复制代码
runBlocking

↓

getUserInfo

↓

getUser

↓

getAvatar

↓

getToken

每一个函数都有自己的局部变量。

例如:

getUser 需要保存:

bash 复制代码
id

realId

getUserInfo 需要保存:

sql 复制代码
user

avatar

token

它们需要保存的状态完全不同。

因此,每个 suspend 函数都会生成属于自己的 ContinuationImpl,专门负责保存自己的执行现场。

至于这些 Continuation 是如何连接起来、最终把结果一层层返回给调用者的,我们下一篇文章再继续分析。


小结

当时困扰我很久的问题,其实答案并不复杂。

反编译代码里的这段逻辑:

php 复制代码
if ($completion instanceof GetUserContinuation) {
    ...
} else {
    $continuation = new GetUserContinuation($completion);
}

实际上就是在区分两种情况:

第一次调用:

bash 复制代码
调用者传入自己的 Continuation

↓

创建当前 suspend 函数自己的 Continuation

恢复执行:

scss 复制代码
invokeSuspend()

↓

把之前创建好的 Continuation 再次传回来

↓

继续执行状态机

所以,一个 suspend 函数的 Continuation 只会创建一次

以后无论挂起、恢复多少次,都会一直使用这个对象。

下一篇,我们继续回答另一个让我困惑了很久的问题:

为什么一个 suspend 函数创建了自己的 Continuation,却还能把结果返回给调用它的 suspend 函数?

这些 Continuation 之间到底是什么关系?

相关推荐
DeepAgent1 小时前
AI Agent 工程实践(17):Agent 为什么需要可观测性(Observability)?
android·llm·agent
提笔了无痕2 小时前
MySQL SQL 从 EXPLAIN 到索引优化,搞懂 SQL 为什么慢
android·sql·mysql
zhangphil2 小时前
Android OAID是什么?有什么功用?
android
Android-Flutter4 小时前
android fragment 使用
android·kotlin
迷茫中的自我5 小时前
KMP全栈开发:从Android到AI Agent的技术演进与实践
android·人工智能
随遇丿而安5 小时前
第13周:页面状态保存 + 数据恢复优化
android
万事可爱^5 小时前
Claude 新发布的 Opus 5,系统提示语删了 80%,半价还能逼近 Fable 5
android·服务器·数据库·人工智能·claude
alexhilton6 小时前
响应式的Android身份验证架构
android·kotlin·android jetpack
喵都学不动了7 小时前
Android 自动化测试完全指南(新人版)
android