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对象实际就是上面的StandaloneCoroutineDeferredCoroutine类型,为什么不直接将协程对象返回呢?就是为了隐藏实现细节,只给我们暴露必要的最小单元,比如async启动的协程需要通过deferred.await()来获取结果值,所以它就返回了Deferred类型(Deferred是Job的子类)。

1.3 Continuation续体

上一篇文章中我们分析了Continuation续体接口和其实现类BaseContinuationImplContinuationImplSuspendLambda的关系,构建协程时传递的挂起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()函数,根据前几步推断传递的实参receivercompletion其实都是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继承了ContinuationImplContinuationImpl继承了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. 协程的创建和启动流程分析)的主要内容,如果未能解决你的问题,请参考以下文章

kotlin协程硬核解读(4. 协程的创建和启动流程分析)

kotlin协程硬核解读(4. 协程的创建和启动流程分析)

kotlin协程硬核解读(6. 协程调度器实现原理)

kotlin协程硬核解读(6. 协程调度器实现原理)

kotlin协程硬核解读(6. 协程调度器实现原理)

kotlin协程硬核解读(6. 协程调度器实现原理)