android kotlin 状态机 Continuation 详解

Continuation 是 Kotlin 协程的核心抽象,理解它等于理解了挂起函数"暂停-恢复"的底层机制。


一、Continuation 的本质

1.1 接口定义

kotlin

复制代码
public interface Continuation<in T> {
    public val context: CoroutineContext
    public fun resumeWith(result: Result<T>)
}

只有两个成员:

  • context:协程上下文(包含调度器、Job、异常处理器等)

  • resumeWith(Result<T>):恢复协程执行的入口,传入成功或失败的结果

1.2 一句话定义

Continuation = "协程剩余未执行代码" + "执行所需上下文"

想象你在看一部电影,按下暂停键时,Continuation 就是一张"书签"------它记录了:

  • 当前播放到哪里(状态)

  • 用什么播放器播放(上下文)

  • 恢复播放的按钮(resumeWith)


二、挂起函数的秘密:CPS 转换

Kotlin 编译器对 suspend 函数做了一件关键的事:CPS(Continuation Passing Style)转换

2.1 你写的代码

kotlin

复制代码
suspend fun fetchUser(): User {
    val token = getToken()           // 挂起点 1
    val user = api.fetchUser(token)  // 挂起点 2
    return user
}

2.2 编译器实际生成的代码(伪代码)

kotlin

复制代码
fun fetchUser(continuation: Continuation<Any?>): Any? {
    // 状态机逻辑...
    when (label) {
        0 -> {
            // 执行 getToken(),传入当前 continuation
            getToken(this)  // this 就是 continuation
            return COROUTINE_SUSPENDED  // 立即返回,不阻塞线程
        }
        1 -> {
            // 恢复后:token 已从之前的结果中取出
            api.fetchUser(token, this)
            return COROUTINE_SUSPENDED
        }
        2 -> {
            // 恢复后:返回最终结果
            return user
        }
    }
}

关键洞察 :每个 suspend 函数编译后都会多一个 Continuation 参数,函数体被转换成一个状态机

2.3 COROUTINE_SUSPENDED

kotlin

复制代码
public val COROUTINE_SUSPENDED: Any = CoroutineSingletons.COROUTINE_SUSPENDED

当一个挂起函数返回这个特殊标记时,表示:

"我已经挂起了,稍后会通过 Continuation 恢复,现在不要继续往下执行"


三、Continuation 状态机详解

编译器生成的 Continuation 是一个匿名类 ,实现了 Continuation 接口。它内部维护:

kotlin

复制代码
// 编译器生成的伪代码
class FetchUserContinuation(
    val completion: Continuation<User>  // 调用者传入的 continuation
) : Continuation<Any?> {
    
    var label: Int = 0      // 状态标签(当前执行到哪个挂起点)
    var result: Any? = null // 上一个挂起函数的返回结果
    
    // 局部变量会被提升为成员变量
    var token: Token? = null
    
    override fun resumeWith(result: Result<Any?>) {
        this.result = result.getOrThrow()
        // 重新进入状态机
        val outcome = fetchUser(this)
        if (outcome !== COROUTINE_SUSPENDED) {
            // 全部执行完毕,恢复调用者
            completion.resumeWith(Result.success(outcome as User))
        }
    }
}

状态流转图

plain

复制代码
开始
  │
  ▼
label = 0 ──► 执行 getToken() ──► 返回 SUSPENDED
  │                                    │
  │◄──────── 网络请求完成 ─────────────┘
  ▼
label = 1 ──► 执行 fetchUser() ──► 返回 SUSPENDED
  │                                    │
  │◄──────── 网络请求完成 ─────────────┘
  ▼
label = 2 ──► return user ──► 调用 completion.resumeWith(user)

四、关键方法:resumeresumeWithException

Kotlin 提供了两个便利扩展函数:

kotlin

复制代码
inline fun <T> Continuation<T>.resume(value: T): Unit =
    resumeWith(Result.success(value))

inline fun <T> Continuation<T>.resumeWithException(exception: Throwable): Unit =
    resumeWith(Result.failure(exception))

使用示例:手写一个底层挂起函数

kotlin

复制代码
suspend fun <T> suspendCancellableCoroutine(
    block: (CancellableContinuation<T>) -> Unit
): T {
    // 这是标准库函数,内部创建 SafeContinuation
}

