抽丝剥茧聊协程之深入理解Continuation原理

1,327 阅读5分钟

1. 前言

这是新年以来的第一篇更文,在此给大家拜个晚年,祝大家在新的一年所想的都能如愿,同时感谢大家一直以来的支持和帮助。这篇文章其实在春节前就已经构思完了,本想着在留京过年期间写完,由于计划变更,回老家过年去了,春节期间大部分时间在走亲戚,文章也就搁置下来了。

闲话少叙,本文我将尝试给大家讲明白,Kotlin协程如何实现用同步的方式实现异步调用。相信不少同学都能说出以下几种概念中的一个或者多个。

  • Kotlin suspend关键字
  • Kotlin内部的Continuation机制
  • Continuation Passing Style (CPS)机制
  • 有限状态机机制

如果你看过Kotlin协程架构师的Deep dive into Coroutines on JVM演讲视频,相信你对上面的概念不会陌生。

视频链接👉www.youtube.com/watch?v=Yrr…

但是如果你没有深入源码探究其实现原理,相信你对上面的概念也是一知半解。本文将带领大家更加详细地了解这一知识。

2. 一个循序渐进的例子

假设有这样一个简单的场景,在App上发起一个请求,10s后拿到响应,更新到用户界面上。我们可能会遇到以下几种写法。

2.1 直接在主线程调用

这种情况,大家都很清楚,对于客户端开发,在主线程执行耗时操作这是万万不可的。为了不阻塞主线程,我们需要通过开启新线程,使用回调的方式,将结果回传过来。

2.2 使用callback

这种方式,虽然解决了在主线程做耗时操作的问题,但是引入了新问题,在子线程更新UI,会导致应用崩溃。为了解决这个问题,我们需要通过主线程Handler,把callback post到主线程运行。

2.3 使用callback和handler组合

目前为止,我们通过线程,回调,Handler组合方式,完美地实现了执行一个异步请求,并将结果更新到UI上。

在这个方案中有三个要素:

1. 线程或者线程池技术

2. 回调机制,将结果反馈给调用者

3. 回调操作在哪个线程中执行


那么此处应该敲黑板划重点了

既然本文是讲协程,那么协程中哪些类分别对应这三个要素呢?

  1. Dispatchers.Main、Dispatchers.IO等对应线程

  2. Continuation对应线程中的回调

  3. DispatchedContinuation同时指定了Continuation回调,又指定了回调在哪个线程中执行。

在此先不展开讲解Continuation和DispatchedContinuation的具体细节,此处做个铺垫,后文会详细讲解。

2.4 使用协程实现

该代码是协程实现的正确代码,没有主线程执行耗时操作问题,没有子线程更新UI问题。当然也没有显式的callback,和线程切换的代码。只对suspendHeavyWork方法增加了suspend关键字,就能将异步的调用 用同步的代码实现。那么魔法在哪呢?

3. 魔法揭秘

首先贴一下完整版代码

使用Android Studio的Show Kotlin Bytecode功能查看反编译后的文件

感慨x1 看了反编译后的代码,可能需要感慨一下,哪有什么岁月静好,只不过有人在替你负重前行。用协程写代码爽,是因为编译器在后面做了不少工作,理解这背后的工作,对我们使用协程大有裨益。

困惑x1 那么编译器生成的这些代码,看起来既熟悉又陌生,说熟悉是因为就语法而言每行代码都认识(switch case都很熟悉),说陌生是因为总体而言,不太明白他们干了什么,甚至有的函数(比如invokeSuspend)连在哪调用的都不知道。

困惑x2 更让人困惑的是,状态机代码中明明出现了return语句,请问后续的代码是如何执行的?要搞清楚状态机的原理必须搞清楚这个问题。

4. 研究launch反编译

上图标出了三个重要的地方。

  1. lambda表达式被转换成了Function2实例,那么Function2是什么?为什么又给Function2传了一个类型为Continuation的null对象?

  2. invokeSuspend方法是干嘛的?注意参数是非空的。

  3. 调用heavyWork(this)传入了this对象。从前文我们可以看到,heavyWork方法反编译之后变成了heavyWork(Continuation var)。那么说明Function2是Continuation类型。

有了以上几个问题,那么离揭晓答案也就不远了。

4.1 invokeSuspend

首先回答最简单的那个问题,invokeSuspend方法是干嘛的。它定义在BaseContinuationImpl类中,是一个抽象方法。

从BaseContinuationImpl(public val completion:Continuation<Any?>?)我们可以知道协程中的回调是用链表串起来的。

假设有suspend函数调用如下,那么Continuation关系图如下。

  1. 首先一个while循环,它保证了状态机能够轮询。
  2. val completion = completion!! 如果当前Continuation 左边没有回调了,快速返回
  3. val outcome = invokeSuspend(param) 调用当前Continuation的 invokeSuspend方法
  4. 如果outcome === COROUTINE_SUSPENDED直接返回。这就是为什么delay方法不会阻塞当前线程的原因,遇到suspend方法 label会+1,当前Continuation会传递给delay。
  5. if (completion is BaseContinuationImpl)继续递归调用invokeSuspend
  6. 否则调用completion.resumeWith(outcome)并且返回

这段代码就是保证状态机能够运行的核心。

4.2 Function2是什么?

Function2是SuspendLambda,定义在ContinuationImpl.kt中。它是BaseContinuationImpl的子类。

5. DispatchedContinuation

internal class DispatchedContinuation<in T>(
    @JvmField val dispatcher: CoroutineDispatcher,
    @JvmField val continuation: Continuation<T>
)

DispatchedContinuation 有dispatcher和continuation两个成员变量。表示在dispatcher所对应的线程,执行continuation回调。在发生线程切换时,一定会生成DispatchedContinuation对象,否则,切完线程后,就无法再切回来了。

例如 delay和withContext,切换线程后都会创建DispatchedContinuation,以记录回调要在哪个线程调用。

欢迎关注"字节小站"同名公众号!! 欢迎关注"字节小站"同名公众号!! 欢迎关注"字节小站"同名公众号!!