第一节:协程的基础知识

前言

2022年6月份笔者入职了一家新能源汽车公司,从传统的互联网行业进入到了车企,直到现在。在Android方面的工作,从开发语言层面来说也从Java语言切换到了现在的Kotlin,接触到了协程。关于协程在实际开发工作中,笔者也使用了近一年的时间。从开始的陌生到现在的熟练使用,我渐渐对协程有了很大的兴趣,所以现在决定把有关协程的知识记录下来,当做学习和记忆。当然学习协程之前,我们需要有一定的Kotlin语法基础,如果对Kotlin语法基础还不熟悉的读者,可以先去阅读一下笔者的Kotlin语法基础的专栏。下面就开始我们的协程之旅吧~

1.什么是协程

  1. 从本质上来说,协程是一种轻量级的线程,不同协程之间的切换仅在编程语言的层面就可以实现。而线程之间的切换需要依靠操作系统的调度才能实现。
  2. 我们可以在单个线程中创建多个协程,协程支持挂起,挂起的同时又不会阻塞当前线程,这也是我们常说的非阻塞式挂起。
  3. 如何理解Android中的协程,我们来看ChatGPT给出的答案:

2.在Android中如何使用协程

由于协程并没有写入Android的内置API中,所以在Android中使用协程,我们需要在Android项目中app目录下的build.gradle.kts文件中加入以下依赖:

scss 复制代码
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")

目前官方最新的协程依赖库已经更新到1.8.1-Beta,而笔者对于协程方面的知识介绍都是基于上面我们引入的1.7.3版本。

3.协程的几种创建方式

  1. GlobalScope

GlobalScope是一个单例类,它实现了CoroutineScope接口。

csharp 复制代码
public object GlobalScope : CoroutineScope {
    override val coroutineContext: CoroutineContext get() = EmptyCoroutineContext
}

launch()则是CoroutineScope的一个扩展函数,该函数的返回值类型则是一个Job,我们使用该函数来启动一个协程,如下代码示例:

kotlin 复制代码
public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
) : Job
kotlin 复制代码
fun main() {
    GlobalScope.launch {
        println("hello launch.")
    }
}

运行这段代码,会发现没有任何日志输出。这是因为,Global.launch()每次创建的都是一个顶层协程,当应用程序运行结束时也会跟着一起结束,协程中的代码还未来得及去执行。要解决这个问题也比较简单,我们让程序延迟一段时间来保证JVM的存活,如下代码示例:

kotlin 复制代码
fun main() {
    GlobalScope.launch {
       println("hello launch.")
    }
    Thread.sleep(100)
}

// 输出
hello launch.

这里我们使用Thread.sleep()方法让当前线程阻塞100毫秒,然后成功输出了launch()函数中的日志。

  1. CoroutineScope

CoroutineScope是Kotin协程库中的一个顶层函数:

kotlin 复制代码
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job())
kotlin 复制代码
internal class ContextScope(context: CoroutineContext) : CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    ...
}
kotlin 复制代码
public interface CoroutineScope {
    // 此范围的上下文。上下文由作用域封装,并用于实现作为作用域扩展的协程生成器。
    // 不建议出于任何目的在常规代码中访问此属性,除非出于高级用途访问 Job 实例。
    public val coroutineContext: CoroutineContext
}

这里我们把CoroutineScope()函数、ContextScope类、以及CoroutineScope接口的代码都贴出来了,希望大家不要弄混了,因为这里的函数名和接口名确实是相同的。

CoroutineScope()函数要求我们传入一个协程上下文的参数,并将该上下文对象保存在ContextScope类中然后返回。

scss 复制代码
ContextScope(if (context[Job] != null) context else context + Job())

创建ContextScope对象的时候,这里会有一个if条件的判断,就是我们上下文中是否包含了Job类型的对象,如果没有就会帮我们强行添加一个。这里涉及到了协程上下文对象的数据结构,关于这块的内容,我们后面的文章也会详细说明,这里就不展开介绍了。而两个上下文对象可以相加,是重载了 + 操作符,在操作符重载的文章里我们也已经详细介绍了。

其用法和GlobalScope是一致的,都是使用CoroutineScope的扩展函数launch()来启动一个协程,如下代码示例:

kotlin 复制代码
fun main() {
    CoroutineScope(Dispatchers.IO).launch {
        println("hello launch.")
    }
    Thread.sleep(100)
}

