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 
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

open-Xu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值