// 实际应用:将回调式 API 转换为挂起函数
suspend fun fetchDataFromNetwork(): String = suspendCoroutine { continuation ->
    api.fetchData(object : Callback<String> {
        override fun onSuccess(data: String) {
            continuation.resume(data)  // 恢复协程,传递结果
        }
        
        override fun onError(e: Throwable) {
            continuation.resumeWithException(e)  // 恢复协程,传递异常
        }
    })
}

五、Continuation 与 CoroutineContext

kotlin

复制代码
public interface Continuation<in T> {
    public val context: CoroutineContext
    // ...
}

context 是 Continuation 的"执行环境",包含:

表格

元素 作用
Job 协程的生命周期、取消信号
CoroutineDispatcher 决定在哪个线程恢复执行
CoroutineName 调试用的协程名称
CoroutineExceptionHandler 未捕获异常的处理策略

恢复时的线程调度

kotlin

复制代码
suspend fun main() {
    withContext(Dispatchers.IO) {
        // 这里创建的 Continuation 携带 Dispatchers.IO
        val data = readFile()  // 在 IO 线程恢复
    }
    // 回到原来的 Dispatcher(通常是 Default/Main)
}

当挂起函数完成后调用 continuation.resume() 时,调度器会决定 resume 在哪个线程执行


六、实际应用:创建自定义挂起点

6.1 使用 suspendCoroutine(不可取消)

kotlin

复制代码
suspend fun delayCustom(time: Long): Unit = suspendCoroutine { continuation ->
    Handler(Looper.getMainLooper()).postDelayed({
        continuation.resume(Unit)
    }, time)
}

6.2 使用 suspendCancellableCoroutine(推荐,支持取消)

kotlin

复制代码
suspend fun fetchUserCancellable(): User = suspendCancellableCoroutine { continuation ->
    val call = api.fetchUser()
    
    // 注册取消回调
    continuation.invokeOnCancellation {
        call.cancel()  // 协程取消时取消网络请求
    }
    
    call.enqueue(object : Callback<User> {
        override fun onResponse(response: User) {
            continuation.resume(response)
        }
        override fun onFailure(e: Throwable) {
            continuation.resumeWithException(e)
        }
    })
}

6.3 使用 ContinuationInterceptor 自定义调度

kotlin

复制代码
class MyInterceptor : ContinuationInterceptor {
    override val key: CoroutineContext.Key<*> = ContinuationInterceptor
    
    override fun <T> interceptContinuation(
        continuation: Continuation<T>
    ): Continuation<T> {
        // 包装原始 Continuation,在 resume 时做自定义处理
        return MyContinuation(continuation)
    }
}

class MyContinuation<T>(val delegate: Continuation<T>) : Continuation<T> {
    override val context = delegate.context
    
    override fun resumeWith(result: Result<T>) {
        // 可以在这里做线程切换、日志、性能监控等
        Log.d("Coroutine", "Resuming with $result")
        delegate.resumeWith(result)
    }
}

七、Continuation 的继承体系

plain

复制代码
Continuation<T> (接口)
    │
    ├── SafeContinuation<T>          // 确保 resume 只被调用一次(线程安全)
    │
    ├── CancellableContinuation<T>   // 支持取消操作
    │       └── CancellableContinuationImpl
    │
    └── DispatchedContinuation<T>    // 包装调度器逻辑
            // 内部持有 delegate + dispatcher
            // resume 时通过 dispatcher.dispatch() 切换线程

SafeContinuation 的重要性

kotlin

复制代码
// 防止重复 resume(会导致 IllegalStateException)
val safe = SafeContinuation(continuation)
safe.resume(value)   // OK
safe.resume(value)   // 第二次调用会抛异常或静默忽略(取决于实现)

八、Android 开发中的典型场景

8.1 将 RxJava/Callback 转为协程

kotlin

复制代码
// 旧代码:Callback 风格
fun loadImage(url: String, callback: (Bitmap) -> Unit) { ... }

// 新代码:挂起函数
suspend fun loadImage(url: String): Bitmap = suspendCancellableCoroutine { cont ->
    loadImage(url) { bitmap ->
        cont.resume(bitmap)
    }
}

8.2 在 ViewModel 中理解 Continuation

kotlin

