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 分支代码。
相关推荐
黄林晴1 天前
Kotlin 2.4.0 正式稳定!Android 升级、Compose、KMP 全变化详解
android·kotlin
Kapaseker1 天前
Kotlin 相等的奥义
android·kotlin
JohnnyDeng941 天前
【Android】Flow vs LiveData:选型指南与迁移实践
android·kotlin·livedata·flow
plainGeekDev1 天前
线程安全集合 → 协程安全替代
android·java·kotlin
zhangphil1 天前
Kotlin管道Channel构造函数参数capacity值RENDEZVOUS与UNLIMITED
android·kotlin
plainGeekDev1 天前
Timer → Coroutines
android·java·kotlin
Coffeeee1 天前
Android17应用内存限制--App:我人不舒服,系统:那你走吧
android·google·kotlin
AI浩1 天前
【数据处理】基于 SAM3 的 LabelMe 标注统一校正方法
android·开发语言·kotlin
zfoo-framework1 天前
[kotlin项目中使用luban配置] 1.java + kotlin共存
kotlin
zhangphil2 天前
Android将ImageView显示的图原样取出转换为Bitmap,Kotlin
android·kotlin