GlobalScope创建的是一个全局的协程作用域,而CoroutineScope()函数创建的是一个临时的协程作用域。所以在实际开发中我们更多的是使用CoroutineScope()函数,而不是GlobalScope这个单例类。

  1. runBlocking

runBlocking同样也是Kotlin协程库中的一个顶层函数:

kotlin 复制代码
public actual fun <T> runBlocking(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T {
    ... 
    val currentThread = Thread.currentThread()
    ...
    val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
    coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
    return coroutine.joinBlocking()
}

和上面介绍到的GlobalScope和CoroutineScope()不同的是,使用runBlocking来创建一个协程作用域,该方法会先阻塞当前线程,直到协程中的代码执行完成,才会释放当前线程,而且该方法拥有返回值。具体用法也比较简单:

kotlin 复制代码
fun main() {
    val result = runBlocking {
        "hello runBlocking"
    }
    println("$result")
}

// 输出
hello runBlocking.

那么runBlocking是如何阻塞当前线程的呢?我们可以看BlockingCoroutine中的joinBlocking方法:

kotlin 复制代码
fun joinBlocking(): T {
    ...
    while (true) {
          if (isCompleted) break
    }
    ...
    val state = this.state.unboxState()
    (state as? CompletedExceptionally)?.let { throw it.cause }
    return state as T
}

为了方便阅读这里对源码进行了一些简化。在这里我们可以看到,runBlocking函数是通过while循环来阻塞当前线程的,通过判断当前协程中的任务是否已经执行完成,来取消循环,从而释放当前线程。

所以在实际开发中我们一定要慎用它,特别是在主线程中,一不小心可能就会造成主线程阻塞,随之就喜提一个ANR。

  1. MainScope

MainScope同样是一个顶层函数,该方法返回一个CoroutineScope对象,并将我们的代码逻辑指定在主线中去运行。

kotlin 复制代码
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

关于参数SupervisorJob + Dispatchers.Main我们会在后面的文章详细讲解,这里我们只需要了解MainScope()函数可以为我们快速的创建一个运行在主线程的协程作用域。使用起来也比较方便,如下代码示例:

scss 复制代码
MainScope().launch { println("MainScope called...") }

4.上下文调度器

上面我们已经介绍到协程是一种轻量级的线程,我们可以在单个线程中创建多个协程。但是这并不意味着我们永远不需要开启线程了,比如说Android中要求网络请求必须在子线程中进行,即使你开启了协程去执行网络请求,假如它是在主线程中的协程,那么程序依然会出错。

所以我们对协程的定义:"轻量级的线程"。这是在协程的创建和切换的维度去定义的,实际开发中我们依然需要结合线程来使用。关于在协程中切换线程Kotlin协程库中已经帮我们封装好了,使用起来也比较方便。

在Dispatchers类中,Kotlin协程库为我们提供了四种不同的协程上下文调度器,它们共同特点是都实现了CoroutineDispatcher这个抽象类:

kotlin 复制代码
public abstract class CoroutineDispatcher :
    AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor
kotlin 复制代码
public actual object Dispatchers {
    public actual val Default: CoroutineDispatcher = DefaultScheduler

    public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher

    public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
    
    public val IO: CoroutineDispatcher = DefaultIoScheduler

具体的区别如下:

类型 区别
Default 默认使用一种低并发的线程策略,适合低密度的任务计算,在子线程中执行代码
Main 切换到主线中执行代码
Unconfined 不切换线程,在当前线程中执行代码
IO 使用较高并发的线程策略,适合高密度的任务计算,在子线程中执行代码

熟悉了协程的上下文调度器,我们就可以在实际开发中灵活的运用它们了,如下代码示例:

ini 复制代码
CoroutineScope(Dispatchers.Main).launch {
    val useInfo = requestUserInfo()
    tvUserName.text = useInfo
    val homeInfo = requestHomeInfo()
    tvHomeInfo.text = homeInfo
}
kotlin 复制代码
suspend fun requestUserInfo() = withContext(Dispatchers.IO) {
    delay(1000)
    "requestUserInfo"
}

suspend fun requestHomeInfo() = withContext(Dispatchers.IO) {
    delay(1000)
    "requestHomeInfo"
}

在挂起函数withContext中,我们使用挂起函数delay来模拟网络请求中的耗时任务在子线程中去执行,获取到结果在切换到主线中更新UI。切换流程如下示意图:

5. withContext

withContext是一个挂起函数,关于挂起函数后面我们会详细的介绍。这里我们只需要知道使用suspend关键字修饰的函数就是挂起函数,挂起函数只能在协程作用域中或者在另一个挂起函数中访问。下面我们就来看下withContext函数的使用:

kotlin 复制代码
public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T               
): T {
    return suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
        ...
        val coroutine = DispatchedCoroutine(newContext, uCont)
        block.startCoroutineCancellable(coroutine, coroutine)
        coroutine.getResult()
    }
}

