kotlin协程硬核解读(2. 协程基础使用&源码浅析)

Posted open-Xu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了kotlin协程硬核解读(2. 协程基础使用&源码浅析)相关的知识,希望对你有一定的参考价值。

版权声明:本文为openXu原创文章【openXu的博客】,未经博主允许不得以任何形式转载

文章目录

上一篇文章我们使用Retrofit+协程的示例初步体验了协程的魅力,并解释了kotlin协程为什么会出现。这篇文章我们了解一些协程的相关概念和kotlin协程库提供的API,文章中可能会对相关类进行一些原理的讲解,如果大家看不明白也没关系,因为后面几篇文章会深入源码讲解相关原理,到时候再回头看这里就明白了。而这篇文章的目的是学会协程库相关API使用。文章内容可能比较多比较杂,大家可以只看一些简单的部分,学会简单的使用协程,当遇到问题时根据大纲定位到相应内容再深入了解。

1. 依赖

kotlin协程源码(kotlinx.coroutines),如果是在纯Java或者Kotlin项目中使用协程,需要添加对kotlinx-coroutines-core模块的依赖,很明显它是kotlin协程核心实现库:

//根build.gradle
buildscript 
    //指定kotlin版本,通常使用最新版本
    ext.kotlin_version = "1.4.20"

allprojects 
    repositories 
        jcenter()   //确保jcenter()或mavenCentral()位于存储库列表中
    


//module build.gradle
dependencies 
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'

如果需要在具体平台使用,还需要引入平台库,比如在android上,通常还需要引入kotlinx-coroutines-android,它是协程针对Android提供的特有的支持,提供了适应Android应用程序的API(就像RxJava和RxAndroid的关系),其Dispatchers.Main 为Android应用程序提供上下文:

def kotlin_coroutines = '1.4.2'  
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines'
//kotlinx-coroutines-android依赖了kotlinx-coroutines-core
//implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines'

注意:kotlinx-coroutines-core包含协程正常运行不需要的资源文件,这些文件仅由调试器使用,在Android开发生成apk时可通过gradle配置在不损失功能的情况下避免apk中包含这些文件:

android 
    packagingOptions 
        exclude "DebugProbesKt.bin"
    

2. 相关概念及术语

协程

协程是可挂起计算的实例,它需要一个代码块(挂起lambda表达式)运行,并具有类似的生命周期(可以被创建、启动和取消),它不绑定到任何特定的线程,可以在一个线程中挂起其执行,并在另一个线程中恢复,它在完结时可能伴随着某种结果(值或异常)。

在Kotlin协程库中,所有的协程都是抽象类kotlinx.coroutines.AbstractCoroutine的子类,在使用协程编写代码时,我们不会接触到该类,因为根本不需要我们关心协程是怎样执行、怎样调度的。我们需要做的就是告诉协程库,我需要开启协程了、我要在哪个线程中执行挂起函数、我需要在什么时候取消协程,Kotlin协程库为我们暴露了协程上下文CoroutineContext、协程作用域CoroutineScope、协程工作Job来完成这些工作,这三个类就是协程库暴露给我们的API,了解这3个类就能玩转协程绝大部分的使用场景。

协程构建器

使用一些“挂起lambda表达式”作为参数来创建一个协程的函数称为协程构建器,并且可选地,还提供某种形式以访问协程的结果。kotlin协程库提供了几个构建器如launchasync等用于构建协程

挂起函数

使用suspend修饰符标记的函数。它可能会通过调用其他挂起函数挂起执行代码,而不阻塞当前执行线程。挂起函数不能在常规代码中被调用,只能在其他挂起函数或挂起lambda表达式中。标准库提供了原始的挂起函数,用于定义其他所有挂起函数。协程之所以能用同步的方式编写异步代码,正是由于挂起函数的非阻塞挂起,说的通俗一点,协程在调用到挂起函数时,会挂起当前状态,让当前线程去做别的事情,而挂起函数通常(但不一定)会切到别的线程执行,当挂起函数执行完毕会将结果值或者异常对象带到当前线程,恢复当前线程的状态让它继续执行协程中的代码

