kotlin协程硬核解读(4. 协程的创建和启动流程分析)
Posted open-Xu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了kotlin协程硬核解读(4. 协程的创建和启动流程分析)相关的知识,希望对你有一定的参考价值。
版权声明:本文为openXu原创文章【openXu的博客】,未经博主允许不得以任何形式转载
文章目录
上一篇文章我们学习了挂起函数,了解了协程return式挂起和resumeWith()恢复的原理,梳理了协程代码块的执行流程,文章末尾我们遗留了两个问题:
-
启动协程时
SuspendLambda
的匿名子类对象被创建了2次,第一次是launch()函数中为构造函数传递null创建的对象被强转为Function2类型,第二次是什么时候创建的?第一次invokeSuspend()
是怎么触发的? -
协程启动、挂起、恢复涉及的线程调度
这篇文章我们讲解第一个问题,打通协程执行的前半程(协程的创建和启动),为下一步协程调度做好铺垫。
1. 协程核心类重温&语法理解
相信有不少同学都尝试过去跟踪kotlin协程的源码,但是因为kotlin灵活的语法和各种变换导致跟着跟着就跟丢了,最终没办法搞懂整个流程。其原因还是相关kotlin语法的原理没弄明白,然后就是协程库主要类的作用和它们的关系理不清,虽然相关类在前几篇文章都多少讲过,这里我们还是简单概括一遍,可能会有新的认识,重点关注加粗字体。
1.1 CoroutineScope协程作用域
在非挂起环境下启动协程需要通过runBlocking()
函数或者CoroutineScope
的实例对象调用其launch()函数,协程库为CoroutineScope
提供了一个单例子类对象GlobalScope
和一个子类ContextScope
:
runBlocking并不是通过协程作用域对象创建协程的,它启动的协程会阻塞"主线程",通常用于测试环境中避免jvm提前退出
object GlobalScope
:这是一个协程作用域的单例对象,用于启动一个全局作用域的协程,生命周期与应用程序生命周期同步,非测试环境不推荐使用,避免内存泄漏internal class ContextScope
:上下文作用域,实际开发中都应该通过这个类的对象启动协程,这个类是internal
的,显然不能直接创建其对象,但协程库提供了CoroutineScope(context)
简单工厂函数通过给定的上下文创建作用域对象,还有一个工厂函数MainScope()
用于构建一个在UI线程调度的作用域对象
协程作用域的作用就是提供原始上下文对象,帮我们快速的创建“协程对象”并启动,提供了扩展函数cancel()
取消协程控制生命周期
1.2 AbstractCoroutine协程
通过协程构建器或者其他方式启动一个协程都将创建一个协程对象,这个对象就是AbstractCoroutine
的子类对象,观察AbstractCoroutine类的定义,发现它继承了Job(作业)、Continuation(续体)、CoroutineScope(作用域),这就意味着一个协程对象可以被需要的地方灵活的转换为这3类对象。不同的构建器将创建其不同的子类对象,比如:
//协程抽象类,同时实现了 作业Job、续体Continuation、作用域CoroutineScope接口
abstract class AbstractCoroutine<in T>(...) : JobSupport(active), Job, Continuation<T>, CoroutineScope
AbstractCoroutine
|
|--BlockingCoroutine //runBlocking启动的协程对象
|--LazyStandaloneCoroutine //launch启动的延迟执行的协程
|--StandaloneCoroutine //launch启动的立即执行的协程
|--LazyDeferredCoroutine //async启动的延迟执行的协程
|--DeferredCoroutine //async启动的立即执行的协程
|--DispatchedCoroutine //withContext启动的协程
|--FlowCoroutine //flow()相关的协程
|--...
在使用协程时,我们没办法直接得到协程的子类对象,因为这些子类都是private
修饰的,但这并不意味着协程库将协程类完全隐藏了,协程库暴露了协程的最小实现单元(Job)。比如通过launch
启动协程将返回一个Job对象,通过async
启动会返回Deferred对象,而返回的Job和Deferred对象实际就是上面的StandaloneCoroutine
和DeferredCoroutine
类型,为什么不直接将协程对象返回呢?就是为了隐藏实现细节,只给我们暴露必要的最小单元,比如async
启动的协程需要通过deferred.await()
来获取结果值,所以它就返回了Deferred
类型(Deferred是Job的子类)。
1.3 Continuation续体
上一篇文章中我们分析了Continuation
续体接口和其实现类BaseContinuationImpl
、ContinuationImpl
、SuspendLambda
的关系,构建协程时传递的挂起Lambda表达式代码块中的代码会被分割为多个部分后填充到SuspendLambda
的匿名子类的invokeSuspend()
函数中,从而实现它。SuspendLambda
的匿名子类就是一个完整的续体实现类,子类的对象就是协程的续体对象。
1.4 kotlin扩展函数
//1. CoroutineScope的launch扩展函数
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
//2. CoroutineScope的匿名挂起扩展函数
block: suspend CoroutineScope.() -> Unit
): Job
launch协程构建器是CoroutineScope的扩展函数,在调用launch传递的挂起Lambda表达式也是CoroutineScope的扩展函数(匿名扩展函数),我们先弄清楚扩展函数的本质是什么。java中是不存在扩展函数的,kotlin的扩展函数在编译为class后实际上变成了一个静态工具函数,并为这个静态函数增加一个参数(放在第一个参数位置),参数的类型就是被扩展的类型CoroutineScope,也叫接受类型,当调用这个静态方法时传递的实参也就是CoroutineScope的实例对象就是接受对象,launch扩展函数对应的java代码如下:
//1. launch()扩展函数
public static final Job launch(
CoroutineScope scope, //新增的参数,参数类型是扩展接受类型CoroutineScope,调用launch时传递的实参就是接收对象
CoroutineContext context,
CoroutineStart start,
Function2 block
)...
在kotlin中使用扩展函数时,可以简单的将扩展函数当作是这个类的成员函数,可通过this随意使用类中的其他成员,this就是接受对象。当被编译成class时,就是将函数中的this都换成接受对象实参了。
1.5 kotlin的函数类型Function
package kotlin
public interface Function<out R>
package kotlin.jvm.functions
public interface Function2<in P1, in P2, out R> : Function<R>
/**
* 调用操作符重载
* Function2 function = new Function2();
* function(p1, p2); //调用function对象就相当于调用invoke(p1, p2)
*/
public operator fun invoke(p1: P1, p2: P2): R
Function是kotlin对函数类型的封装,java中并不支持函数类型,所有的kotlin函数类型对象将被编译为FunctionX系列对象,其中X表示的是函数接受X个参数,如果函数接受2个参数,则这个函数对应的就是Function2类型,所有的Function都重写了调用操作符(),对应的函数为invoke(),当使用**括号()**调用函数对象时就会触发invoke()。
launch()构建器最后一个参数类型是block: suspend CoroutineScope.() -> Unit
,它是一个函数类型,所以会被编译为FunctionX的子类,Function2表示该函数类型在调用时接受两个参数:
- 它被当作是
CoroutineScope
的扩展,将CoroutineScope类型作为第一个参数 - 它是一个挂起函数类型,会遵循续体传递风格自动增加
Continuation
类型的参数
1.6 调用操作符()重载
操作符重载:Kotlin允许为预定义操作符提供自定义的实现,可通过固定名字的成员函数或者扩展函数重写操作符,参考文档
如果一个类中定义了invoke(...)
函数并使用operator
修饰,那么这个对象就可以使用调用操作符()直接调用,否则则不能使用()调用。需要纠正的是并非只有函数类型Function可以被调用,普通的类也可以,普通类的对象后面跟着调用操作符()就是调用其invoke():
data class User(val name:String)
//如果没有使用operator覆盖invoke()函数,调用user()会报错
operator fun invoke()
println("调用对象:$name")
fun main()
val user = User("openXu")
user() //调用user对象
//main()函数中的内容反编译为java如下
User user = new User("openXu");
user.invoke(); //调用操作符就是直接调用对象的invoke()函数
2. 协程的启动、执行流程分析
启动协程有多种方式,通过runBlocking
或者CoroutineScope作用域的扩展launch
创建最外层协程,或者通过扩展async
创建并发子协程、通过withContext()
创建子协程等等,不管是在非挂起作用域创建外层协程还是创建子协程,其实步骤都是差不多的。创建外层协程时根据作用域的上下文对象构建一个协程对象,然后根据启动模式启动协程;而创建子协程时则是将父协程的实例当作作用域,然后重复这个步骤。所以如果我们把从作用域的launch构建器构建协程到协程的启动执行搞通了,其他的情况都是差不多的,接下来就从launch着手跟踪源码:
GlobalScope.launch
delay(1000)
println("协程执行完毕")
下面的源码跟踪流程请看注释,★index(实心五角星index)表示调用主流程,☆index(空心)表示重要主流程的分支;★★★表示很重要的步骤
2.1 CoroutineScope.launch()创建协程对象
//★1. 协程作用域构建协程
CoroutineScope.launch(context, start, block:Function2)
/**协程构建器*/
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT, //默认启动模式
block: suspend CoroutineScope.() -> Unit
): Job
//★2. 为协程创建新上下文 = 作用域上下文 + 参数上下文 + Dispatchers.Default(如果没有设置调度器或者拦截器的情况下)
val newContext = newCoroutineContext(context)
//★3. 创建一个协程对象
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
//★4. 调用协程的start()函数启动协程
coroutine.start(start, coroutine, block)
return coroutine
//☆2. 为新协同程序创建上下文
public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext
//新上下文 = 作用域上下文 + 参数上下文
val combined = coroutineContext + context
val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined
//★★★ 如果没有指定其他dispatcher或[ContinuationInterceptor]时将安装[Dispatchers.Default]
return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null)
debug + Dispatchers.Default else debug
/**☆4. 调用AbstractCoroutine协程抽象类的start()函数*/
public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T)
initParentJob() //★★★4.1 初始化父作业
/**
* ★4.2 调用start(...),基于start参数启动协程,注意start()的后两个参数receiver和this都是上一步创建的协程对象
* 很多人跟踪源码时在这里断掉了,不知道start()是什么意思,点击后发现光标定位在参数start上,
* 调用start对象的某个函数?调用构造函数构造start对象?其实这里是调用了start对象的调用操作符重载函数invoke()
*/
start(block, receiver, this)
★1: 通过作用域扩展函数launch()
创建并启动协程,launch()接受三个参数
- context:额外的上下文,默认为空
EmptyCoroutineContext
,将和作用域的上下文组合为新的上下文对象 - start:
CoroutineStart
类型枚举值,表示启动模式,默认为CoroutineStart.DEFAULT
立即启动 - block:挂起Lambda函数类型,上一篇文章我们分析过,它将被封装为一个继承了
SuspendLambda
并实现Function2
的匿名类,这个类会被创建两次对象,第一次是调用launch()函数时创建匿名对象将其作为一个普通的挂起函数类型Function2使用,第二次是调用Function2对象时在create()中创建对象。这两次创建对象的根本区别是传递给SuspendLambda
的构造方法的实参不同,第一次创建时传递的构造参数为null,表示这个对象仅仅是作为一个Function2类型的函数对象使用,而第二次传递的是launch()中创建的协程对象(请看下面2.4章节)
//jvm指令:编译期自动生成的一个匿名类,也就是launch的挂起Lambda表达式,发现它继承了SuspendLambda,实现了Function2接口
final class runable/SuspendKt$main$1 extends kotlin/coroutines/jvm/internal/SuspendLambda implements kotlin/jvm/functions/Function2
//反编译后的java代码,launch()最后一个参数实际上就是创建上面的SuspendKt$main$1类型对象,只是反编译后的写发更贴近我们的思维(匿名类),但是真实的jvm中是存在这个类的
BuildersKt.launch$default((CoroutineScope)GlobalScope.INSTANCE, (CoroutineContext)null, (CoroutineStart)null,
//① 第一次创建对象,传递的参数为null,被抢转为Function2类型
(Function2)(new Function2((Continuation)null)
private CoroutineScope p$;
int label;
public final Object invokeSuspend(@NotNull Object $result)
public final Continuation create(@Nullable Object value, @NotNull Continuation completion)
//③ 第二次创建对象,构造参数为completion,经过后面的跟踪发现completion就是launch中创建的协程对象
Function2 var3 = new <anonymous constructor>(completion);
//④ 将作用域对象赋值给成员变量p$
var3.p$ = (CoroutineScope)value;
return var3;
//② Function2的调用函数,接受两个参数,第一个是CoroutineScope类型,第二个是Continuation类型
public final Object invoke(Object var1, Object var2)
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
)...);
★2: 将作用域中的上下文对象和参数上下文组合为一个新的上下文对象newContext
,值得注意的是如果上下文中不存在ContinuationInterceptor
元素,则默认添加一个Dispatchers.Default
调度器,所以协程的上下文中比包含一个ContinuationInterceptor
类型的元素,协程调度器对象就是ContinuationInterceptor
的子类对象,下篇文章详细讲解
★3: 根据启动模式创建协程对象,将新上下文对象作为构造参数传入,发现协程中保存的是父协程的上下文,由于我们是在非挂起作用域构建协程,所以构建的协程并没有父协程,它的上下文是通过作用域上下文、参数上下文、调度器、当前协程Job组合的。如果是在一个协程中开启另一个子协程,那么子协程初始上下文将继承自父协程的上下文
public abstract class AbstractCoroutine<in T>(
//父协程的上下文 = 初始上下文(作用域的上下文or父协程上下文) + 构建器参数上下文 + 续体拦截器(调度器)
protected val parentContext: CoroutineContext,
active: Boolean = true
) ...
//context是当前协程对象的上下文 = 父上下文+当前协程对象作为Job
public final override val context: CoroutineContext = parentContext + this
★4: 调用协程AbstractCoroutine.start()
函数启动协程,传入了启动模式start、协程对象和Function2类型的block。协程的start(…)函数做了两件事:
- **★4.1 😗*调用
initParentJob()
将作用域Scope中初始上下文中的Job作为被创建的协程的父Job,以传递取消【详见2.2】 - **★4.2 😗*调用了参数中的启动模式
start(block, receiver, this)
,很多人在这里断掉了,就是因为不了解kotlin的调用操作符重载,start对应类型CoroutineStart
重写了调用操作符,所以start对象后跟这()表示调用该对象,将执行其invoke()
函数【详见2.3】
2.2 initParentJob()父子Job绑定,传递取消
//AbstractCoroutine协程抽象类的initParentJob()函数,
internal fun initParentJob()
//调用父类JobSupport的initParentJobInternal(),将作用域Scope中的Job作为父Job传入
initParentJobInternal(parentContext[Job])
//JobSupport类的函数,用于将当前协程对象作为子Job绑定给Scope中的父Job,这样就可以直接调用scope.cancel()实现取消传递了
internal fun initParentJobInternal(parent: Job?)
assert parentHandle == null
if (parent == null) //Scope中的Job可能为空,比如示例中我们用的GlobalScope
parentHandle = NonDisposableHandle
return
parent.start() // make sure the parent is started
//将当前协程作为子Job绑定到父Job上,返回一个句柄,这个handle维护了父子Job的引用
val handle = parent.attachChild(this)
parentHandle = handle
...
通过上一步创建的协程对象调用其initParentJob()
函数,这个函数在协程抽象类中,它从又调用了父类JobSupport
中的initParentJobInternal(parent: Job?)
函数并将父上下文中的Job作为父Job作为参数传入,然后通过attachChild()
函数为父子Job建立关系。
在之前的文章中我们说过协程作用域CoroutineScope
可以通过扩展函数cancel()
取消协程控制协程的生命周期,cancel函数从作用域上下文中获取Job对象调用其cancel(),该Job对象是作用域创建的协程对象的父Job。所有的作用域对象都应该包含一个Job上下文(除了GlobalScope外),用于传递取消。如果作用域中不存在Job类型的上下文元素,调用其cancel()函数会报错Scope cannot be cancelled because it does not have a job
,所以在自定义作用域时应该遵循这一规则。
2.3 CoroutineStart启动模式调用
/**启动模式枚举*/
public enum class CoroutineStart
DEFAULT, // 立即执行协程体,随时可以取消
LAZY, // 只有在用户需要的情况下运行
ATOMIC, // 立即执行协程体,但在开始运行协程体之前无法取消
UNDISPATCHED; // 立即在当前线程执行协程体,直到第一个 suspend函数调用,这里可以理解为耗时函数
//★5. 以receiver作为接受对象启动协程挂起Lambda代码块,注意传递的参数receiver、completion其实都是launch()中创建的StandaloneCoroutine类型的协程对象coroutine
public operator fun <R, T> invoke(block: suspend R.() -> T, receiver: R, completion: Continuation<T>): Unit =
when (this)
//★6:由于创建协程时使用默认的启动模式,会走这里
DEFAULT -> block.startCoroutineCancellable(receiver, completion)
ATOMIC -> block.startCoroutine(receiver, completion)
UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion)
LAZY -> Unit //懒执行的情况将直接返回,不会启动协程执行,需要调用job.start()启动
★6: 启动模式CoroutineStart的调用操作符重载函数invoke()中根据不同的启动模式调用不同的block扩展函数,根据lacunch()传入的默认启动模式DEFAULT
将会调用startCoroutineCancellable()
函数。block是launch()中传入的挂起函数类型block: suspend CoroutineScope.() -> Unit
的实例对象,而startCoroutineCancellable()
是定义给suspend (R) -> T
类型的函数的,这两个函数类型看起来不一样,我们将block的函数类型变换一下:
- 首先block是一个挂起函数,返回类型为Unit,所以初步将其定义为
suspend () -> Unit
- 其次,block对应的挂起函数将作为
CoroutineScope
的扩展函数,根据前面对kotlin扩展函数的讲解,在编译为class后,函数将增加一个CoroutineScope扩展接受类型参数在第一位,所以block的类型最终可被写成suspend (CoroutineScope) -> Unit
的形式
经过变化后,发现block的类型确实符合suspend (R) -> T
的形式,所以可以通过block对象调用startCoroutineCancellable()函数,根据前几步推断传递的实参receiver
和completion
其实都是launch()构建器中创建的StandaloneCoroutine
类型的协程对象coroutine
。
package kotlinx.coroutines.intrinsics
/**
* 接★6:挂起函数类型的扩展函数,针对block可以将该函数写为如下的java形式:
* static final void startCoroutineCancellable(
Function2 block, //block对应的类型
CoroutineScope receiver, //作用域
Continuation completion, //续体
Function1 onCancellation) //取消回调函数,因为给了默认值可不传
*/
internal fun <R, T> (suspend (R) -> T).startCoroutineCancellable(
receiver: R, completion: Continuation<T>,
onCancellation: ((cause: Throwable) -> Unit)? = null
) =
//★7. 安全的启动协程
runSafely(completion)
//★8. 调用挂起函数类型的扩展函数,重新创建SuspendLambda匿名子类对象,传入的两个参数receiver、completion都是launch()中创建的协程对象(协程第一次包装,包装为SuspendLambda)
createCoroutineUnintercepted(receiver, completion)
//★9. 调用续体(SuspendLambda实例)的intercepted(),(协程第二次包装DispatchedContinuation)
.intercepted()
//★10. 真正启动协程执行的地方
.resumeCancellableWith(Result.success(Unit), onCancellation)
private inline fun runSafely(completion: Continuation<*>, block: () -> Unit)
try
//☆7. 调用block(),这里的block是上一步传递给runSafely()函数的Lambda表达式,也就是执行★8、★9、★10
block()
catch (e: Throwable)
completion.resumeWith(Result.failure(e)) //执行失败将以一个异常结束协程
★7: runSafely()函数实际上就是执行了createCoroutineUnintercepted(receiver, completion)intercepted().resumeCancellableWith(Result.success(Unit), onCancellation)
这段代码,接下来的三部都将围绕SuspendLambda
展开
★8: 调用当前函数类型的扩展函数createCoroutineUnintercepted()第二次创建SuspendLambda
的匿名子类对象。【详见2.4】
★9: 第8步创建的SuspendLambda
匿名子类对象也是续体对象(SuspendLambda继承了ContinuationImpl),这里调用续体的intercepted()
函数拦截续体,将续体对象包装为一个DispatchedContinuation
类型。【详见2.5】
★10: 调用续体扩展函数resumeCancellableWith()
调度续体的执行。【详见2.6】
2.4 第二次创建SuspendLambda子类对象(协程对象包装为SuspendLambda)
createCoroutineUnintercepted()
函数定义在IntrinsicsJvm.kt
文件中,源码通过android Studio看不了,可以直接去github上查源码:
//源码路径 kotlin/libraries/stdlib/jvm/src/kotlin/coroutines/intrinsics/IntrinsicsJvm.kt
package kotlin.coroutines.intrinsics
/**
* ☆8. 创建一个新的接收器类型为R结果类型为T的可挂起计算实例,
* 针对上面的block来说就是创建接受类型为CoroutineScope,返回类型为Unit的
*/
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)
//☆8.1 当前函数对象block的类型是class runable/SuspendKt$main$1 extends SuspendLambda Function2,调用其create()再次创建SuspendLambda子类对象
create(receiver, probeCompletion)
else
createCoroutineFromSuspendFunction(probeCompletion)
(this as Function2<R, Continuation<T>, Any?>).invoke(receiver, it)
在2.1中分析launch()函数时,第一次创建的匿名挂起Lambda表达式对象虽然被抢转为Function2
类型使用,但是这个对象它确实也是SuspendLambda
类型的,SuspendLambda
继承了ContinuationImpl
,ContinuationImpl
继承了BaseContinuationImpl
。上面的函数首先判断block是不是BaseContinuationImpl
类型,发现是的就直接调用了create()
函数,如果不是则将block作为普通Function2类型调用从而间接触发create()。所以这一步的目的就是第二次创建真正意义的SuspendLambda子类对象,对协程对象进行包装,将launch()中创建的协程对象作为CoroutineScope类型赋值给p$,作为Continuation类型传递给SuspendLambda的构造参数。8.4说明了协程构建器接受的协程代码块block: suspend CoroutineScope.() -> Unit
为什么是CoroutineScope的匿名扩展函数,以及这个函数的真实接受者是当前的协程对象,所以协程代码块中的隐形的this就是当前协程对象。
//jvm指令:编译期自动成成的一个匿名类
final class runable/SuspendKt$main$1 extends SuspendLambda implements Function2
//反编译后的java代码
BuildersKt以上是关于kotlin协程硬核解读(4. 协程的创建和启动流程分析)的主要内容,如果未能解决你的问题,请参考以下文章