Kotlin 协程的挂起(suspend)原理

用了几年协程,你真的理解 suspend 背后发生了什么吗?

一、一个简单的例子

我们先写一个最普通的 suspend 函数:

kotlin 复制代码
// Test.kt
suspend fun fetchData() {
    println("step 1")
    delay(1000L)
    println("step 2")
}

二、反编译

编译之后,可以用 javap 命令反编译 class 文件查看编译结果:

bash 复制代码
javap -c -p TestKt.class
javap -c -p 'TestKt$fetchData$1.class'

编译器在编译期会生成一个匿名类(状态机)和一个静态方法(状态分发),如下:

java 复制代码
// 匿名类(状态机):
final class TestKt$fetchData$1 extends ContinuationImpl {

    Object result;     // 上一个 suspend 调用的返回值
    int label;         // 当前执行到哪个挂起点(0、1)

    public TestKt$fetchData$1(Continuation completion) {
        super(completion);
    }

    @Override
    public Object invokeSuspend(Object $result) {
        this.result = $result;
        this.label |= Integer.MIN_VALUE;
        return TestKt.fetchData(this);
    }
}

// 静态方法(状态分发):
public static final Object fetchData(Continuation<? super Unit> $completion) {

    // 第一步:创建或复用状态机对象
    TestKt$fetchData$1 sm;
    if ($completion instanceof TestKt$fetchData$1) {
        sm = (TestKt$fetchData$1) $completion;
        if ((sm.label & Integer.MIN_VALUE) != 0) {
            sm.label -= Integer.MIN_VALUE;
        }
    } else {
        sm = new TestKt$fetchData$1($completion);
    }

    // 第二步:状态分发 ------ 本质上就是 switch-case
    switch (sm.label) {
        case 0:   // 初始状态
            ResultKt.throwOnFailure(sm.result);
            System.out.println("step 1");
            sm.label = 1;
            if (delay(1000L, sm) == COROUTINE_SUSPENDED) return null;
            break;

        case 1:   // 从 delay 恢复后
            ResultKt.throwOnFailure(sm.result);
            System.out.println("step 2");
            return Unit.INSTANCE;           // 返回结果
    }
    throw new IllegalStateException("call to 'resume' before 'invoke'");
}

三、执行过程

3.1 调用入口

假设我们在协程作用域中调用了 fetchData()

经过协程的一系列调用和转换(scope.launch { fetchData() } → ... → startCoroutineCancellableresumeWith(Unit)),最终会调用到 TestKt$fetchData$1.invokeSuspend(Unit) 这个方法:

java 复制代码
// 前面生成的匿名类的方法
@Override
public Object invokeSuspend(Object $result) {
    this.result = $result;
    this.label |= Integer.MIN_VALUE;   // 标记"正在执行"
    return TestKt.fetchData(this);      // 调用状态分发器
}

fetchData(this) 中的 this 就是状态机本身

这时 label == 0,所以进入 tableswitchcase 0 分支:

java 复制代码
case 0:                     // 初始进入 fetchData,sm.label == 0
    ResultKt.throwOnFailure(sm.result);   // 检查上次是否异常
    System.out.println("step 1");
    sm.label = 1;           // ← 记录下一点:下次进来走 case 1
    if (delay(1000L, sm) == COROUTINE_SUSPENDED) {
        return null;        // ← 挂起!函数返回了
    }
    break;

线程没有被阻塞,方法调用从 fetchData 函数 return null 了就结束了。
sm 传给了 delay,相当于传过去了一个回调函数。
delay 内部开了个定时器,1 秒后回调 sm.resumeWith(Result.success(Unit)) 恢复执行。

3.2 再次调用 invokeSuspend

1 秒后定时器触发:

kotlin 复制代码
// delay 的实际行为(简化示意)
// delay 不阻塞线程,它往 Dispatcher 的定时器队列注册了一个回调
// 1 秒后 Dispatcher 触发该回调,恢复续体:
sm.resumeWith(Result.success(Unit))

// sm.resumeWith 内部执行了:
sm.invokeSuspend(Unit)

// sm.invokeSuspend(Unit) 内部执行了:
fetchData(sm)

此时 sm.label == 1,所以执行 label 1 的分支:

java 复制代码
case 1:                     // 从 delay 恢复后
    ResultKt.throwOnFailure(sm.result);   // 检查 Unit,正常
    System.out.println("step 2");
    return Unit.INSTANCE;                  // 执行完毕,返回结果

函数执行完了,Unit.INSTANCE 会一层层向上传,协程结束。

四、简单总结

  1. 协程挂起的本质还是回调,只是提供了语法糖,可以写出同步风格的代码。
  2. 挂起就是给别的函数传入了一个回调方法,然后直接 return,不阻塞线程。
  3. 回调的时候根据 label 来判断执行哪个 case 分支代码。
相关推荐
小强闯江湖4 小时前
不只是让 AI 写代码:ViewCompose 把 Android UI 做成了可编译、可渲染的闭环
android·人工智能·kotlin
bytebitx1 天前
MacOS 编译腾讯 Mars XLog 及使用(已16KB对齐)
kotlin·mac·apk
alexhilton2 天前
让架构边界变成可执行的测试
android·kotlin·android jetpack
数据治理自习室2 天前
AI 应用评测体系(GraphRAG)
android·大数据·人工智能·kotlin
名剑走天下3 天前
Android 面试题大全
android·kotlin
chjif3 天前
Android 设备管控开发实战:自定义 Launcher 如何只显示指定 App?
android·kotlin
非凡ghost3 天前
用 Kotlin 和 Jetpack Compose 重写的安卓视频播放器,原生体验有多流畅?
android·java·kotlin·音视频·软件需求
名剑走天下4 天前
android 面试题
kotlin
蜡台4 天前
Jetpack Compose 稳定性、重组优化(Stable / @NonRestartableComposable)
android·kotlin·compose·jepack
JMchen1234 天前
Jetpack 内核实战(十):实战(下)——编辑页、全局设置与内存泄漏排查
kotlin·内存泄漏·android 实战·datastore 设置·jetpack 项目优化·编辑页开发