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 分支代码。
相关推荐
plainGeekDev1 天前
Activity 间传值 → Navigation 参数
android·java·kotlin
plainGeekDev1 天前
onActivityResult → ActivityResult API
android·java·kotlin
alexhilton2 天前
Android车载OS中的Remote Compose
android·kotlin·android jetpack
plainGeekDev3 天前
广播接收器 → Flow + Lifecycle
android·java·kotlin
plainGeekDev3 天前
EventBus → SharedFlow
android·java·kotlin
Kapaseker4 天前
学不动了,入门 Compose Styles API
android·kotlin
plainGeekDev5 天前
MVC 写法 → MVVM
android·java·kotlin
plainGeekDev5 天前
单例模式 → object 声明
android·java·kotlin
rocpp6 天前
Android 多语言切换实战:从 Context 到 Android 13 应用语言适配
android·kotlin
黄林晴7 天前
用了这么久 Koin Scope,原来一直都用错了?
android·kotlin