withContext函数接收两个参数,一个是协程上下文对象context,一个是带有接收者的函数类型参数block,而且这个函数类型是挂起函数类型。在该函数类型初始化的Lambda表达式中将会拥有当前协程的作用域。Lambda表达式中的最后一行代码也会作为withContext函数的返回值。withContext函数会将当前协程挂起,直到获取到结果后才会恢复当前协程。

kotlin 复制代码
fun main() {
    CoroutineScope(Dispatchers.IO).launch {
        val result = withContext<String>(coroutineContext) {
            return@withContext "result"
        }
        println(result)
    }
    Thread.sleep(100)
}

// 输出
result

这里我们使用父协程中的上下文对象作为参数传递给了withContext()函数,并且在Lambda表达式中返回了一个字符串result。在withContext函数内部又调用了suspendCoroutineUninterceptedOrReturn挂起函数,该函数会捕获到当前协程作用域中的Continuation实例,具体代码如下:

kotlin 复制代码
public suspend inline fun <T> suspendCoroutineUninterceptedOrReturn(crossinline block: (Continuation<T>) -> Any?): T {
    contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
    throw NotImplementedError("Implementation of suspendCoroutineUninterceptedOrReturn is intrinsic")
}

在这里我们并不能看到它是如何捕获到协程中的Continuation实例,我想这也是Kotlin协程设计Continuation的初衷,开发者只能通过suspendCoroutineUninterceptedOrReturn这个方法来获取,如果你对协程的使用比较熟悉,你会发现像suspendCoroutine()suspendCancelableCoroutine()delay()yield()等方法中都是通过该方法来获取当前协程的Cotinuation实例。

6. async、await

async也是CoroutineScope的一个扩展函数,它总是和挂起函数await()成对的出现。该函数拥有一个Deferred类型的返回值:

kotlin 复制代码
public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> 

当我们调用async函数时,其拥有协程作用域的Lambda表达式中的代码是立即执行的,而当我们调用await()函数时,如果async函数中的逻辑还没有执行完成,await()函数就会将当前协程挂起。下面我们就来看一个具体的案例:

kotlin 复制代码
fun main() {
    val curTimeMills = System.currentTimeMillis()
    runBlocking {
        val left = async { 5 + 5 }.await()
        val right = async { 6 + 6 }.await()
        println("result = ${left + right}")
    }
    val endTimeMills = System.currentTimeMillis() - curTimeMills
    println("time = $endTimeMills")
}

// 输出
result = 22
time = 262

在上面这段示例代码中我们可以看到,我们使用两个async()函数执行了一个简单的求和逻辑运算,并将最终的结果输出,总耗时是 262 毫秒。上面我们说了await()函数是会阻塞当前协程的,我们可以将上面的代码稍作改动:

kotlin 复制代码
fun main() {
    val curTimeMills = System.currentTimeMillis()
    runBlocking {
        val leftDef = async { 5 + 5 }
        val rightDef = async { 6 + 6 }
        println("result = ${leftDef.await() + rightDef.await()}")
    }
    val endTimeMills = System.currentTimeMillis() - curTimeMills
    println("time = $endTimeMills")
}

// 输出
result = 22
time = 186

最终我们的逻辑执行时长从 262 毫秒变成了 186 毫秒,当然这个例子还不够明显,如果aysnc函数中执行的是较耗时的任务,那么结果会更明显。我们再来分析一下这两段代码的区别:

1.第一种写法在调用完async函数后,立即执行了await()函数,那么后面的逻辑会等待async函数中的逻辑处理完成以后才会执行。

2.第二种写法开始先分别执行async()函数中的逻辑,然后再分别调用await()函数。这样我们两个async()函数在开始都可以执行,当我们调用await()方法的时候再去判断是否挂起当前协程。如果async()中的任务执行完成,则直接返回结果;如果未执行完成,则先挂起协程,等待任务执行完成。