复制代码
class MyViewModel : ViewModel() {
    fun loadData() = viewModelScope.launch {
        // launch 创建了一个 Continuation,其 context 包含:
        // - Job (与 viewModelScope 关联,ViewModel 清除时自动取消)
        // - Dispatchers.Main.immediate (默认)
        
        val data = repository.fetch()  // 挂起,Continuation 被保存
        // 恢复时,如果 ViewModel 已清除,Job 是取消状态,
        // 会抛出 CancellationException
    }
}

8.3 跨线程恢复

kotlin

复制代码
suspend fun fetchOnIo(): String = withContext(Dispatchers.IO) {
    // 进入时:当前 Continuation 被包装为 DispatchedContinuation
    // 调度器将任务投递到 IO 线程池
    
    FileReader.read()  // 在 IO 线程执行
    
    // 返回时:resume 通过 Dispatchers.Main 切回主线程
}

九、常见误区澄清

表格

误区 真相
Continuation 会阻塞线程 ❌ 不会。挂起时立即返回 COROUTINE_SUSPENDED,线程继续执行其他任务
resume 会创建新线程 ❌ 不一定。取决于 CoroutineDispatcher,可能在原线程、线程池或主线程恢复
每个 suspend 函数都会挂起 ❌ 只有调用其他挂起函数时才可能挂起。纯计算型 suspend 函数不会挂起
Continuation 是回调的语法糖 ❌ 不是。它是编译器级的 CPS 转换,状态机机制比回调更高效

十、总结

plain

复制代码
┌─────────────────────────────────────────────────────────┐
│                    Continuation 核心要点                  │
├─────────────────────────────────────────────────────────┤
│ 1. 它是协程的"书签":记录挂起位置和恢复所需的一切信息      │
│ 2. 编译器通过 CPS 转换将 suspend 函数变为状态机           │
│ 3. resumeWith() 是恢复执行的唯一切入口                   │
│ 4. context 决定了恢复时的线程、生命周期和异常处理          │
│ 5. SafeContinuation 保证 resume 的线程安全和单次调用       │
└─────────────────────────────────────────────────────────┘

理解 Continuation 后,你就能明白为什么 Kotlin 协程是**"编译器魔法 + 状态机"** ,而不是简单的线程封装。这也是 suspend 函数能在不阻塞线程的情况下实现"顺序编写异步代码"的根本原因。

在 Android Studio 中查看编译器为 suspend 函数生成的实际代码,主要有以下几种方法:


方法一:Kotlin Bytecode 工具(最推荐)

这是 Android Studio 内置的功能,可以直接看到编译后的字节码,还能反编译成 Java 代码。

操作步骤

  1. 打开 Kotlin 文件 ,找到你的 suspend 函数 2.点击顶部菜单栏:Tools → Kotlin → Show Kotlin Bytecode

    • 或者使用快捷键(Windows/Linux: Ctrl+Shift+A 搜索 "Show Kotlin Bytecode")
  2. 在右侧打开的 Kotlin Bytecode 面板中,点击左上角的 Decompile 按钮

  3. 你会看到类似这样的 Java 伪代码:

java

复制代码
// 编译器生成的状态机类
final class fetchUser$1 extends SuspendLambda implements Function2<CoroutineScope, Continuation<? super User>, Object> {
    int label;
    Object result;
    
    // 局部变量提升为成员变量
    Token token;
    
    public final Object invokeSuspend(Object $result) {
        this.result = $result;
        this.label |= Integer.MIN_VALUE;
        return fetchUser(this);
    }
}

关键观察点

在反编译后的代码中,你可以看到:

  • SuspendLambda 基类:所有 suspend lambda 的父类

  • label 字段:状态机的当前状态

  • invokeSuspend() 方法:状态机的核心逻辑

  • 局部变量被提升为字段:因为函数可能在不同线程恢复执行


方法二:使用 javap 命令行

如果你想更精确地查看字节码指令:

bash

复制代码
# 1. 先编译项目
./gradlew :app:compileDebugKotlin

# 2. 找到编译后的 .class 文件
# 路径示例:app/build/tmp/kotlin-classes/debug/com/example/YourClass.class

# 3. 使用 javap 反汇编
javap -c -p com/example/YourClass.class

# 或者更详细,包含常量池
javap -v -p com/example/YourClass.class

输出示例

你会看到 suspend 函数实际上变成了:

java

复制代码
public final java.lang.Object fetchUser(kotlin.coroutines.Continuation<? super User>);