挂起函数类型

表示挂起函数以及挂起lambda表达式的函数类型。它就像一个普通的函数类型,但具有suspend修饰符。举个例子,suspend () -> Int是一个没有参数、返回Int的挂起函数的函数类型。一个声明为suspend fun foo() : Int 的挂起函数符合上述函数类型。launch()函数接受一个挂起函数类型的参数block

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit   //接受一个 挂起函数类型 作为参数
): Job 
	...

挂起lambda表达式

如果函数的最后一个参数是函数类型,调用该函数时需要传入一个函数类型实例,由于函数最后一个参数是函数类型或者函数接口,调用它时可以将函数类型实例写在方法体外,这就是普通的普通的lambda表达式。挂起lambda表达式与普通lambda表达式在形式上是一样的,不同的是它的函数类型被suspend修饰符标记。就像常规lambda表达式是匿名局部函数的短语法形式一样,挂起lambda表达式是匿名挂起函数的短语法形式。我们可以在挂起lambda表达式中调用其他挂起函数挂起执行代码,而不阻塞当前执行线程。

你可能在上一篇文章或者其他文章中有看到这样的描述:创建协程时括起来的就是协程,这其实是不准确的,括起来的实际上是一个匿名挂起函数,它作为实参传给launch函数,这个函数中封装了协程需要执行的所有代码。由于它是一个挂起函数,所以我们可以在它的中调用其他挂起函数。

    /**
     * launch()函数接受一个挂起函数类型作为参数,在调用lacunch函数是使用lambda表达式的形式
     * 由于这个lambda表达式对应的函数类型是挂起函数类型,所以称这个lambda表达式为挂起lambda表达式
     */
    GlobalScope.launch    //挂起lambda表达式
        //调用其他挂起函数
    

挂起作用域

挂起作用域是指挂起函数的函数体之内的区域,通俗的讲就是挂起函数的括起来的区域就是挂起作用域。只有在这个区域内才能调用挂起函数。如下:

    GlobalScope.launch    //launch()接受一个挂起函数类型作为参数,所以lambda表达式里面就是挂起作用域
        //挂起作用域
    

    //挂起函数
    suspend fun mySuspendFun() : Int = withContext(Dispatchers.IO)
        //挂起作用域
    

挂起点

协程执行过程中可能被挂起的位置,从语法上说,挂起点是对一个挂起函数的调用,但实际的挂起在挂起函数调用了标准库中的原始挂起函数时发生。挂起的原理是函数return了一个特殊的COROUTINE_SUSPENDED标志,这个会在下篇文章中详细讲解。

续体

是挂起的协程在挂起点时的状态,它在概念上表示在挂起点之后的剩余应执行的代码。已经创建,但尚未启动的协程,由它的初始续体表示,这由它的整个执行组成,类型为Continuation

3. 协程构建器

如果要调用挂起函数,首先得通过协程构建器构建协程,因为挂起函数的调用源头只能是协程代码块中。标准库提供了用于在常规非挂起作用域中启动协程执行的函数,这些函数称为协程构建器。在点击查看GlobalScope.launch()函数源码时,发现这个函数定义在kotlinx.coroutines.Builders.common.kt文件中,定位发现它编译后的class文件为kotlinx.coroutines.BuildersKt.class

为什么源文件和class文件的名称不同?因为kotlin允许直接在文件中定义函数,这些函数称为顶层函数,但是kotlin源文件最终会被编译为class文件,这些顶层函数应该被放在哪个class文件中?在Builders.common.kt文件最上方发现了@file:JvmName("BuildersKt")注解,表示这个源文件中的file成员会被编译到名为BuildersKt的class文件中