7. coroutineScope

coroutineScope()函数同样也是一个挂起函数,与我们上面所介绍的CoroutineScope()函数并不是同一个函数,该函数首字母是小写的。它的特点是会继承外部的协程作用域并创建一个子作用域。同withContext函数一样两者都是使用Kotlin协程库中的顶层函数 suspendCoroutineUninterceptedOrReturn()函数来实现的。所以coroutineScope函数也会挂起当前协程,在任务执行完成后,再恢复当前协程。然后将获取到的结果在Lambda表达式中返回。

kotlin 复制代码
fun main() {
    runBlocking {
        val result = coroutineScope {
            println("coroutineScope called.")
            return@coroutineScope "coroutineScope"
        }
        println("result = $result")
    }
}

// 输出
coroutineScope called.
result = coroutineScope

从这段示例代码的输出结果来看,coroutineScope函数确实会先挂起当前协程,直到自己创建的子协程中的逻辑执行完成,才会释放当前协程。

8.挂起函数

  1. 挂起函数

Kotlin协程的挂起和恢复能力本质上就是挂起函数的挂起和恢复。在Kotlin中我们使用关键字suspend来声明一个挂起函数。

挂起函数只能在协程作用域内或其他挂起函数内调用。普通的函数无法调用挂起函数

如下代码示例,我们声明一个名为reqeust()的挂起函数:

kotlin 复制代码
suspend fun reqeust() { //doSomething }

2.挂起点

在协程内部挂起函数的调用处被称为挂起点,挂起点如果出现异步调用,那么当前协程就被挂起,直到对应的Continuation的resume函数被调用才会恢复当前协程。

例如,通过suspendCoroutine函数获得的Continuation是一个SafeContinuation的实例,与创建协程时得到的用来启动协程的Continuation实例没有本质上的差别。SafeContinuation类的作用也很简单,它可以确保只有发生异步调用时才会挂起,如下代码示例,虽然也有resume函数的调用,但协程并不会真的挂起。

kotlin 复制代码
suspend fun notSuspend() = suspendCoroutine<Int> { 
    it.resume(100)
}

异步调用是否发生,取决于resume函数与对应的挂起函数的调用是否在相同的调用栈上,切换函数调用栈的方法可以是切换到其他线程上执行,也可以是不切换线程但在当前函数返回之后的某一时刻再执行。前者比较容易理解,后者其实通常就是先将Continuation的实例保存下来,在后续合适的时机再调用。

这段描述来自霍丙乾老师的深入理解Kotlin协程。

  1. CPS

挂起函数通过 Continuation-Passing-Style(CPS, 续体传递风格)实现。每个挂起函数都有一个附加的 Continuation [续体] 参数,在调用时隐式传入,这一过程是Kotlin编译器帮我们实现的,无需开发者自己动手实现。

那么Cotinuation是什么呢?事实上Cotinuation就是一个接口,它的具体源代码如下:

kotlin 复制代码
// 表示挂起点之后的延续接口。
public interface Continuation<in T> {
    // 与此延续相对应的协程上下文、
    public val context: CoroutineContext

    // 恢复执行相应的协程,传递一个成功或失败的 result 作为最后一个挂起点的返回值。
    public fun resumeWith(result: Result<T>)
}

关于这个接口,注释写的也比较清楚,context属性用来保存当前协程中的上下文对象,resumeWith方法用于恢复执行协程,并将执行结果作为最后一个挂起点的返回值。

例如我们有这么一个挂起函数request:

kotlin 复制代码
suspend fun <T> request() : T

CPS 变换之后,它的实现实际是这样的:

kotlin 复制代码
suspend fun request(completion: Cotinuation<T>): Any?

其返回类型 T 移动到了附加的续体参数的类型参数位置。

实现中的返回值类型 Any? 被设计用于表示挂起函数的动作。当挂起函数 挂起 协程时,函数返回一个特别的标识值 COROUTINE_SUSPENDED。如果一个挂起函数没有挂起协程,协程继续执行时,它直接返回一个结果或者抛出一个异常。这样,request() 函数实现中的返回值类型 Any? 实际上是 TCOROUTINE_SUSPENDED 的联合类型,这并不能在 Kotlin 的类型系统中表示出来。

COROUTINE_SUSPENDED标志是个常量,定义在Intinsics.kt中

kotlin 复制代码
public val COROUTINE_SUSPENDED: Any get() = CoroutineSingletons.COROUTINE_SUSPENDED

internal enum class CoroutineSingletons { COROUTINE_SUSPENDED, UNDECIDED, RESUMED }

4.状态机

这里我们还是用上面所例举的例子来说明:

scss 复制代码
CoroutineScope(Dispatchers.Main).launch {
    // requestUserInfo()先挂起,返回COROUTINE_SUSPENDED,切换到子线程获取到结果
    // 再将获取到的结果传递给主线程
    val useInfo = requestUserInfo()
    tvUserName.text = useInfo
    
    // 同上requestUserInfo()的执行过程
    val homeInfo = requestHomeInfo()
    tvHomeInfo.text = homeInfo
}

这里涉及到我们requestUserInfo()函数内部withContext函数中创建的DispathcerCoroutine将会持有外部协程的Cotinuation实例,以及线程的切换流程,所以想要更清晰的看执行流程的同学可以把这段代码放到一个Activity中去,然后通过debug的方式来查看详细的执行流程。这里我们主要来介绍下有关这块代码涉及到的协程执行流程。 在Android Studio中依次打开Tools -> Kotlin -> Show Kotlin ByteCode我们就可以在右边的视图中看到其生成的字节码。

scala 复制代码
final class .../ContinuationKt$main$1 extends SuspendLambda implements Function2 { 
      public final invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;

      public final create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;

      public final invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
}

为了方便阅读这里将字节码做了一些简化。ContinuationKt$main$1熟悉字节码的同学应该不难看出这是一个内部类的命名方式,该类继承自SuspendLambda并实现了Kotlin中函数类型对应的接口Function2,也就是我们launch函数中所对应的挂起函数类型:
suspend CoroutineScope.() -> Unit的实例。 在Android Studio中我们双击shift在弹出的搜索框中,我们输入SuspendLambda,接着我们进入SuspendLambda类中我们可以看到其继承关系如下:

lua 复制代码
-- Continuation
   -- BaseContinuationImpl
      -- ContinuationImpl
         -- SuspendLambda

在BaseContinautionImpl这个抽象类中我们可以看到有一个名为invokeSuspend的抽象方法,并且我们在resumeWith方法中调用了该方法,invokeSuspend方法在协程状态流转中起着关键性的作用。下面是BaseContinautionImpl类的源码:

kotlin 复制代码
internal abstract class BaseContinuationImpl(
    // 启动该协程时创建的Cotinuation实例,对于launch函数是StandaloneCoroutine
    // 对于withContext是DisPatchedCoroutine
    public val completion: Continuation<Any?>?
) : Continuation<Any?>, CoroutineStackFrame, Serializable {
    
    public final override fun resumeWith(result: Result<Any?>) {
        var current = this
        var param = result
        while (true) {
            // 无需关注
            probeCoroutineResumed(current)
            with(current) {
                val outcome: Result<Any?> =
                    try {
                        // 状态流转的核心代码
                        val outcome = invokeSuspend(param)
                        // 协程处于挂起状态
                        if (outcome === COROUTINE_SUSPENDED) return
                        Result.success(outcome)
                    } catch (exception: Throwable) {
                        Result.failure(exception)
                    }
                releaseIntercepted() 
                if (completion is BaseContinuationImpl) {
                    current = completion
                    param = outcome
                } else {
                    // 恢复协程,传递结果值
                    completion.resumeWith(outcome)
                    return
                }
            }
        }
    }
    
protected abstract fun invokeSuspend(result: Result<Any?>): Any?

...
}

launch()函数反编译后的Java代码:

typescript 复制代码
launch$default(CoroutineScope((CoroutineContext)Dispatchers.getMain()), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
   int label;

   public final Object invokeSuspend(Object $result) {
      Object result;
      label: {
         Object state = IntrinsicsKt.getCOROUTINE_SUSPENDED();
         switch (this.label) {
            case 0:
               // 检查结果
               ResultKt.throwOnFailure($result);
               this.label = 1;
               // 挂起,执行异步操作
               if (state == requestUserInfo(this))
               return state
               break;
               
            case 1:
               // 检查结果
               ResultKt.throwOnFailure($result);
               result = $result;
               break;
               
            case 2:
               // 检查结果
               ResultKt.throwOnFailure($result);
               result = $result;
               break label;
            default:
               throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
         }

         String requestUserInfo = (String)result;
         this.label = 2;
         // 挂起,执行异步操作
         if (state == requestHomeInfo(this)) 
         return state
      }

      String requestHomeInfo = (String)result;
      return Unit.INSTANCE;
   }

   public final Continuation create(Object value, Continuation completion) {
      Intrinsics.checkNotNullParameter(completion, "completion");
      Function2 var3 = new <anonymous constructor>(completion);
      return var3;
   }

   public final Object invoke(Object var1, Object var2) {
      return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
   }
}), 3, (Object)null);

