一、概述
上篇文章我们介绍了 kotlin 协程的具体使用方法,并且理解了什么是协程的创建和挂起。这篇文章我们将从源码探索一下协程创建和挂起的底层逻辑。
学习记录型的博客,有任何错误请多多包涵,欢迎各位大佬在评论区发表自己的理解和建议。
二、寻找创建函数
众所周知,协程启动最常见的函数就是launch接下来我们就从 launch开始说起。先看一下launch的源码。
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext, // 协程的上下文,默认是空上下文
start: CoroutineStart = CoroutineStart.DEFAULT, // 协程启动方式,默认是默认方式
block: suspend CoroutineScope.() -> Unit // 协程执行的挂起函数体
): Job {
// 创建一个新的协程上下文,基于传入的 context
val newContext = newCoroutineContext(context)
// 如果启动方式是惰性启动(Lazy),创建一个 LazyStandaloneCoroutine,否则创建一个 StandaloneCoroutine
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) // 惰性协程:只有被启动时才开始执行
else
StandaloneCoroutine(newContext, active = true) // 普通协程:立即启动并处于活动状态
// 启动协程,传入启动方式、协程对象以及执行的 block 函数
coroutine.start(start, coroutine, block)
// 返回创建的协程对象也就是 Job,用于表示协程的生命周期
return coroutine
}
继续进入
public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
start(block, receiver, this)
}
进入到这里,可能就会卡住了。我们仔细观察肯定会发现,start不是一个变量名吗?为什么能在后面加括号?
这是因为 start 的调用涉及到运算符重载,实际上会调用 CoroutineStart.invoke() 方法。
举个例子大家就明白了:
class Example {
operator fun invoke() {
println("Called Example object as a function")
}
}
fun main() {
val example = Example()
example() // 这实际上是调用 example.invoke()
}
在 Kotlin 中,operator 关键字用于标记特定的函数。运算符重载允许开发者为内置运算符(例如 +, -, *, [], =, 等)定义自定义的行为。这让你可以像使用内置类型一样使用自定义类,并且实现直观、简洁的运算符操作。
既然找到了具体函数的位置,那我们就继续。
@InternalCoroutinesApi
public operator fun <T> invoke(block: suspend () -> T, completion: Continuation<T>): Unit {
when (this) {
DEFAULT -> block.startCoroutineCancellable(completion) // 默认启动方式,支持取消
ATOMIC -> block.startCoroutine(completion) // 原子启动方式,无法被取消
UNDISPATCHED -> block.startCoroutineUndispatched(completion) // 不使用调度器,直接执行
LAZY -> Unit // 懒启动,不做任何操作,直到显式启动
}
}
@InternalCoroutinesApi
public fun <T> (suspend () -> T).startCoroutineCancellable(completion: Continuation<T>): Unit = runSafely(completion) {
createCoroutineUnintercepted(completion)
.intercepted()
.resumeCancellableWith(Result.success(Unit))
}
到这里出现了一连串的三个方法调用,我们遵循看函数先看名字的原则,简单翻译一下:create创建Unintercepted未截获;intercepted截获;resumeCancellable恢复可取消的。我们一个一个来看。
1.createCoroutineUnintercepted
ctrl + 左键 进入,人傻了这是哪门子的函数啊。
@kotlin.SinceKotlin public fun <T> (suspend () -> T).createCoroutineUnintercepted(completion: kotlin.coroutines.Continuation<T>): kotlin.coroutines.Continuation<kotlin.Unit> {
/* compiled code */
}
compiled code:编译代码。完蛋,怎么代码还有编译的时候自己生成的?!
这一行注释表示函数的实现是由编译器自动生成的代码,通常在源码中看不到具体的实现。
经过我一番寻找,查资料结果发现了:上面这段代码所在文件的结尾是 .class 。真正的代码存放在末尾带有 Jvm 的文件中。
在 IntrinsicsJvm.kt下文件 BaseContinuationImpl 抽象类中。
@SinceKotlin("1.3")
public actual fun <R, T> (suspend R.() -> T).createCoroutineUnintercepted(
receiver: R, // 协程的接收者对象,传递给挂起函数
completion: Continuation<T> // 协程完成后的回调接口,处理协程结果或异常
): Continuation<Unit> { // 返回一个 Continuation 对象,表示协程的控制
// 使用 probeCoroutineCreated 函数处理传入的 completion,通常用于监控协程创建过程
val probeCompletion = probeCoroutineCreated(completion)
// 判断当前的挂起函数是否是 BaseContinuationImpl 类型
// BaseContinuationImpl 是协程实现类,表示协程的基本实现
return if (this is BaseContinuationImpl)
// 如果是 BaseContinuationImpl,调用 create 方法创建协程
// create 方法会使用 receiver 和 probeCompletion 来创建一个新的协程
create(receiver, probeCompletion)
else {
// 如果不是 BaseContinuationImpl,则使用 createCoroutineFromSuspendFunction 来创建协程
// 这个方法将挂起函数转化为协程,并返回一个新的 Continuation 对象
createCoroutineFromSuspendFunction(probeCompletion) {
// 将当前的挂起函数(this)转化为一个 Function2 类型,并调用其 invoke 方法
// Function2 接口的签名是 (receiver: R, continuation: Continuation<T>) -> Any?
(this as Function2<R, Continuation<T>, Any?>).invoke(receiver, it)
}
}
}
乍一看,有个不认识的actual关键字:
在 Kotlin 中,actual 关键字用于与 expect 配合使用,表示对特定平台的实现。这是 Kotlin 多平台(Kotlin Multiplatform)特性的核心概念之一。
expect 和 actual 的配合使用:
Kotlin 多平台开发允许你为不同的平台(如 Android、iOS、JVM、JavaScript 等)编写共享代码,并为每个平台提供平台特定的实现。expect 和 actual 是实现这一目标的关键机制。
expect:在共享代码中声明一个接口或属性,表示该接口或属性的声明需要在不同平台上有不同的实现。actual:在平台特定的代码中提供expect声明的实际实现。
要在一段代码中迅速抓住关键:create(receiver, probeCompletion),创建一个新的协程。所以,从这里开始,正式开始创建协程了。
但是......
public open fun create(value: Any?, completion: Continuation<*>): Continuation<Unit> {
throw UnsupportedOperationException("create(Any?;Continuation) has not been overridden")
}
has not been overridden,也就是说,我们看到的是没有重写的代码,那重写了的代码到底在哪里呢?
最有可能的地方就是在创建的时候,我们随便写一个用 launch 启动的协程。
fun createCoroutine() {
val scope = CoroutineScope(Dispatchers.Main)
scope.launch {
delay(1000)
}
}
运用 AS 自带的反编译工具,编译一下
public final class LaunchTestKt {
public static final void createCoroutine() {
// 创建一个 CoroutineScope,指定协程执行的上下文是主线程(Dispatchers.getMain())
CoroutineScope scope = CoroutineScopeKt.CoroutineScope((CoroutineContext)Dispatchers.getMain());
// 调用 BuildersKt.launch$default 创建协程并启动,传入的匿名函数表示协程的行为
BuildersKt.launch$default(scope, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
int label; // 协程的状态机标签,用来标识协程执行的步骤
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
// 获取挂起的标志符,表示协程是否挂起
Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
// 根据协程的 label,判断协程当前执行到了哪个阶段
switch (this.label) {
case 0: // 第一步:协程第一次调用
ResultKt.throwOnFailure($result); // 如果上一步失败,抛出异常
this.label = 1; // 设置为下一阶段(label = 1)
// 调用 delay(1000L),并检查是否挂起,如果挂起则返回
if (DelayKt.delay(1000L, this) == var2) {
return var2; // 协程挂起,等待 1 秒后恢复
}
break;
case 1: // 第二步:协程恢复后继续执行
ResultKt.throwOnFailure($result); // 检查是否恢复失败
break;
case 2: // 第三步:协程最终结束
ResultKt.throwOnFailure($result); // 检查是否恢复失败
return Unit.INSTANCE; // 协程完成
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); // 如果出现未定义的状态,则抛出异常
}
// 第三步,协程设置为再次延迟 2 秒后恢复
this.label = 2;
if (DelayKt.delay(2000L, this) == var2) {
return var2; // 再次挂起,等待 2 秒后恢复
} else {
return Unit.INSTANCE; // 如果没有挂起,直接返回,协程完成
}
}
@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
// 创建协程的初始化对象
Intrinsics.checkNotNullParameter(completion, "completion"); // 校验 completion 不为空
Function2 var3 = new <anonymous constructor>(completion); // 创建新的 Function2 对象
return var3; // 返回新的 Continuation 实例
}
// invoke 方法,传入两个参数用于启动协程
public final Object invoke(Object var1, Object var2) {
// 创建协程并调用 invokeSuspend 开始执行协程
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
}
}), 3, (Object)null); // 启动协程,传入参数 3 表示第 3 个协程任务
}
}
很容易就发现了create 函数,原来是在这里自动创建的!
反编译的 java 代码都看了,那不妨再看看字节码。我们只看能看懂的部分,比如下面的 new 了一个实例。
这个实例在当前字节码中还能找到
提取一下有用的信息:在 launch lambda 表达式中输入的内容会自动生成一个叫LaunchTestKt$createCoroutine$1的类,这个类继承了SuspendLambda实现了Function2 接口。
final class com/jxdx/corecodelibrary/LaunchTestKt$createCoroutine$1 extends kotlin/coroutines/jvm/internal/SuspendLambda implements kotlin/jvm/functions/Function2 {
// access flags 0x11
public final invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
public final create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
// access flags 0x11
public final invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
final static INNERCLASS com/jxdx/corecodelibrary/LaunchTestKt$createCoroutine$1 null null
// compiled from: launchTest.kt
}
那就有个疑问了 lambda 表达式中的代码都被放到哪个方法中了?左边选中,右边 AS 自动选中,原来里面的代码都被放到了invokeSuspend这个函数的里面去了,所以我们也就得出个结论:invokeSuspend中存放的是协程的具体实现逻辑。
(理一下思路)这里思路回到最先研究的问题,很容易就明白了,createCoroutineUnintercepted返回的是一个 SuspendLambda 实例。找到SuspendLambda 类看一下。
internal abstract class SuspendLambda(
public override val arity: Int,
completion: Continuation<Any?>?
) : ContinuationImpl(completion), FunctionBase<Any?>, SuspendFunction {
constructor(arity: Int) : this(arity, null)
public override fun toString(): String =
if (completion == null)
Reflection.renderLambdaToString(this) // this is lambda
else
super.toString() // this is continuation
}
一步步进入,我们能发现其继承关系为: SuspendLambda -> ContinuationImpl -> BaseContinuationImpl -> Continuation。
由以上的继承关系我们可以确认一点,协程体本质上就是一个继体。
2.Continuation继体
Continuation 是 Kotlin 协程中的一个核心接口,负责协程的状态管理,特别是挂起(suspend)和恢复(resume)协程的操作。它充当了协程的控制器,维护协程执行的状态,并处理协程的恢复逻辑。
先大致了解一下Continuation 在协程中的工作流程(要结合前面的代码分析思考):
- 挂起点的创建:当协程遇到一个挂起点(例如
delay()或者其他挂起函数)时,Kotlin 会创建一个Continuation对象。这个对象保存了当前协程的状态,并将控制权返回给调用者(通常是调度器)。 - 恢复协程:当挂起操作完成,协程需要恢复执行时,调用
resumeWith方法。resumeWith将恢复协程的执行,继续执行挂起点之后的代码。 Continuation与suspend函数:在协程中的suspend函数内部,会自动创建和使用Continuation对象来管理协程的挂起和恢复。在实际的suspend函数中,协程体通常会通过Continuation来挂起自己,并等待某个条件(如网络请求完成、定时器触发等)来恢复执行。
interface Continuation<in T> {
val context: CoroutineContext // 协程的上下文
fun resumeWith(result: Result<T>) // 恢复协程执行
}
总结一下上面所说的:调用createCoroutineUnintercepted()创建了SuspendLambda实例,也就是Continuation 继体。
3.intercepted
拦截,从上面三个一连串的调用可以看出,每个协程SuspendLambda都会被拦截,也就是每创建一次,就拦截一次。
@SinceKotlin("1.3")
public actual fun <T> Continuation<T>.intercepted(): Continuation<T> =
(this as? ContinuationImpl)?.intercepted() ?: this
public fun intercepted(): Continuation<Any?> =
intercepted
?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
.also { intercepted = it }
在 MainCoroutineDispatcher 类中
public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor
实现了 CoroutineDispatcher 方法:
public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
DispatchedContinuation(this, continuation)
interceptContinuation 这个方法的目的是将一个原始的 Continuation 对象通过某种方式拦截,并且将其转化为一个新的 Continuation 对象。具体来说,它将原始的 Continuation 封装在一个
DispatchedContinuation 对象中,并返回这个新的对象。
拦截到的是一个继体,同样的,返回的居然也是一个 Continuation 继体 。 注意,这里主体变了(重新包装了一遍),变成了拦截之后的Continuation 继体,也就是DispatchedContinuation。
很自然的就会想到一个问题,为什么要这样子对Continuation进行包装,创建DispatchedContinuation的意义到底是什么?
当协程恢复时,DispatchedContinuation 的 resumeWith() 方法会被调用:
class DispatchedContinuation(
private val dispatcher: CoroutineDispatcher,
private val continuation: Continuation<T>
) : Continuation<T> {
override fun resumeWith(result: Result<T>) {
dispatcher.dispatch(context) { // 切换到目标线程
continuation.resumeWith(result) // 执行原始 Continuation
}
}
}
通过拦截 Continuation,协程可以在挂起后恢复时切换到指定线程(如主线程、IO 线程)。
4.resumeCancellableWith
从名字看就知道,这部分是关于恢复(resume)的。
@InternalCoroutinesApi
public fun <T> Continuation<T>.resumeCancellableWith(
result: Result<T>, // 协程执行的结果,可以是成功的结果或失败的异常
onCancellation: ((cause: Throwable) -> Unit)? = null // 可选的取消回调,处理协程取消时的清理工作
): Unit = when (this) { // 检查当前的 Continuation 类型
is DispatchedContinuation -> resumeCancellableWith(result, onCancellation) // 如果是 DispatchedContinuation 类型,调用其自定义的 resumeCancellableWith 方法
else -> resumeWith(result) // 否则调用 resumeWith 方法直接恢复协程
}
先看一下resumeCancellableWith()
// We inline it to save an entry on the stack in cases where it shows (unconfined dispatcher)
// It is used only in Continuation<T>.resumeCancellableWith
@Suppress("NOTHING_TO_INLINE")
inline fun resumeCancellableWith(
result: Result<T>, // 协程的执行结果
noinline onCancellation: ((cause: Throwable) -> Unit)? // 可选的取消回调函数
) {
// 将 Result 转换为协程状态,同时传入取消回调函数
val state = result.toState(onCancellation)
// 检查是否需要调度协程(例如,当前线程不适合执行该协程)
if (dispatcher.isDispatchNeeded(context)) {
_state = state // 更新协程状态
resumeMode = MODE_CANCELLABLE // 设置协程恢复模式为可取消
dispatcher.dispatch(context, this) // 调度协程到合适的上下文或线程
} else {
// 如果不需要调度,则直接在当前上下文中恢复协程
executeUnconfined(state, MODE_CANCELLABLE) {
// 如果协程未被取消,则继续恢复
if (!resumeCancelled(state)) {
resumeUndispatchedWith(result) // 恢复协程并传递执行结果
}
}
}
}
同样的抓住重点部分,分为两种情况:
- 需要调度(
dispatcher.isDispatchNeeded(context)为true)则通过调度器派发协程,dispatcher 是协程的调度器(如 Dispatchers.IO、Dispatchers.Main)。dispatch() 方法将协程的执行逻辑(this)提交到目标线程的队列中。 - 不需要调度,直接在当前线程执行。
看到 dispatcher 立马反应过来,这里开始分发了。
public open fun dispatch(context: kotlin.coroutines.CoroutineContext, block: kotlinx.coroutines.Runnable /* = java.lang.Runnable */): kotlin.Unit { /* compiled code */ }
这里注意到个关键点 block 是 Runnable 类型的。
dispatch 有什么用?这里举个例子:
Dispatchers.Main 的 dispatch 方法通过 Handler 将任务派发到主线程:
override fun dispatch(context: CoroutineContext, block: Runnable) {
// 获取主线程的 Handler
val handler = Handler(Looper.getMainLooper())
// 将任务提交到主线程的消息队列
handler.post(block)
}
所以dispatch 就是用来调度任务的。
这里还出现了resumeWith()我们看一下,这个函数究竟是怎么恢复协程的。resumeWith()很熟悉啊,就是Continuation接口里面的一个方法,最终找到是在BaseContinuationImpl 类中实现的。
public final override fun resumeWith(result: Result<Any?>) {
// 该循环展开递归,通过非递归方式恢复协程的执行,
// 使得恢复过程的堆栈跟踪更加清晰简洁
var current = this // 当前的 continuation 对象,表示当前正在恢复的协程
var param = result // 传入的结果,通常是协程的结果或异常
while (true) {
// 通过循环展开,避免递归带来的栈深度问题
// 在每次恢复协程时,触发调试探针,用于追踪哪些协程已经被恢复,
// 这样调试工具可以更精确地跟踪协程调用栈的恢复进度
probeCoroutineResumed(current)
with(current) {
// 获取该 continuation 的 completion(即协程的后续执行)
val completion = completion!! // 快速失败:如果 completion 为 null,则抛出异常
// 定义协程恢复后的结果 outcome
val outcome: Result<Any?> =
try {
// 执行协程体中的挂起函数,传入参数 `param`。
val outcome = invokeSuspend(param)
if (outcome === COROUTINE_SUSPENDED) return // 如果协程没有完成,挂起当前协程
Result.success(outcome) // 如果协程执行完毕,返回成功结果
} catch (exception: Throwable) {
Result.failure(exception) // 如果协程执行过程中抛出异常,捕获并返回失败结果
}
releaseIntercepted() // 释放拦截器,表示当前状态机实例终结
// 如果 completion 还是一个 BaseContinuationImpl(递归展开的基础协程体),
// 继续展开栈,并将 outcome 作为参数传递到下一个 continuation
if (completion is BaseContinuationImpl) {
current = completion // 将当前 continuation 设为 completion,继续展开栈
param = outcome // 传递当前的 outcome 作为下一个 continuation 的参数
} else {
// 到达协程的最顶层(没有更多的 continuation),
// 调用 completion 的 resumeWith 方法恢复最终的协程结果
completion.resumeWith(outcome)
return // 完成协程恢复,退出方法
}
}
}
}
这段代码通过 循环展开递归 来实现协程的恢复,避免了递归调用导致的栈溢出问题。它同时还包括了 异常处理、调试探针 和 拦截器释放 等机制。通过这种方式,协程可以被平滑地恢复,并且能够在协程被恢复时,准确地处理协程链条中的每一个 Continuation 对象,最终恢复协程的执行并传递结果。
注意,这里又重新调用了 val outcome = invokeSuspend(param) if (outcome === COROUTINE_SUSPENDED) return,这个函数很熟悉的,我们在反编译过来的 java 代码中看到过(这里的逻辑想仔细思考一下就能理通了)。接下来介绍一下协程的状态机。
5.状态机
协程本质上是通过一个状态机来执行的。每次挂起操作都会将协程的执行状态(包括当前执行到的位置)保存起来。当协程恢复时,它会从保存的状态继续执行。通常使用 label(标签)来表示协程的执行状态。
之前的代码如下:
public final class LaunchTestKt {
public static final void createCoroutine() {
BuildersKt.launch$default(scope, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
int label; // 协程的状态机标签,用来标识协程执行的步骤
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
// 获取挂起的标志符,表示协程是否挂起
Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
// 根据协程的 label,判断协程当前执行到了哪个阶段
switch (this.label) {
case 0: // 第一步:协程第一次调用
break;
case 1: // 第二步:协程恢复后继续执行
break;
case 2: // 第三步:协程最终结束
return Unit.INSTANCE; // 协程完成
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); // 如果出现未定义的状态,则抛出异常
}
// 第三步,协程设置为再次延迟 2 秒后恢复
this.label = 2;
if (DelayKt.delay(2000L, this) == var2) {
return var2; // 再次挂起,等待 2 秒后恢复
} else {
return Unit.INSTANCE; // 如果没有挂起,直接返回,协程完成
}
}
}), 3, (Object)null); // 启动协程,传入参数 3 表示第 3 个协程任务
}
}
也就是说他是通过上面讲的 resume 恢复过来,调用的val outcome = invokeSuspend(param) ,这方法这个状态切换代替了传统的回调,使得效率更高。而且这样就避免了传统线程的切换开销,并允许开发者编写异步的代码,像编写同步代码一样直观。
这就是挂起的真正原理。如果前面的outcome返回了COROUTINE_SUSPENDED,就直接 return 掉了。那就意味着方法被暂停了,协程也被暂停了。
6.疑问
看到这里你们绝对和我一样还有一个很大的疑问,异步操作的具体代码跑哪去?为什么没看到?我的并行代码在哪里执行了?
以 delay() 来举例子。
suspend fun delay(timeMillis: Long) {
suspendCoroutine<Unit> { continuation ->
// 使用某个调度器(比如 Default 或 Main)安排一个延迟任务
val dispatcher = Dispatchers.Default
// 安排一个任务,等待指定的时间(timeMillis)
dispatcher.scheduleResumeAfterDelay(timeMillis) {
// 当延迟结束后,恢复协程
continuation.resume(Unit)
}
}
}
// Default 调度器的一个简单实现(比如基于线程池或事件循环)
object Dispatchers {
val Default: CoroutineDispatcher = object : CoroutineDispatcher {
override fun scheduleResumeAfterDelay(timeMillis: Long, block: () -> Unit) {
// 使用某种机制安排任务(比如 Timer、ScheduledExecutorService、事件循环等)
// 这里以简单的 Timer 为例
Timer().schedule(object : TimerTask() {
override fun run() {
block() // 延迟后执行恢复协程的代码
}
}, timeMillis)
}
}
}
前面提过一个Runnable,转递给 dispatcher 了,也就是说具体怎么执行的是由调度器决定的,这里就不深入讨论了。
三、总结
1. 协程的核心
Continuation继体:协程的执行体需要一个Continuation来跟踪执行的进度。Continuation记录了协程当前的状态(如执行进度、局部变量等),它会在协程挂起和恢复时进行切换。invokeSuspend:协程的实际执行体在invokeSuspend方法中定义,挂起和恢复的逻辑都在这里。协程开始时调用invokeSuspend,挂起时返回COROUTINE_SUSPENDED,恢复时继续执行。
2. 挂起恢复
- 挂起函数:
suspend关键字标记的函数可以挂起协程的执行。挂起函数会返回一个特殊的值(COROUTINE_SUSPENDED),这表示当前协程需要暂停,直到某些条件满足后再恢复执行。 - 恢复协程:协程恢复时,
Continuation.resumeWith方法会被调用,协程会从上次挂起的位置继续执行。恢复时,invokeSuspend会被再次调用,继续执行协程的代码。
3. 调度与执行
- 协程调度器 (
CoroutineDispatcher) :协程的执行是由调度器管理的,调度器负责确定协程在何时、在哪个线程上执行。
4. 状态机
- 协程的状态机:每个协程的执行逻辑都可以看作一个状态机。状态机的状态由
label(通常是一个整数)表示,不同的label表示协程执行的不同阶段。每次协程挂起时,label会改变,以便在恢复时继续从正确的地方执行。
四、鸣谢
Android 技术指导 :小王学长