BuildersKt.class内容如下:

package kotlinx.coroutines

public fun <T> runBlocking(context: kotlin.coroutines.CoroutineContext /* = compiled code */, block: suspend kotlinx.coroutines.CoroutineScope.() -> T): T  contract  /* compiled contract */ ; /* compiled code */ 
...

public suspend fun <T> withContext(context: kotlin.coroutines.CoroutineContext, block: suspend kotlinx.coroutines.CoroutineScope.() -> T): T  contract  ;

public fun <T> kotlinx.coroutines.CoroutineScope.async(context: kotlin.coroutines.CoroutineContext , start: kotlinx.coroutines.CoroutineStart, block: suspend kotlinx.coroutines.CoroutineScope.() -> T): kotlinx.coroutines.Deferred<T>  

public suspend inline operator fun <T> kotlinx.coroutines.CoroutineDispatcher.invoke(noinline block: suspend kotlinx.coroutines.CoroutineScope.() -> T): T   

public fun kotlinx.coroutines.CoroutineScope.launch(context: kotlin.coroutines.CoroutineContext , start: kotlinx.coroutines.CoroutineStart , block: suspend kotlinx.coroutines.CoroutineScope.() -> kotlin.Unit): kotlinx.coroutines.Job  

从类名BuildersKt可以看出这是协程的构建器对应的类,一共有5个函数,都可以用来构建一个新的协程,其中runBlocking()withContext()是顶层函数,这两个函数可以直接调用(不需要创建对象),但是withContext是一个suspend挂起函数,它只能在协程或其他挂起函数中调用(必须先有协程)。invoke函数是CoroutineDispatcher(协程调度器)的扩展,它也是一个suspend函数,同样也是需要先有协程。launch()async()CoroutineScope接口的扩展函数,需要使用CoroutineScope的实例对象调用。

注意:所有的构建器其实都是函数,下面的讲解中可能会出现如launch()或者launch的形式其实都是指launch()函数的构建器。只是不同的语言场景表现形式不一样罢了,如果把它当成构建器偏向于写成launch的形式,将它描述为函数是将写为launch()

综上,如果要在非挂起作用域中启动协程,有3种构建器:

3.1 runBlocking

fun main() 
    // 使用runBlocking顶层函数创建协程
    runBlocking 
        ...
    

runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T)函数是一个顶层函数(直接定义在Builders.kt文件中),它接受两个参数,第一个参数是CoroutineContext协程上下文,被赋予默认值EmptyCoroutineContext,所以调用它时通常只需要传第二个参数。

这个函数创建的协程会阻塞当前线程,直到协程代码块执行完毕,所以通常用于main函数或者其他测试用例中

因为在main函数中启动一个协程去执行耗时任务,如果不阻塞main函数的线程,main函数执行完jvm就退出了,为了避免jvm退出,通常在最后需要Thread.sleep(Long.MAX_VALUE)让主线程休眠来等待协程执行完毕。但是如果使用runBlocking创建协程就不会出现jvm提前退出的问题

3.2 launch

//使用GlobalScope单例对象调用launch
fun main() 
    val job = GlobalScope.launch 
        //调用其他挂起函数
        for(i in 1..5)
            delay(1000)   //每隔1s打印一个数字
            println("--->$i")
        
    

    Timer().schedule(object:TimerTask()
        override fun run() 
            println("-----------------------------3s时间到----------------------------")
            println("协程是否活动?$job.isActive")          //true
            println("协程是否执行完毕?$job.isCompleted")   //false
            println("协程是否取消?$job.isCancelled")       //false
            job.cancel("中途取消协程,生命周期结束")
            println("协程是否活动?$job.isActive")          //false
            println("协程是否执行完毕?$job.isCompleted")   //false
            println("协程是否取消?$job.isCancelled")      //true
        
    , 3000)   //3s后结束协程
    Thread.sleep(100000)  //阻止jvm退出