以及内部生成的匿名类,名称类似 YourClass$fetchUser$1


方法三:使用 ASM Bytecode Viewer 插件

  1. 进入 Settings → Plugins → Marketplace

  2. 搜索安装 ASM Bytecode Viewer

  3. 右键点击 Kotlin 文件中的 suspend 函数

  4. 选择 ASM Bytecode Viewer → Show Bytecode

  5. 可以查看详细的 JVM 字节码和 ASM 代码


方法四:使用 Kotlin 编译器参数(进阶)

如果你想让编译器输出更详细的中间表示(IR):

kotlin

复制代码
// build.gradle.kts (Module: app)
kotlin {
    compilerOptions {
        // 打印编译后的 IR(需要 Kotlin 1.9+)
        freeCompilerArgs.addAll(
            "-Xdump-declarations-to", 
            "${layout.buildDirectory.get()}/declarations.txt"
        )
    }
}

实际查看示例

假设你有如下 Kotlin 代码:

kotlin

复制代码
class UserRepository {
    suspend fun fetchUser(): String {
        delay(1000)
        return "User"
    }
}

反编译后看到的 Java 代码

java

复制代码
public final class UserRepository {
    
    // 1. 原始 suspend 函数变成了带 Continuation 参数的函数
    @Nullable
    public final Object fetchUser(@NotNull Continuation<? super String> $completion) {
        // 2. 创建状态机实例
        UserRepository$fetchUser$1 sm = 
            new UserRepository$fetchUser$1(this, $completion);
        return sm.invokeSuspend(Unit.INSTANCE);
    }
}

// 3. 编译器生成的状态机类
final class UserRepository$fetchUser$1 extends SuspendLambda 
    implements Function2<Object, Continuation<? super String>, Object> {
    
    int label;
    final UserRepository this$0;
    
    UserRepository$fetchUser$1(UserRepository repo, Continuation continuation) {
        super(2, continuation);
        this.this$0 = repo;
    }
    
    @Nullable
    public final Object invokeSuspend(@NotNull Object $result) {
        Object coroutine_suspended = IntrinsicsKt.getCOROUTINE_SUSPENDED();
        
        switch (this.label) {
            case 0:
                // 第一次进入
                ResultKt.throwOnFailure($result);
                this.label = 1;
                // 调用 delay,传入自身作为 continuation
                if (DelayKt.delay(1000L, this) == coroutine_suspended) {
                    return coroutine_suspended;  // 真正挂起
                }
                break;
                
            case 1:
                // 恢复后继续
                ResultKt.throwOnFailure($result);
                break;
                
            default:
                throw new IllegalStateException("call to 'resume' before 'invoke'");
        }
        
        return "User";  // 返回最终结果
    }
}

快速对比表

表格

方法 难度 可读性 适用场景
Kotlin Bytecode → Decompile ⭐ 简单 ⭐⭐⭐ 高 日常学习,快速查看
javap -c ⭐⭐ 中等 ⭐⭐ 中 精确分析字节码
ASM 插件 ⭐⭐ 中等 ⭐⭐ 中 深入字节码分析
编译器参数 ⭐⭐⭐ 难 ⭐ 低 编译器开发/调试
相关推荐
Zha0Zhun2 小时前
Jetpack Compose 实现 iOS 风格 3D WheelPicker
android
深念Y3 小时前
RIO-UL00(EMUI 4.1 / Android 6.0.1 / arm64)开机自启动 sshd
android·linux·华为·安卓·chroot·sshd·emui
爱和冰阔落3 小时前
【MySQL 慢查询排查实战】列表接口逐渐变慢时,怎样从请求链路定位原因
android·数据库·mysql
hunterandroid20 小时前
Android WebView JSBridge 治理实战:从线上白屏崩溃到协议化通信
android·前端
一个用户名i20 小时前
【Compose 系列】第 1 篇:认识 Compose,为什么要学它
android·android jetpack
矩子01X21 小时前
保姆级智能座舱测试入门:从 Android Automotive OS 到 SOA 服务的 7 层验证
android·自动化测试·网络协议·汽车测试
极客猴子21 小时前
iPhone实时转写软件推荐:会议录音功能真实体验
android·人工智能·飞书
达令哥21 小时前
告别 ARouter!基于 Google 官方 Navigation 3 + KSP 打造 Compose 时代的双轨制路由框架
android·前端