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 之间到底是什么关系?

相关推荐
好的佩奇4 小时前
蓝牙之广播与扫描
android·flutter·蓝牙
传奇开心果编程6 小时前
【Jetpack Compose娓娓道来】第36课:焦点管理与键盘输入——让遥控器、键盘和输入法都“找得到北”
android·kotlin·移动开发·交互·android jetpack·ui框架·声明式ui
hai_android7 小时前
AudioTrack 深度解析:从原理到实战
android·java·kotlin
红尘伴蝶舞7 小时前
从秒开优化到连环崩溃:深入剖析 ViewPager2 在常驻 DialogFragment 中的复用陷阱与自愈方案
android·kotlin
巫山老妖11 小时前
Android 应用架构演进:MVC / MVP / MVVM / MVI (2026)
android·android jetpack
YB137513 小时前
kotlin中闭包特性
kotlin
木头左16 小时前
【架构设计】教育类 App 服务端化改造:AI 上移、题库分层与个人题库归属
android·架构设计·系统设计·后端开发·数据库设计
墨狂之逸才16 小时前
USB 的 HOST 和 DEVICE 到底有什么区别?ADB 调试应该选哪个?
android
奶油话梅糖17 小时前
苹果的内核跑上了安卓手机:2,631 Star 的 HakiOS 让 XNU 在 Adreno GPU 上画出 118 帧
android·macos·智能手机
恋猫de小郭17 小时前
Flutter THLC 的 WebView 滚动终于从 70 FPS 提升到 120 FPS
android·前端·flutter