launch()函数在不阻塞当前线程的情况下启动新的协程,并将对协程的引用作为Job返回,可以通过调用Job.cancel()取消协程。

需要注意的是通过GlobalScope创建的协程是全局范围的协程,是顶层协程,其生命周期与应用程序同步。也就是说即便协程执行完毕,但是应用程序没有关闭,协程还是会继续运行着,如果我们不停的创建很多顶层协程,虽然它是轻量级的但仍然会消耗一些内存资源,所以如果要创建顶层协程,通常需要手动保持Job引用,在合适的时机调用job.cancel()退出这些协程

3.3 async

// 使用 GlobalScope单例对象调用async
fun main() 
    GlobalScope.async 
        //调用其他挂起函数
        delay(1000)
        println("GlobalScope.async")
    

asynclaunch是差不多的,默认情况下,创建的协程都会立即执行,不同的是这两个函数返回类型不同,launch返回的是Job,可以通过Job取消协程,而async返回的是Deferred类型,DeferredJob的子类,所以同样可以cancel协程,但是它是一个有结果的Job,也就是说async可以返回一个值,这个值将保存在Deferred中,可以调用Deferred.await()获取协程的返回值,而**await()是一个挂起函数,只能在挂起作用域内调用,所以通常不用async来创建最外层的协程**,因为非挂起作用域无法调用await()函数获取协程的返回值,所以返回值没有意义,这样的话async()的返回值Deferred就是普通的Job,所以完全可以使用launch代替async

async通常用于在挂起作用域中构建并发的子协程,这些子协程作业将并发执行,但是可以等待并发协程都返回数据后,合并处理结果,这种需求在开发中是非常常见的:

fun main() 
    val job = GlobalScope.launch 
        //通过async构建的协程默认会立即执行,因为默认启动模式为CoroutineStart.DEFAULT
        val deferred1 = async(start = CoroutineStart.DEFAULT)
            delay(1000)
            println("1 子协程返回数据")
            "deferred1"
        
        val deferred2 = async
            delay(2000)
            println("2 子协程返回数据")
            "deferred2"
        
        //async的执行结果被封装在Deferred对象中,需要调用await()获取结果值
        val result1 = deferred1.await()  //获取第1个子协程的返回值
        val result2 = deferred2.await()  //获取第2个子协程的返回值
        println("返回数据:$result1 - $result2")   //合并两个返回结果 deferred1 - deferred2
    
    Thread.sleep(100000)  //阻止jvm退出

3.4 怎样获取CoroutineScope的实例对象?

launch()async()构建器是CoroutineScope接口的扩展函数,只有CoroutineScope的子类对象才能调用这两个函数。kotlin协程核心库中只暴露了GlobalScope这一个子类单例给我们,所以上面的示例中我就直接使用了这个对象创建协程,但是GlobalScope全局作用域创建的协程即使执行完毕也不会退出,会引发内存泄漏,使用局限性太大,难道就没有其他子类对象可使用了吗?

协程库还提供了MainScope()CoroutineScope(context:CoroutineContext)两个工厂函数来获取一个CoroutineScope实例对象。当然我们还可以通过实现CoroutineScope接口自定义作用域类然后创建它的对象:

/**自定义协程作用域类*/
class MyCoroutineScope(context: CoroutineContext) : CoroutineScope 
    override val coroutineContext: CoroutineContext = context

fun main() 
    //构建一个自定义作用域对象
    val coroutineScope = MyCoroutineScope(Dispatchers.Unconfined)
    //调用launch函数创建协程
    coroutineScope.launch 
        delay(1000)
        println("协程是在$Thread.currentThread()执行的")
    
    Thread.sleep(100000)  //阻止jvm退出

至于我们在什么情况下该选择哪种作用域对象,稍后会单独详细讲解CoroutineScope

3.5 其他构建器(构建子协程)

