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)
四、关键方法:resume 与 resumeWithException
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 代码。
操作步骤
-
打开 Kotlin 文件 ,找到你的
suspend函数 2.点击顶部菜单栏:Tools → Kotlin → Show Kotlin Bytecode- 或者使用快捷键(Windows/Linux:
Ctrl+Shift+A搜索 "Show Kotlin Bytecode")
- 或者使用快捷键(Windows/Linux:
-
在右侧打开的 Kotlin Bytecode 面板中,点击左上角的 Decompile 按钮
-
你会看到类似这样的 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 插件
-
进入 Settings → Plugins → Marketplace
-
搜索安装 ASM Bytecode Viewer
-
右键点击 Kotlin 文件中的 suspend 函数
-
选择 ASM Bytecode Viewer → Show Bytecode
-
可以查看详细的 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 插件 | ⭐⭐ 中等 | ⭐⭐ 中 | 深入字节码分析 |
| 编译器参数 | ⭐⭐⭐ 难 | ⭐ 低 | 编译器开发/调试 |