-
一、Continuation Passing Style (CPS)
- 1、原理
- 2、续体在kotlin中的声明
-
二、State Machine
- 1、续体+状况机
- 2、这两个东西合作能够完成什么?
-
三、launch履行进程
- 1、全体流程
- 2、createCoroutineUnintercepted办法
-
3、协程创立履行进程剖析
- (1)模仿launch进程建议协程
- (2)反编译检查创立进程
- (3)拓宽:生成SuspendLambda的办法
- 四、delay履行进程
- 五、协程品种及联系
- 六、协程上下文
(内容略长,前三点是要点,四五六点主张结合源码观看)
一、ContinuationPassingStyle(CPS)
协程在多种语言中都有运用,那它们通用的辅导概念是什么?那便是CPS
1、原理
(1)举个比如,平时运用的直接编码风格Direct Style
// 三个进程,发送一个token
fun postItem(item: Item) {
val token = generateToken() // step1
val post = createPost(token, item) // step2
processPost(post) // step3
}
CPS
的 C 是Continuation
,是续体
的意思,上面代码中 step2、step3 是 step1 的续体
,step3 是 step2 的续体
(2)将上述代码进行CPS
转化后,变成传接续体风格Continuation Passing Style
fun postItem(item: Item) {
generateToken { token -> // setp1
createPost(token, item) { post -> // step2
processPost(post) // step3
}
}
}
这个变化后能够发现,CPS
转化其实是将下一步变成了回调。定论:CPS == Callbacks
(3)Kotlin 提供了一种Direct Style
的写法,而且一起能完成CPS
的效果。为什么能这么完成?由于 kotlin在编译的时分替咱们完成了回调。看个比如,咱们将上面的比如写成kotlin的代码:
suspend fun postItem(item: Item) {
val token = generateToken() // 在上面的比如中需求传送续体(callback),所以这个办法也是 suspend 办法
val post = createPost(token, item) // 也是 suspend 办法
processPost(post)
}
将上述代码编译成Java后,会发现带suspend
的办法 在编译后办法签名都会增加一个续体
参数,下面比照一下
// 编译前:Kotlin
suspend fun createPost(token: Token, item: Item): Post { … }
// 编译后:Java/JVM(cont其实是一个callback,Post为成果)
Object createPost(Token token, Item item, Continuation<Post> cont) { … }
回到上面的举例代码,其间前两步每一步都会产生成果,而每一步的成果都需求传递给下一步,所以能够思考一下,续体
的效果是什么?
总结:续体
担任将当时进程成果
传递给下个进程,一起移送下一步的调用权
调用权
是怎么产生的?是由于咱们将下一步的代码打包成了目标,持续传递下去,所以持续履行下一步的调用权
会移送给后边履行的代码(这才是异步履行的中心思想)。
2、续体在kotlin中的声明
下面是Kotlin中续体接口,看下这个接口的注释,suspension point
便是标示suspend
的办法
这个接口内有一个目标和一个办法:
- CoroutineContext:是一个链表结构,能够运用「+」操作符,其间包含协程履行时需求的一些参数:称号/ID、调度器 Dispatcher、操控器 Job、反常 Handler等(把 Job 称为「操控器」感觉好了解一些)
-
resumeWith:触发下一步的办法,参数 result 是当时进程的成果(上面说到了
调用权
,resumeWith
便是调用进口)
「Continuation is a generic callback interface」: Continuation
是一个续体
通用的回调接口
二、StateMachine
状况机
是一个能够操控状况的目标
续体
会缓存成果
递交给下一步,状况机
会缓存进程编号
,并在每一步触发的时分将状况
改为下一步,以完成进程切换
1、续体+状况机
kotlin中的suspend
办法终究会被编译成一个状况机
,下面举个比如,来看看suspend
办法是如何转化的
(1)咱们先给每个进程编号:
suspend fun postItem(item: Item) {
switch (label) {
case 0:
generateToken()
case 1:
createPost(token, item)
case 2:
processPost(post)
}
}
(2)然后进行CPS
转化,加上续体
,完成成果
传递、调用权
转移;一起看到转化后有个「resume」办法,每一次触发下一步都会调用该办法,一起在「postItem」内完成状况机
,保证每次调用会触发下一进程
// 进口:postItem
fun postItem(item: Item, cont: Continuation) {
val sm = object : CoroutineImpl(cont) { // cont是父协程的续体,在当时协程完毕时会触发cont的resume
fun resume(...) {
postItem(null, this) // 续体回调进口
}
}
switch (sm.label) {
case 0:
sm.item = item // 初始参数,履行进程中传递的一些参数
sm.label = 1 // 状况操控
requestToken(sm) // step1,传入续体,履行完成后调用resume,并将成果传递下去
case 1:
val token = sm.result as Token // 从续体中拿取上一步的成果
val item = sm.item // 从续体中拿取初始参数
sm.label = 2 // 状况操控
createPost(token, item, sm) // step2,传入续体&参数,履行完成后调用resume,并将成果传递下去
case 2:
val post = sm.result as Post // 从续体中拿取上一步的成果
processPost(post, sm) // spte3
}
}
细心看上面的额注释,幻想一下履行进程,首先进入「case 0」然后「label」变成1,在「requestToken」办法内调用一次「sm」目标的「resume」,再次进入「postItem」,创立新的「sm」,此时假定新的「sm」会承继「cont」的数据,那么会进入「case 1」……
再复习一下:
续体
传递成果,移送下一步调用权
状况机
完成每调用一次同一个办法,就履行一个进程
2、这两个东西结合能够完成什么?
有了续体
&状况机
咱们能够做什么?
╮( ̄▽  ̄)╭「在恣意的时分建议下一步」 |
---|
幻想一下,假如我只会调用「resumeWith」…… 没联系,续体
&状况机
帮咱们完成了悉数(参数传递、进程切换),咱们只用在恣意时刻恣意线程中调用「resumeWith」来触发下一步
三、launch履行进程
1、全体流程
理解什么是续体
、状况机
后,咱们来看一下协程建议的进程,从CoroutineScope.launch
看起:
源码就不逐个截图了,全体流程如下图,从左上角开端:
上图浅赤色的是要点,后续会要点打开讲createCoroutineUnintercepted
的反编译的代码,先看一下上图的几个要害目标:
(1)SuspendLambda
SuspendLambda
是一个续体
的完成,下面是SuspendLambda
承继联系,能够看到它是现已完成了resumeWith
办法了
(2)Dispatcher
Dispatcher
是续体
的分发器
,它有多种完成,最简略的便是Dispatchers.Main
,咱们也从这个源码开端看起。咱们先来看下承继联系,能够先简略的以为,各类Dispatcher
的爷爷便是ContinuationInterceptor
,爹便是CoroutineDispatcher
上图中有两种Dispatcher
,其间HandlerContext
的dispatch
完成会回调到主线程:
(3)DispatchedContinuation
DispatchedContinuation
也是续体
,不过它是一个续体
的托付类,内部持有一个续体
目标。来看一下DispatchedContinuation
的承继联系,SchedulerTask
在run
办法中调用「resume相关办法 & 反常Handler」
由于续体
的resumeWith
都交由DispatchedContinuation
的run
办法调用,所以会称之为续体的托付操控者
(看到这个by
是不是有一种茅塞顿开的感觉,可读性大大提高)
2、反编译createCoroutineUnintercepted办法
其他办法根本都有源码可看,就这个办法隐藏的特别深,终究找到kotlin开源项目中的代码:kotlin/IntrinsicsJvm.kt at master JetBrains/kotlin (github.com)
来看下这个办法的说明:
-
Creates unintercepted coroutine with receiver type [R] and result type [T]. 运用receiver和result的类型创立一个不行打断的协程(receiver能够忽略,重视result即可)
-
This function creates a new, fresh instance of suspendable computation every time it is invoked. 这个办法会创立suspend相关逻辑(中心逻辑实际上是编译器创立的,这儿边只履行一个new)
-
To start executing the created coroutine, invoke `resume(Unit)` on the returned [Continuation] instance. 经过resume办法发动协程
-
The [completion] continuation is invoked when coroutine completes with result or exception. completion是一个续体,在这儿创立的协程完毕或许反常时会被调用
先说一个定论,图中的「this」是一个承继BaseContinuationImpl
的目标,为什么呢,接着看下面反编译剖析
3、协程创立履行进程剖析
(1)模仿launch进程建议协程
由于直接反编译规范协程代码并不能非常直接的看到调用进程,所以咱们依据createCoroutineUnintercepted
的解说建议协程
import kotlinx.coroutines.delay
import kotlin.coroutines.*
import kotlin.coroutines.intrinsics.createCoroutineUnintercepted
fun main() {
launch3 {
print("before")
delay(1_000)
print("\nmiddle")
delay(1_000)
print("\nafter")
}
Thread.sleep(3_000)
}
fun <T> launch3(block: suspend () -> T) {
// 1、传入代码块block,运用block创立协程,
// 2、一起自行创立一个续体,「resumeWith」终究会被调用
val coroutine = block.createCoroutineUnintercepted(object : Continuation<T> {
override val context: CoroutineContext
get() = EmptyCoroutineContext
override fun resumeWith(result: Result<T>) {
println("\nresumeWith=$result")
}
})
// 3、履行block协程
coroutine.resume(Unit)
}
承认履行成果:
before
middle
after
resumeWith=Success(kotlin.Unit)
(2)反编译检查创立进程
以上操作首要为了能更清晰的看到反编译的代码,咱们运用dx2jar
进行反编译,看以下代码中注释的1-3步
import kotlin.Metadata;
import kotlin.Result;
import kotlin.ResultKt;
import kotlin.Unit;
import kotlin.coroutines.Continuation;
import kotlin.coroutines.CoroutineContext;
import kotlin.coroutines.EmptyCoroutineContext;
import kotlin.coroutines.intrinsics.IntrinsicsKt;
import kotlin.coroutines.jvm.internal.DebugMetadata;
import kotlin.coroutines.jvm.internal.SuspendLambda;
import kotlin.jvm.functions.Function1;
import kotlin.jvm.internal.Intrinsics;
import kotlinx.coroutines.DelayKt;
public final class KTest6Kt {
public static final void main() {
// 1、编译后,代码块变为承继「SuspendLambda」的目标,该目标还完成了Function接口,是一个「续体+状况机」的目标
launch3(new KTest6Kt$main$1(null));
Thread.sleep(3000L);
}
public static final <T> void launch3(Function1<? super Continuation<? super T>, ? extends Object> paramFunction1) {
Intrinsics.checkNotNullParameter(paramFunction1, "block");
// 2、「createCoroutineUnintercepted」会创立一个新的「KTest6Kt$main$1」目标(为什么?),传入咱们自定义的续体
Continuation continuation = IntrinsicsKt.createCoroutineUnintercepted(paramFunction1, new KTest6Kt$launch3$coroutine$1());
Unit unit = Unit.INSTANCE;
Result.Companion companion = Result.Companion;
// 3、发动协程
continuation.resumeWith(Result.constructor-impl(unit));
}
// 咱们自定义的续体的内部类
public static final class KTest6Kt$launch3$coroutine$1 implements Continuation<T> {
public CoroutineContext getContext() {
return (CoroutineContext)EmptyCoroutineContext.INSTANCE;
}
public void resumeWith(Object param1Object) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("\nresumeWith=");
stringBuilder.append(Result.toString-impl(param1Object));
param1Object = stringBuilder.toString();
System.out.println(param1Object);
}
}
// 协程「续体+状况机」的内部类
static final class KTest6Kt$main$1 extends SuspendLambda implements Function1<Continuation<? super Unit>, Object> {
int label;
KTest6Kt$main$1(Continuation param1Continuation) {
super(1, param1Continuation);
}
public final Continuation<Unit> create(Continuation<?> param1Continuation) {
Intrinsics.checkNotNullParameter(param1Continuation, "completion");
return (Continuation<Unit>)new KTest6Kt$main$1(param1Continuation);
}
public final Object invoke(Object param1Object) {
return ((KTest6Kt$main$1)create((Continuation)param1Object)).invokeSuspend(Unit.INSTANCE);
}
// 4、resumeWith触发
public final Object invokeSuspend(Object param1Object) {
Object object = IntrinsicsKt.getCOROUTINE_SUSPENDED();
int i = this.label;
if (i != 0) {
if (i != 1) {
if (i == 2) {
ResultKt.throwOnFailure(param1Object);
} else {
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
} else {
ResultKt.throwOnFailure(param1Object);
param1Object = this;
System.out.print("\nmiddle");
((KTest6Kt$main$1)param1Object).label = 2;
}
} else {
ResultKt.throwOnFailure(param1Object);
param1Object = this;
System.out.print("before");
((KTest6Kt$main$1)param1Object).label = 1;
if (DelayKt.delay(1000L, (Continuation)param1Object) == object)
return object;
param1Object = this;
System.out.print("\nmiddle");
((KTest6Kt$main$1)param1Object).label = 2;
}
System.out.print("\nafter");
return Unit.INSTANCE;
}
}
}
ps:对于两个参数的重载办法,能够看到第一个参数并没有用:
其间第3步的resumeWith
办法完成在哪里?咱们看下上面的SuspendLambda
的承继联系图即可知道是BaseContinuationImpl # resumeWith
办法:
internal abstract class BaseContinuationImpl(
// 创立时传入,完成时调用。在上面的比如中,这是咱们自定义的续体,在「KTest6Kt$main$1 # create」办法中传入
public val completion: Continuation<Any?>?
) : Continuation<Any?>, CoroutineStackFrame, Serializable {
// This implementation is final. This fact is used to unroll resumeWith recursion.(打开递归)
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
// unrolling:铺开,这儿表示铺开递归,运用循环代替递归,减少栈深度
//「this」即block转化为的「SuspendLambda」目标,即「KTest3Kt$main$1」目标
var current = this
//「result」一般默许是「Result.success(Unit)」,上面的比如中是「Unit」
var param = result
while (true) {
// Invoke "resume" debug probe on every resumed continuation, so that a debugging library infrastructure
// can precisely track what part of suspended callstack was already resumed
probeCoroutineResumed(current)
with(current) {
// 获取「current」的「completion」
// 在上面的比如中,「completion」是内部协程目标,即「KTest3Kt$launch$coroutine$1」目标
val completion = completion!! // fail fast when trying to resume continuation without completion
//「invokeSuspend」获取履行成果
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
// 假如用于完成的续体内还有 completion,则开端套娃,直至获取到最外层的「续体」
// (「BaseContinuationImpl」是「SuspendLambda」的父类,只要「CPS」的时分会生成)
// 「suspend」办法中有「suspend」办法时会触发这儿的逻辑
current = completion
param = outcome
} else {
// top-level completion reached -- invoke and return
// 在默许状况下,最外部协程的续体是「StandaloneCoroutine」
// 上面的比如中,最外部的是咱们自己声明的匿名内部续体(object : Continuation<T>)
completion.resumeWith(outcome)
return
}
}
}
}
......
}
「resumeWith」→ 「invokeSuspend」→
(1)invokeSuspend
回来COROUTINE_SUSPENDED
,return完毕当时resumeWith
,等候下次resumeWith
(2)invokeSuspend
回来其他或许反常,生成Result
目标,假如completion
仍是BaseContinuationImpl
目标,则套娃,否则调用completion
续体的resumeWith
这儿套娃的状况出现在suspend
办法中还有suspend
办法,completion
实际上是父协程的续体
,即续体
里还有续体
(3)拓宽:生成SuspendLambda的办法
- kotlin/SuspendLambdaLowering.kt at master JetBrains/kotlin (github.com)
- SuspendLambdaLowering #generateContinuationClassForLambda
- kotlin/JvmSymbols.kt at master JetBrains/kotlin (github.com)
以上要点已讲完,下面最重要的便是那两张UML图
四、delay履行进程
对续体
有深入了解后,咱们再来看看下面比如中delay
的履行进程
GlobalScope.launch(Dispatchers.Main) {
print("before")
delay(1_000)
print("\nmiddle")
delay(1_000)
println("\nafter")
}
delay
办法:
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) {
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
}
suspendCancellableCoroutine
效果:获取一个续体后,传入scheduleResumeAfterDelay
办法
看一下suspendCancellableCoroutine
的内容,其间经过cancellable.getResult() → trySuspend()
回来COROUTINE_SUSPENDED
停止当时履行,等候下次resume
COROUTINE_SUSPENDED
这个标志代表 return
当时办法,履行权力交由下次调用续体
resumeWith
的目标
public suspend inline fun <T> suspendCancellableCoroutine(
crossinline block: (CancellableContinuation<T>) -> Unit
): T =
// Obtains the current continuation instance inside suspend functions
suspendCoroutineUninterceptedOrReturn { uCont ->
// 创立「CancellableContinuationImpl」
val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)
/*
* For non-atomic cancellation we setup parent-child relationship immediately
* in case when `block` blocks the current thread (e.g. Rx2 with trampoline scheduler), but
* properly supports cancellation.
*/
cancellable.initCancellability()
block(cancellable) // block内发送推迟音讯,发送完后当时调用栈即可完毕,即可「return」
cancellable.getResult() // 首次调用,回来「COROUTINE_SUSPENDED」,「COROUTINE_SUSPENDED」便是「return」
}
其间atomic操作开源代码:kotlinx.atomicfu/AtomicFU.kt…
回到delay
办法中,其间context.delay
从context中获取ContinuationInterceptor
(该类是各种Dispatcher
的基类),反回空就运用DefaultDelay
internal val CoroutineContext.delay: Delay get() = get(ContinuationInterceptor) as? Delay ?: DefaultDelay
本例中运用的是Dispatcher.Main
,这儿获取的便是HandlerContext
目标,能够看到HandlerContext
中的scheduleResumeAfterDelay
办法
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
val block = Runnable {
with(continuation) { resumeUndispatched(Unit) }
}
if (handler.postDelayed(block, timeMillis.coerceAtMost(MAX_DELAY))) {
continuation.invokeOnCancellation { handler.removeCallbacks(block) }
} else {
cancelOnRejection(continuation.context, block) // 发送失败就 cancel
}
}
运用handler.postDelayed
发送推迟音讯,音讯体中触发CancellableContinuationImpl
续体的resumeUndispatched
办法
override fun CoroutineDispatcher.resumeUndispatched(value: T) {
val dc = delegate as? DispatchedContinuation
resumeImpl(value, if (dc?.dispatcher === this) MODE_UNDISPATCHED else resumeMode)
}
delegate
是啥?能够看下CancellableContinuationImpl
的结构办法
在前面suspendCancellableCoroutine
办法中能够看到传入的是uCont.intercepted()
,即当时协程的续体
再转化成DispatchedContinuation
目标
假如必定要看到uCont
是什么,能够看到咱们第一次反编译的代码delegate
便是KTest6Kt$main$1
目标,便是传入「delay」办法的续体,便是「SuspendLambda」
下一行中的dispatcher
便是当时协程的调度器Dispatcher.Main
,意思是假如resume和launch的dispatcher是同一个,则传入undispatched的状况,这儿便是同一个
回来再看CancellableContinuationImpl # resumImpl
,其间的操作是:假如当时是NoCompleted
状况则调用dispatchResume
办法
调用进程如下:
CancellableContinuationImpl # resumImpl
↓ // NotCompleted
CancellableContinuationImpl # dispatchResume
↓ // tryResume 回来false
DispatchedTask<T>.dispatch
↓ // mode == MODE_UNDISPATCHED
DispatchedTask<T>.resume
↓
DispatchedContinuation # resumeUndispatchedWith
↓
Continuation # resumeWith
假如本例中运用的是Dispatcher.IO
,则context.delay
获取的是DefaultDelay
看到EventLoopImplBase
中的完成
public override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
val timeNanos = delayToNanos(timeMillis)
if (timeNanos < MAX_DELAY_NS) {
val now = nanoTime()
DelayedResumeTask(now + timeNanos, continuation).also { task ->
continuation.disposeOnCancellation(task)
schedule(now, task)
}
}
}
这儿运用了DelayedResumeTaqsk
进行了推迟履行resumeWith
,能够看到续体仍是一路被携带的,我们能够自行剖析下调用进程。
五、协程品种及联系
-
Job
提供协程的根本操作:start、cancel、join,且声明一个子Job
序列:children
a job is a cancellable thing with a life-cycle that culminates in its completion.
-
JobSupport
完成Job
的根本办法,完成父子联系、状况操控(父Job撤销,子Job
悉数撤销;子Job
反常,父子Job
悉数撤销,除SupervisorCoroutine
)
A concrete implementation of [Job]. It is optionally a child to a parent job.
-
AbstractCoroutine
,在JobSupport
基础上增加协程上下文、resumeWith
办法、生命周期回调办法
Abstract base class for implementation of coroutines in coroutine builders.
4、各类协程完成
办法名 | 完成 | 效果 | 要害操作、原理 |
---|---|---|---|
CoroutineScope.launch | StandaloneCoroutine | 建议非堵塞协程,回来操控器Job | 使命调度(线程池+Handler) |
CoroutineScope.runBlocking | BlockingCoroutine | 建议堵塞协程 | LockSupport.parkNanos挂起当时线程 |
CoroutineScope.async | DeferredCoroutine | 建议非堵塞协程,回来Deferred | CancellableContinuationImpl.getResult办法回来 COROUTINE_SUSPENDED 挂起父协程 |
coroutineScope | ScopeCoroutine | 承继外部context,回来新的操控器Job | 新建一个新的协程 |
supervisorScope | SupervisorCoroutine | 一个子协程反常不会影响到其他子协程 | 新建一个协程,重写 childCancelled 办法 |
withContext | ScopeCoroutineDispatchedCoroutineUndispatchedCoroutine | 在当时协程基础上运用新的context发去协程 | 比照当时协程context与新传入的context,判断运用那种协程,…… |
withTimeout | TimeoutCoroutine | 超时自动cancel协程 | 各种Dispatcher都有自己的完成 |
CoroutineScope | ContextScope | 回来一个只完成了context的scope |
- Job状况操控
JobSupport
中的注释有详细解说// TODO
六、协程上下文
上图紫色部分是完成了操作符的相关类
CoroutineContext
是能够相加的,加完变成这种结构:
中心办法便是plus
、get
,完成上下文的拼接,一起运用伴生目标完成去重
// 举个比如
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
↓
SupervisorJob().plus(Dispatchers.Main)
↓
Job.plus(ContinuationInterceptor)
↓
public operator fun plus(context: CoroutineContext): CoroutineContext =
if (context === EmptyCoroutineContext) {
// fast path -- avoid lambda creation
// 假如要合并的是一个空上下文,直接回来当时的上下文
this
} else {
// this -> Job, context -> ContinuationInterceptor
context.fold(this) { acc, element ->
// acc -> Job, context -> ContinuationInterceptor
// 取出右侧的上下文的 key, acc.minusKey计算出左侧上下文除掉这个key后剩下的上下文内容
val removed = acc.minusKey(element.key)
// removed -> Job
if (removed === EmptyCoroutineContext) {
// acc 与 element 相等
element
} else {
// make sure interceptor is always last in the context (and thus is fast to get when present)
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)
}
}
}
}
}
恕我直言,这篇文章写的真好:Kotlin协程上下文CoroutineContext是如何可相加的,是时分锻炼下逻辑了 (:」∠) 我就不打开讲了。
以上,如有错漏敬请告知