上面几种方式可以在非挂起作用域中启动协程(构建最外层协程),如果是在挂起作用域内,还可以通过其他构建器创建子协程,如withContext()coroutineScope()。严格的说来这两个函数称为作用域函数,只是它们真的可以创建子协程,所以这里暂且给它们一个协程构建器的名分吧:

GlobalScope.launch()   
    //1. 构建子协程: 通常用于切换线程、或者自定义挂起函数
    withContext(Dispatchers.IO)
    //2. 构建子协程: 通常用于分担父协程任务(将作业任务分块)
    coroutineScope
    //3. 构建子协程: 通常用于构建并发任务,等待结果合并
    val deferred = async   
    ...

3.6 总结

协程构建器有runBlockinglaunchasyncwithContext()coroutineScope等,这么多种协程构建器到底什么时候该用哪一种?上面已经对每种构建器进行了分析,相信大家也应该知道怎么选择了,这里总结一下:

注:文章中的"最外层协程"、“顶层协程”、"非挂起作用域构建协程"都是指在非挂起函数中构建的协程

构建器能否构建最外层协程能否构建子协程特点常用指数(五星)常用场景
runBlocking阻塞"主线程"避免jvm退出★★main函数或者其他测试场景
launch在非挂起作用域构建协程,不会阻塞"主线程"★★★★★所有场景中构建顶层协程
async能(不建议,因为没有意义,完全可以用launch代替)多个async协程能并发执行,可以有返回值★★★★需要并发执行的场景
withContext(Dispatchers.xx)不能通常接受Dispatchers调度器上下文元素作为参数★★★★★★通常用于切换线程或者自定义挂起函数,这个可以说是比launch的使用频率还高
coroutineScope不能上下文环境跟父协程完全相同通常用于分担封装任务,便于代码阅读

4. CoroutineScope协程作用域

4.1 怎样选择合适的CoroutineScope

public interface CoroutineScope 
    /**作用域的上下文*/
    public val coroutineContext: CoroutineContext

CoroutineScope是一个接口,直译为协程范围(作用域),我们可以大胆猜测它可能就是控制协程的创建和取消的,而协程就运行在这个动作中间的时段,有点控制协程生命周期的意思了哈。如果要在非挂起函数内构建协程,要么调用runBlocking(会阻塞线程,使用局限性大),要么通过CoroutineScope的子类对象调用扩展函数launch(),那它到底有多少种子类呢?请看下图分析:

CoroutineScope的子类看起来有好多,但是以Coroutine结尾的子类实际上都是AbstractCoroutine(协程抽象类)的子类,虽然AbstractCoroutine也继承自CoroutineScope,但是还是把它们叫做协程而不是协程作用域。就好比虽然人也是动物,其他动物也是动物,你可以当着小朋友的面指着一条小狗说:你看这个小动物好可爱啊,但是你要是指着一个人说:你看这个小动物好可爱啊,你看人家不抽你。你可能会觉得委屈,人确实是动物啊,没毛病吧?严格意义上来说确实没毛病,但是人和其他动物创造的价值不同(也就是功能关注点不同),人们觉得自己对社会创造的价值比其他动物大,而动物只是简单的吃喝拉撒睡为了活着,所以不应该和它们混为一谈,而人之所以是动物也是为了活着去创造价值,如果人类能找到一个不吃喝就能活着的办法,也许就会慢慢进化得和动物没有任何关系了,但是却永远不会丢失创造价值的主要功能。AbstractCoroutine继承了JobCoroutineScopeContinuation,当我们创建一个协程对象时,返回的类型是Job,而不是AbstractCoroutine,因为协程的目的是按顺序执行协程代码块中的代码以及控制协程的生死,返回的Job类型就可以满足这些功能,而作为续体和作用域类型的作用是协程内部实现时为了方便,而这些对我们是不可见的,所以当我们关注一个类的时候应该了解它的主要功能是什么,其他的功能只是辅助。

