前言
如今很多的新语言都引入了 协程 这个概念,当然作为新生派代表的 kotlin 自然也支持。用了这么久的 协程 ,现在是时候该再进一步了解其中的原理了。
还不太了解协程的同学,建议可以先阅读 《从最基础的角度认识协程》。
suspend关键字的解释
suspend 关键字标识了该方法是一个耗时操作,需要将其放入协程作用域中才能正常编译。因为只有在协程中,才能将其正确分发到对应的执行线程,如果和其余普通的代码放在一起,便会在当前线程执行,那么这个 suspend 关键字意义就不大了。
挂起函数也称为一个挂起点,当代码执行到这里时,会将该代码所在的协程挂起并释放对当前线程资源的占用,使得该线程可以去执行其他协程任务。当挂起函数执行完毕时,它内部会通过类似回调方式通知该协程从挂起状态恢复并执行其剩余的代码块。
明确关键类的继承关系
网上也浏览过很多关于协程的文章,但大多数讲得比较绕,缺乏协程基础的同学很难看懂。因此,在探讨 协程的运行原理前,我们先认识几个比较重要的类。
我们先从最基础的一个接口入手 -- Continuation
@SinceKotlin("1.3")
public interface Continuation<in T> {
// 协程中的上下文
public val context: CoroutineContext
// [重点关注] 协程挂起时恢复执行的方法
public fun resumeWith(result: Result<T>)
}
continuation 是协程当中非常重要的一个接口,子协程的挂起恢复到父协程的恢复,都是它化作一座桥梁,联系着彼此。CoroutineContext 表示一个协程在运行当中必要保存的一些上下文变量。我们先来看看里面都有些什么方法。
public interface CoroutineContext {
public operator fun <E : Element> get(key: Key<E>): E?
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
public operator fun plus(context: CoroutineContext): CoroutineContext =
if (context === EmptyCoroutineContext) this else
context.fold(this) { acc, element ->
// 查看当前的context中是否含有有目标context中相同的key
val removed = acc.minusKey(element.key)
// 如果已经有了,直接返回自己
if (removed === EmptyCoroutineContext) element else {
// 没有的话,先去查看当前context中是否有ContinuationInterceptor为Key的元素
// 如果查找到了,将其作为第二个参数,创建一个整合context
// 因为以ContinuationInterceptor为Key的context经常会出现
// 这样做按照CombinedContext内部的代码逻辑可以快速访问到
val interceptor = removed[ContinuationInterceptor]
if (interceptor == null) CombinedContext(removed, element) else {
val left = removed.minusKey(ContinuationInterceptor)
if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
CombinedContext(CombinedContext(left, element), interceptor)
}
}
}
public fun minusKey(key: Key<*>): CoroutineContext
public interface Key<E : Element>
public interface Element : CoroutineContext {
public val key: Key<*>
public override operator fun <E : Element> get(key: Key<E>): E? =
@Suppress("UNCHECKED_CAST")
if (this.key == key) this as E else null
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(initial, this)
public override fun minusKey(key: Key<*>): CoroutineContext =
if (this.key == key) EmptyCoroutineContext else this
}
}
CoroutineContext 重写了 plus 运算符,在源码中我们会看见 coroutineContext1 + coroutineContext2 的这种写法,其实现就对应 plus 这个方法。不同的 CoroutineContext 可以相加,组合成一个新的 CombinedContext ,这样就同时存储了两个上下文的信息。
CombinedContext也是继承自 CoroutineContext,内部是一个链表结构。因为在协程源码中,有很多类型的CoroutineContext,因此采用了这种方式存储上下文信息。
前面提到,CoroutineContext的子类非常多。现在我们举几个例子。
CoroutineExceptionHandler: 协程当中的异常拦截器,如果希望包装异常处理逻辑,那么可以继承它,定制一个异常处理器。
class CustomCoroutineExceptionHandler : CoroutineExceptionHandler {
// 实现接口的 key 属性
override val key: CoroutineContext.Key<*> = CoroutineExceptionHandler.Key
// 异常处理逻辑
override fun handleException(context: CoroutineContext, exception: Throwable) {
logError("协程异常捕获: ${exception.message}", exception)
Thread.getDefaultUncaughtExceptionHandler()?.uncaughtException(Thread.currentThread(), exception)
}
private fun logError(message: String, exception: Throwable) {
println("[$message] 异常详情: ${exception.stackTraceToString()}")
}
}
// example for using
CoroutineScope(
Dispatchers.IO + CustomCoroutineExceptionHandler()
).launch {}
CoroutineDispatcher: 协程调度器,指定当前协程运行在哪个调度器上。本质上是指定了运行在哪个线程上。常见的 Dispatcher 有:
-
Default:运行在一个专门的线程池里,其中线程池大小等于当前的CPU核心数(至少是2个线程),专门用于处理 CPU密集型 任务。当启动协程未显式指定时,默认启用该调度器。Main:在Android平台上特指主线程,本质通过Handler来将代码 post 到主线程运行,刷新UI相关的逻辑应该在此运行。IO:本质也是一个线程池,专做 I/O密集型 任务,比如网络请求,文件读写,数据库操作等,默认最大支持64个并发线程。
CorountineName:可以指定协程名称,方便调试和日志追踪。
GlobalScope.launch(Dispatchers.IO + CoroutineName("nihao")) {
Log.d(TAG, coroutineContext.toString())
}
// 结果
[CoroutineName(nihao), StandaloneCoroutine{Active}@8a4352a, Dispatchers.IO]
在协程庞大的源码体系中,Continuation 还会和其他的各种接口组合形成新的接口。但现在,我们可以先关注两大类。
一类是以 SuspendLambda 为代表,他们主要的职责是子协程的开启、挂起以及恢复。第二类则是以StandaloneCouroutine 为代表,他们主要负责父协程的开启与恢复(协程作用域的创建),也是我们 block 代码块开启执行的入口。
有了前期的准备,现在我们从 CoroutineScope.launch() 入手,看看协程内部是如何运作的。
从launch方法开启一个协程
串联协程开启的完整链路
我们以这个例子开启一个协程:
GlobalScope.launch(Dispatchers.IO) {
loadUsername()
loadPageData()
}
suspend fun loadUsername(): String {
delay(200)
println("loadUsername")
return "loadUsername"
}
suspend fun loadPageData(): String {
delay(200)
println("loadPageData")
return "loadPageData"
}
现在,我们看看 launch 的内部实现。
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
// 创建一个新的CoroutineContext,
// 如果当前的context是EmptyCoroutineContext
// 会生成一个Dispatcher.Default调度器给他
val newContext = newCoroutineContext(context)
// 不在CoroutineStart指定的话,默认是default
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
// 根据我们的例子
// 这里会创建一个StandaloneCoroutine并开启协程
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
newCoroutineContext 会结合我们传的 context 参数,生成一个新的 CoroutineContext。然后接着创建出一个 CoroutineScope -- StandaloneCoroutine 并将其开启。
CoroutineScope 代表了一个协程作用域,它也是我们前面讲的组成 第二大类Continuation 的又一接口。在这里面我们能调用协程相关的东西,比如官方已经内置好的挂起函数 -- withContext(), delay()
public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
start(block, receiver, this)
}
@InternalCoroutinesApi
public operator fun <R, T> invoke(block: suspend R.() -> T, receiver: R, completion: Continuation<T>): Unit =
when (this) {
// 继续执行这个方法
DEFAULT -> block.startCoroutineCancellable(receiver, completion)
ATOMIC -> block.startCoroutine(receiver, completion)
UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion)
LAZY -> Unit // will start lazily
}
这里会开启一个可取消的协程,我们继续跟进代码。
internal fun <R, T> (suspend (R) -> T).startCoroutineCancellable(
receiver: R, completion: Continuation<T>,
onCancellation: ((cause: Throwable) -> Unit)? = null
) =
runSafely(completion) {
createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellableWith(Result.success(Unit), onCancellation)
}
我们一点点来剖析。
runSafely 可以看做一个语法糖,就是在我们的定义的 block 代码块外面加了 try catch。
继续分析 createCoroutineUnintercepted(completion),这里的 completion 是前面我们创建的StandaloneCoroutine。
kotlin中的completion 参数指向当前协程的父协程
@SinceKotlin("1.3")
public actual fun <R, T> (suspend R.() -> T).createCoroutineUnintercepted(
receiver: R,
completion: Continuation<T>
): Continuation<Unit> {
// 标志协程开启
val probeCompletion = probeCoroutineCreated(completion)
return if (this is BaseContinuationImpl)
// 命中,真正创建协程的方法
create(receiver, probeCompletion)
else {
createCoroutineFromSuspendFunction(probeCompletion) {
(this as Function2<R, Continuation<T>, Any?>).invoke(receiver, it)
}
}
}
此时,this 指向的是我们开启协程时传入的 block 代码块,我们一会看其反编译代码,会发现它是继承自 SuspendLambda 的,因此这里会执行 create(receiver, probeCompletion)。
create(receiver, probeCompletion) 的实现我们待会再看,我们先把这条链路大体走通一遍再看其内部细节。
我们继续看 intercepted 方法。
@SinceKotlin("1.3")
public actual fun <T> Continuation<T>.intercepted(): Continuation<T> =
// 通过前面的createCoroutineUnintercepted()方法创建出的对象继承自SuspendLambda
// 根据上文的继承关系,可知其也会继承ContinuationImpl,这里会接着调用ContinuationImpl.intercepted()
// 咱们继续跟进这个方法
(this as? ContinuationImpl)?.intercepted() ?: this
public fun intercepted(): Continuation<Any?> =
// 从CoroutineContext中取ContinuationInterceptor元素
// 这里就会得到CoroutineDispatcher,即我们传进来的Dispatcher.IO这个参数
// 接着继续调用CoroutineDispatcher.interceptContinuation(this)
// 继续跟进
intercepted
?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
.also { intercepted = it }
// 这个方法将Dispatcher和continuation又包了一层,组合成一个新对象
public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
DispatchedContinuation(this, continuation)
原来 intercepted() 主要将 createCoroutineUnintercepted() 生成的对象和 Dispatcher 又包了一层,组成新的对象 DispatchedContinuation。
那咱们现在看看最后一个方法 —— resumeCancellableWith()。
@InternalCoroutinesApi
public fun <T> Continuation<T>.resumeCancellableWith(
result: Result<T>,
onCancellation: ((cause: Throwable) -> Unit)? = null
): Unit = when (this) {
// 命中此分支,执行resumeCancellableWith()
is DispatchedContinuation -> resumeCancellableWith(result, onCancellation)
else -> resumeWith(result)
}
@Suppress("NOTHING_TO_INLINE")
inline fun resumeCancellableWith(
result: Result<T>,
noinline onCancellation: ((cause: Throwable) -> Unit)?
) {
val state = result.toState(onCancellation)
// 如果当前的协程需要被分发到其他线程执行,则通过该dispatcher进行分发
if (dispatcher.isDispatchNeeded(context)) {
_state = state
resumeMode = MODE_CANCELLABLE
dispatcher.dispatch(context, this)
} else {
// 不需要分发器,直接在当前线程调用
executeUnconfined(state, MODE_CANCELLABLE) {
if (!resumeCancelled(state)) {
resumeUndispatchedWith(result)
}
}
}
}
@Suppress("NOTHING_TO_INLINE")
inline fun resumeUndispatchedWith(result: Result<T>) {
// 执行resumeWith,开启协程并执行我们创建协程时传入的block代码块
// 我们的block代码块会被编译器编译成一个继承自SuspenLambda的对象
// 因此这执行的是BaseContinuarionImpl.resumeWith
withContinuationContext(continuation, countOrElement) {
continuation.resumeWith(result)
}
}
DispatchedContinuation#DispatchedContinuation将被调用,接着会判断其是否需要在当前线程执行。
- 如果是的话,直接调用
BaseContinuarionImpl#resumeWith()方法执行创建协程时传入的block代码块。 - 如果需要将其放在其他线程执行,则通过
Dispatcher调度到指定线程运行。以最开始的例子为例,这里的Dispatcher具体应该是DefaultIoScheduler。现在我们来看看其如何分发到对应线程的。
internal object DefaultIoScheduler : ExecutorCoroutineDispatcher(), Executor {
// 创建一个64和当前设备的可用处理器核心数中的较大值作为默认并行度的线程池
// 本质上创建的是:
// CoroutineScheduler(corePoolSize, maxPoolSize, idleWorkerKeepAliveNs, schedulerName)
private val default = UnlimitedIoScheduler.limitedParallelism(
systemProp(
IO_PARALLELISM_PROPERTY_NAME,
64.coerceAtLeast(AVAILABLE_PROCESSORS)
)
)
override fun dispatch(context: CoroutineContext, block: Runnable) {
default.dispatch(context, block)
}
}
DefaultIoScheduler 本质上创建的是继承 Executor 的 CoroutineScheduler的线程池。 这里进一步跟进,最终线程池中调用的是 DispatchedTask 中的 run() 方法。
public final override fun run() {
val taskContext = this.taskContext
var fatalException: Throwable? = null
try {
val delegate = delegate as DispatchedContinuation<T>
val continuation = delegate.continuation
withContinuationContext(continuation, delegate.countOrElement) {
...
// 调用BaseContinuarionImpl.resumeWith()
continuation.resume(getSuccessfulResult(state))
...
}
} catch (e: Throwable) {
fatalException = e
} finally {
val result = runCatching { taskContext.afterTask() }
handleFatalException(fatalException, result.exceptionOrNull())
}
}
至此,我们知道不论是将 block 代码放在当前线程执行还是其他线程执行,最终都会调用 BaseContinuarionImpl#resumeWith()。
这个方法就是开启我们创建协程时传入的 block 代码块的最终方法!
internal abstract class BaseContinuationImpl(
public val completion: Continuation<Any?>?
) : Continuation<Any?>, CoroutineStackFrame, Serializable {
public final override fun resumeWith(result: Result<Any?>) {
var current = this
var param = result
while (true) {
// 标志当前协开始执行
probeCoroutineResumed(current)
with(current) {
val completion = completion!! // fail fast when trying to resume continuation without completion
val outcome: Result<Any?> =
try {
// 执行我们创开启协程时传入的block代码块
val outcome = invokeSuspend(param)
// 如果返回COROUTINE_SUSPENDED,退出该方法执行
if (outcome === COROUTINE_SUSPENDED) return
Result.success(outcome)
} catch (exception: Throwable) {
Result.failure(exception)
}
releaseIntercepted() // this state machine instance is terminating
if (completion is BaseContinuationImpl) {
// 将current指向父协程并将结果保存下来
// 通过while循环,准备执行父协程当中的block代码块
current = completion
param = outcome
} else {
// 顶级协程,直接返回结果
// 这里调用的是AbstractCouroutine.resumeWith()返回结果并结束该协程
completion.resumeWith(outcome)
return
}
}
}
}
}
最外层的 while(true) 是因为,协程之间可能会出现嵌套关系,即存在协程作用域再开启一个协程,因此需要依次遍历执行完所有协程中的代码。接着调用 invokeSuspend() ,执行我们传入的 block 代码块,如果返回的是 COROUTINE_SUSPENDED,则退出该方法执行,将该协程挂起,让出占用的线程的资源。如果 completion 不属于 BaseContinuationImpl ,则代表它是顶级协程了,这时应该返回结果结束执行了;反之,则证明该协程还有父协程,需要继续调用 invokeSuspend 执行父协程中的代码块。
回顾:上文提到
continuation分成两大类。一类是我们开启协程时传入的Lambda参数会被编译成一个继承自BaseContinuationImpl的对象;另一类是像StandaloneCouroutine这种顶级协程。
那这个 invokeSuspend 方法具体做了什么呢?为什么通过这个方法就能调用到我们传入的 lambda 参数呢?
Lambda参数是如何被协程调用的
回过头来,我们继续分析一下上面遗留的代码 create(receiver, probeCompletion)。此时按照惯例,我们应该去查看 create() 的源码。但是当你点进去时会发现, 内部没有具体实现。所以我们直接查看上述例子反编译后的 java 代码。
下面是我简化后的代码。
final class LaunchSuspendLambda extends SuspendLambda implements Function2<CoroutineScope, Continuation<? super Unit>, Object> {
int label;
public final Object invokeSuspend(Object $result) {
Object COROUTINE_SUSPENDED = IntrinsicsKt.getCOROUTINE_SUSPENDED();
Continuation continuation;
switch (this.label) {
case 0:
ResultKt.throwOnFailure($result);
continuation = (Continuation)this;
this.label = 1;
if (TestActivity.loadUsername(continuation) == COROUTINE_SUSPENDED) {
return COROUTINE_SUSPENDED;
}
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");
}
continuation = (Continuation)this;
this.label = 2;
if (TestActivity.loadPageData(continuation) == COROUTINE_SUSPENDED) {
return COROUTINE_SUSPENDED;
} else {
return Unit.INSTANCE;
}
}
public final Continuation create(Object value, Continuation $completion) {
return (Continuation)(new <anonymous constructor>($completion));
}
public final Object invoke(CoroutineScope p1, Continuation p2) {
return ((<undefinedtype>)this.create(p1, p2)).invokeSuspend(Unit.INSTANCE);
}
// $FF: synthetic method
// $FF: bridge method
public Object invoke(Object p1, Object p2) {
return this.invoke((CoroutineScope)p1, (Continuation)p2);
}
}), 2, (Object)null);
我们在这里发现了这个 invokeSuspend()!说明我们上文讲到的 BaseContinuationImpl#resumwWith() 的实现就在这里。至此我们终于串通了协程开启的整个链路。接下来我们接着分析。
在启动协程时我们传入的 lambda 参数,其实最终会被编译器生成一个继承自 SuspendLambda 的对象(其他普通的 lambda参数也会生产一个对象,因为 kotlin 最终也是在 JVM 上运行,但 JVM 并没有这种基本类型,因此 kotlin 将函数类型参数包装成一个对象,最终是调用其对象内部的 invoke 方法执行),我们可以从上边贴出的代码中发现 LaunchSuspendLambda 中的 invoke 方法内部大体是一个 switch 结构,并且通过 label 来控制分支的执行。
拓展:在编译时,编译器将我们的lambda和suspend函数进行了转换,变成了上述代码的样子。这种机制叫做CPS(cotinuation-passing-style)转换。网上也有一种对 kotlin协程 的相关解释:协程的代码工作原理本质是CPS + 状态机
我们先约定一下术语,每个 suspend 函数,我们将其称之为一个 挂起点(挂起函数) ,因为这种函数通常都是耗时操作,并不会马上返回最终结果。因此在协程中,代码执行到这里时(如果是耗时操作),协程内部的的后续代码会暂停执行,但其所在的线程可以立即得到释放并调度执行其他协程任务。这个操作我们称之为 挂起,上述也说明了协程的一个重要特性 -- 非阻塞性。
现在,我们来分析一下上面这段代码。当协程启动调用 invoke 方法时,此时 label = 0,会先执行 ResultKt.throwOnFailure($result),若协程在挂起期间被取消或发生异常,此方法会抛出 CancellationException 或其他异常,终止后续代码执行。其实每个 switch 分支上第一行都是这个代码,因为我们启动的协程时可取消的,这样的目的是在协程的每个状态下,都能第一时间检测到是否被取消或者发生异常。接着将 label的值置为1,然后执行我们第一个挂起函数 loadUsername()。
public final Object loadUsername(@NotNull Continuation $completion) {
Object $continuation;
label20: {
if ($completion instanceof <undefinedtype>) {
$continuation = (<undefinedtype>)$completion;
if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) {
((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
break label20;
}
}
// completion指向的是父协程即LaunchSuspendLamda
// 此时再创建一个自己的continuation
$continuation = new ContinuationImpl($completion) {
// $FF: synthetic field
Object result;
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
this.result = $result;
this.label |= Integer.MIN_VALUE;
return TestActivity.this.loadUsername((Continuation)this);
}
};
}
Object $result = ((<undefinedtype>)$continuation).result;
Object COROUTINE_SUSPENDED = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch (((<undefinedtype>)$continuation).label) {
case 0:
ResultKt.throwOnFailure($result);
((<undefinedtype>)$continuation).label = 1;
if (DelayKt.delay(200L, (Continuation)$continuation) == COROUTINE_SUSPENDED) {
return COROUTINE_SUSPENDED;
}
break;
case 1:
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
String res = "loadUsername";
System.out.println(res);
return res;
}
我们跟进 loadUsername(),发现其内部结构和 LaunchSuspendLamda 相似。首先也是进入 case 0 分支,这里又执行到一个 挂起函数 -- delay ,不过它是官方定义的内部挂起函数,我们待会再看其内部实现,这里先告诉大家它会立即返回 COROUTINE_SUSPENDED。这样一路 return,我们又回到了 LaunchSuspendLamda 中的 invoke() 的 分支 case 0 中,现在继续执行发现它也 return COROUTINE_SUSPENDED。至此,我们的方法执行结束,这也意味着该线程会得到释放,可以去执行其他协程或非协程代码。
下面是依次返回 COROUTINE_SUSPENDED 的链路图。
那像LaunchSuspendLamda这类代码中,其余 switch 分支的代码何时执行呢?我们把目光重新放在 Delay#delay 上,前文提到当其被调用时,会立即返回 COROUTINE_SUSPENDED并释放占用的该线程资源。
public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return // don't delay
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
// if timeMillis == Long.MAX_VALUE then just wait forever like awaitCancellation, don't schedule.
if (timeMillis < Long.MAX_VALUE) {
// 会在delay任务结束时执行tryResume
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
}
private fun dispatchResume(mode: Int) {
// 标志resume状态
if (tryResume()) return // completed before getResult invocation -- bail out
// 开始利用dispatcher进行resume状态的分发
dispatch(mode)
}
public final override fun run() {
assert { resumeMode != MODE_UNINITIALIZED } // should have been set before dispatching
val taskContext = this.taskContext
var fatalException: Throwable? = null
try {
val delegate = delegate as DispatchedContinuation<T>
val continuation = delegate.continuation
withContinuationContext(continuation, delegate.countOrElement) {
// 执行resume,内部调用的是BaseContinuation.resuemWith()
continuation.resume(getSuccessfulResult(state))
}
} catch (e: Throwable) {
// This instead of runCatching to have nicer stacktrace and debug experience
fatalException = e
} finally {
val result = runCatching { taskContext.afterTask() }
handleFatalException(fatalException, result.exceptionOrNull())
}
}
Delay 的调用链太长,这里我就不一一列源码出来了。我这里做个总结: Delay 中的任务会被包装成 DelayTask 然后寻找 Dispatcher 去分发到对应线程并执行 delay任务。当任务结束后,会通知协程恢复,即重回 resume 状态。此时又会通过 Dispatcher 将通知发送到当前的协程,代码层面则对应的是执行 DispatchedTask 中的 run(),进而执行 BaseContinuation.resumeWith()向上逐级恢复协程。
internal abstract class BaseContinuationImpl(
public val completion: Continuation<Any?>?
) : Continuation<Any?>, CoroutineStackFrame, Serializable {
public final override fun resumeWith(result: Result<Any?>) {
// This loop unrolls recursion in current.resumeWith(param) to make saner and shorter stack traces on resume
var current = this
var param = result
while (true) {
probeCoroutineResumed(current)
with(current) {
val completion = completion!! // fail fast when trying to resume continuation without completion
val outcome: Result<Any?> =
try {
val outcome = invokeSuspend(param)
if (outcome === COROUTINE_SUSPENDED) return
Result.success(outcome)
} catch (exception: Throwable) {
Result.failure(exception)
}
releaseIntercepted() // this state machine instance is terminating
if (completion is BaseContinuationImpl) {
// unrolling recursion via loop
current = completion
param = outcome
} else {
// top-level completion reached -- invoke and return
completion.resumeWith(outcome)
return
}
}
}
}
}
看!我们恢复协程时再次调用了这个方法,意味着 invokeSuspend() 将再次被调用。所以,LaunchSuspendLambda#invoke() 会再次被调用,进而loadUsername 也将再次被调用执行,但这次的 loadUsername#label 将变成 1。
public final Object loadUsername(@NotNull Continuation $completion) {
Object $continuation;
label20: {
if ($completion instanceof <undefinedtype>) {
$continuation = (<undefinedtype>)$completion;
if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) {
((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
break label20;
}
}
// completion指向的是父协程即LaunchSuspendLamda
// 此时再创建一个自己的continuation
$continuation = new ContinuationImpl($completion) {
// $FF: synthetic field
Object result;
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
this.result = $result;
this.label |= Integer.MIN_VALUE;
return TestActivity.this.loadUsername((Continuation)this);
}
};
}
Object $result = ((<undefinedtype>)$continuation).result;
Object COROUTINE_SUSPENDED = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch (((<undefinedtype>)$continuation).label) {
case 0:
ResultKt.throwOnFailure($result);
((<undefinedtype>)$continuation).label = 1;
if (DelayKt.delay(200L, (Continuation)$continuation) == COROUTINE_SUSPENDED) {
return COROUTINE_SUSPENDED;
}
break;
case 1:
// 执行label=1的分支
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
// 最终会执行到这里并返回结果
String res = "loadUsername";
System.out.println(res);
return res;
}
此时会将结果返回给 LaunchSuspendLambda,LaunchSuspendLambda#label 的值也将是 1 并且接着执行其 1 分支的代码,即会进行 loadPageData() 中的逻辑。
这里的 loadPageData() 的逻辑和上文阐述的 loadUsername() 逻辑一模一样,这里就不再赘述了。
总结
通过 launch{}启动协程时传入的 block 代码块,最终会被编译成继承自 SuspendLambda 的对象,当 block 代码块执行时,其实是调用了该对象的 invokeSuspend() 方法。协程开启时,会依次调用子协程中的代码,如果协程中的代码是耗时操作,那么其 invokeSuspend() 会立即返回 COROUTINE_SUSPENDED 并将其所在的协程挂起,释放占用的线程资源,此时线程可做其他工作。当耗时操作结束时,最底层的挂起函数会调用 BaseContinuation.resumeWith() 向上逐级恢复协程,恢复的过程其实是再次执行了该协程的代码块,并且通过label 来控制每次具体执行的 swith分支。这也是 以同步代码的编写方式,实现了异步调用 的原因。