为了方便阅读,这里将反编译的代码进行了一些调整。这里你可能会感到疑惑,invokeSuspend方法是否会多次调用,答案是一定的。每当withContext函数挂起后,切换到子线程执行异步任务获取结果后,都会将结果值传递给外层的Continuation实例,获取结果后再传递到主线程,然后进行下一个状态的流转,也就是通过我们上面的lable来控制的。代码可能不太好理解,这里笔者就结合我们上面这个示例用几张流程图来说明:

类的继承关系:

  • 1、处标记的地方是上下文调度器Dispatchers.Main、Dispatchers.Io
  • 2、3、4处标记的地方是我们协程执行流程中所创建的Continuation[续体]的几种类型: 其中2处是编译器为我们创建的,3处是我们在launch、withContext函数中创建的、4处是我们在ContinuationImpl中的intercepted方法中创建的。而协程启动过程中使用create方法创建的Continuation实例和我们标记的2处是同一类型的。

执行流程示意简图:

对于上述的代码示例来说,这是一个比较完整的协程执行流程闭环图。当然这里有很多细节省略了。

  • 1、2、3、4、5、6代表着Continuaiton实例,其中1、3、5是相同的类型:
    $name extends SuspendLambda implement Function2
    为什么intercepted方法会创建一个DispachedContinuation?下面我们就根据具体的源码来分析一下:
kotlin 复制代码
public actual fun <T> Continuation<T>.intercepted(): Continuation<T> =
    (this as? ContinuationImpl)?.intercepted() ?: this
kotlin 复制代码
internal abstract class ContinuationImpl(
    completion: Continuation<Any?>?,
    private val _context: CoroutineContext?
) : BaseContinuationImpl(completion) {
    ...
    public fun intercepted(): Continuation<Any?> =
    intercepted?.(context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
    .also { intercepted = it }
   
}

这里的context[ContinuationInterceptor]中的context是一个CombinedContext,我们获得的结果是Dispacher.Main或Dispacher.IO。它们都是CoroutineDisptacher的子类,所以这里调用的是CoroutineDisptacher中覆盖的方法interceptContinuation,具体代码如下:

kotlin 复制代码
public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
    DispatchedContinuation(this, continuation)

该方法的返回值就是一个DispatchedContinuation实例。

上下文和续体流转示意简图:

总结:

首先我们先来回顾下这篇文章的主要内容:

1.通过单例类GlobleScope、顶层函数CoroutineScope、runBlocking、MainScope来创建并启动一个协程

2.挂起函数withContext、async/await、coroutineScope的运用

3.协程内部通过CPS + 状态机的方式来控制协程的执行流程

第一次写有关协程的文章。这篇文章可能也不是那么好理解,但是笔者的个人水平也有限。如有什么不对的地方,欢迎指出,大家一起学习。

参考的书籍和官网地址:

郭霖老师:第一行代码

霍丙乾老师:深入理解Kotlin协程

Kotlin设计文档:github.com/Kotlin-zh/K...

github源码地址:github.com/JetBrains/k...

相关推荐
zhougl9962 小时前
html处理Base文件流
linux·前端·html
花花鱼2 小时前
node-modules-inspector 可视化node_modules
前端·javascript·vue.js
HBR666_2 小时前
marked库(高效将 Markdown 转换为 HTML 的利器)
前端·markdown
careybobo3 小时前
海康摄像头通过Web插件进行预览播放和控制
前端
杉之5 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
喝拿铁写前端5 小时前
字段聚类,到底有什么用?——从系统混乱到结构认知的第一步
前端
再学一点就睡5 小时前
大文件上传之切片上传以及开发全流程之前端篇
前端·javascript
木木黄木木6 小时前
html5炫酷图片悬停效果实现详解
前端·html·html5
每次的天空6 小时前
Android学习总结之算法篇四(字符串)
android·学习·算法
请来次降维打击!!!7 小时前
优选算法系列(5.位运算)
java·前端·c++·算法