子类中以Scope结尾的还有几个,除了ContextScopeGlobalScope,另外的都不在协程核心库,而ContextScope(根据协程上下文创建的协程作用域)是internal(模块内使用),所以Kotlin协程库真正暴露给我们的就只有GlobalScope这一个子类。既然定义了ContextScope为什么不给我们使用?留着过年吗?不是的,协程库提供了顶层函数MainScope()CoroutineScope()来获取ContextScope的实例对象,CoroutineScope(context)函数就是根据传入的协程作用域对象创建一个ContextScope上下文作用域实例对象,MainScope()其实也是调用了CoroutineScope(context),它传入Dispatchers.Main主线程调度器上下文作为参数。

我就想通过作用域对象创建一个协程,你给我扒扒儿这么多干什么?其实我就是想告诉你,在不同的场景创建协程应该使用不同作用域对象,而协程核心库中只有GlobalScopeContextScope两个子类可以使用,TMD两个怎么够?最起码要3个啊…其实这两个已经足以应付大部分使用场景了。比如:

  • 我只想在main函数中做个测试,那就用runBlocking或者GlobalScope.launch
  • 我想在UI线程中开启协程,那就用MainScope().launch
  • 其他场景中那就用CoroutineScope(context).launch ,想在哪个线程执行、协程名字是什么…通过传递上下文参数都搞定,这种方式通吃一切场景

4.2 CoroutineScope有什么作用

想要知道一个类有什么作用,最直接的方式就是通过看它的源码了解它有那些成员变量、提供了哪些函数(包括扩展函数)。CoroutineScope相关的源码在下面👇👇👇贴出来了,内容比较长大家可以选择不看,直接看总结:

  • 接口没有任何抽象方法需要实现,仅仅维护了一个上下文成员变量。所以不同的作用域类型或者对象的本质区别是它们持有不同的上下文数据对象,它将作为初始上下文传递给被创建的协程(协程上下文就是一堆数据集合,是专门为协程保存数据的)
  • 除了单例对象GlobalScope获取作用域实例对象的最佳方法是CoroutineScope(context)MainScope()工厂函数
  • 重载了+操作符,可以通过plus操作符将指定上下文添加到此作用域,用相应的键覆盖当前作用域上下文中的现有元素。比如CoroutineScope(Dispatchers.Main)CoroutineScope(EmptyCoroutineContext)+Dispatchers.Main是一样的效果
  • cancel()扩展函数用于取消作用域(取消协程),在Android开发中通常需要在组件onDestroy()方法中调用scope.cancel()避免内存泄漏,需要注意的是只有上下文中包含Job的Scope才能调用cancel(),否则会抛异常
  • isActive扩展属性判断当前作用域是否处于活动状态

根据源码发现作用域除了构建协程外,其他的操作都是围绕它的成员遍历coroutineContext协程上下文,要么就是获取or修改上下文对象,要么就通过上下文元素实现某个功能,比如cancel()isActive都是通过coroutineContext来完成的

package kotlinx.coroutines

/**
 * ☆☆☆不要看我,太长看了也没用
 * 
 * 定义新协同程序的作用域,每个协程构建器(比如launch、async等)都是CoroutineScope的扩展函数,通过CoroutineScope创建的协程它也有自己的协程上下文CoroutineContext,它继承自作用域的coroutineContext元素来自动传播其上下文和可取消性(CoroutineContext.cancel()扩展)。
 *
 * 获取Scope作用域实例对象的最佳方法是CoroutineScope(context)和MainScope()工厂函数
 *
 * ### 结构化并发惯例
 *
 * 不建议手动实现CoroutineScope接口,而应首选委派实现.
 * 根据惯例,CoroutineScope的上下文coroutineContext应包含一个[Job]的实例,以强制执行结构化并发,并传播取消
 *
 * 每个协程构建器(比如launch、async等)和每个作用域函数(如coroutineScope、withContext等)都将自己的作用域
 * 及其自己的[Job]实例提供给它运行的内部代码块。
 * 按照惯例,它们都会等待代码块中的所有协程完成之后再完成它们自己,从而强制执行结构化并发。
 *
 * ### Android使用
 *
 * Android中所有带生命周期lifecycle的实体都提供了对协程范围的支持
 *
 * ### 自定义使用
 *
 * [CoroutineScope] 应该在具有明确定义生命周期的实体上实现或者声明为属性,这些实体负责启动子协程,比如:
 *
 * class MyUIClass 
 *     val scope = MainScope() // the scope of MyUIClass
 *     fun destroy()  // MyUIClass实例销毁
 *         scope.cancel() // 取消在此范围内启动的所有协程
 *     
 *     //注意: 如果实例被销毁或者这个方法中任何已启动的协程抛出一个异常,将取消所有嵌套的协程
 *     fun showSomeData() = scope.launch  // 在主线程中启动协程
 *        // ... 在这里,我们可以使用挂起函数或与其他调度器的协程构建器
 *        draw(data) // 主线程更新UI
 *     
 * 
 */
public interface CoroutineScope 
    /**1. ★★★作用域的上下文成员变量*/
    public val coroutineContext: CoroutineContext


/**
 * 用于获取当前协程的上下文对象,此函数是避免在接收器位置[CoroutineScope.coroutineContext]名称冲突
 * GlobalScope.launch  // this: CoroutineScope
 *         //父协程
 *         val flow = flow<Unit>   //this: FlowCollector
 *           //子协程,如果在这里想获取子协程的上下文,直接使用coroutineContext是不行的
 *           coroutineContext          // 这是父协程的上下文
 *           currentCoroutineContext() // 当前协程的上下文
 *         
 *     
 */
public suspend inline fun currentCoroutineContext(): CoroutineContext = coroutineContext

/*****************2. ★★★获取作用域实例对象************************/
/**
 * 为UI组件创建主协程作用域
 * 使用示例:
 * class MyAndroidActivity 
 *     private val scope = MainScope()
 *     override fun onDestroy() 
 *         super.onDestroy()
 *         scope.cancel()
 *     
 * 
 */
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

/**根据给定的协程上下文元素创建一个协程作用域对象*/
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
        ContextScope(if (context[Job] != null) context else context + Job())

/**
 * 不绑定到任何作业的全局[CoroutineScope]
 * 全局作用域用于启动在整个应用程序生命周期内运行且不会过早取消的顶级协程
 * 应用程序代码通常应该使用应用程序定义的[CoroutineScope],不推荐使用GlobalScope
 */
public object GlobalScope : CoroutineScope 
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext


/*****************3. ★★★重载了+操作符************************/

/**
 * plus操作符将指定上下文添加到此作用域,用相应的键覆盖当前作用域上下文中的现有元素。
 * 这是“CoroutineScope(thiscope+context)”的缩写。
 */
public operator fun CoroutineScope.plus(context: CoroutineContext): CoroutineScope =
        ContextScope(coroutineContext + context)
/*****************4. ★★★取消作用域************************/
/**使用可选的原因取消此作用域,包括其Job及所有子协程。如果该作用域没有job则抛IllegalStateException异常*/
public fun CoroutineScope.cancel(cause: CancellationException? = null) 
    val job = coroutineContext[Job] ?: error("Scope cannot be cancelled because it does not have a job: $this")
   

以上是关于kotlin协程硬核解读(2. 协程基础使用&源码浅析)的主要内容,如果未能解决你的问题,请参考以下文章

kotlin协程硬核解读(2. 协程基础使用&源码浅析)

kotlin协程硬核解读(2. 协程基础使用&源码浅析)

kotlin协程硬核解读(1. 协程初体验)

kotlin协程硬核解读(1. 协程初体验)

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

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