乐趣区

关于android:Kotlin协程实现原理SuspendCoroutineContext

明天咱们来聊聊 Kotlin 的协程Coroutine

如果你还没有接触过协程,举荐你先浏览这篇入门级文章 What? 你还不晓得 Kotlin Coroutine?

如果你曾经接触过协程,置信你都有过以下几个疑难:

  1. 协程到底是个什么货色?
  2. 协程的 suspend 有什么作用,工作原理是怎么的?
  3. 协程中的一些要害名称 (例如:JobCoroutineDispatcherCoroutineContextCoroutineScope)它们之间到底是怎么样的关系?
  4. 协程的所谓非阻塞式挂起与复原又是什么?
  5. 协程的外部实现原理是怎么样的?

接下来的一些文章试着来剖析一下这些疑难,也欢送大家一起退出来探讨。

协程是什么

这个疑难很简略,只有你不是野路子接触协程的,都应该可能晓得。因为官网文档中曾经明确给出了定义。

上面来看下官网的原话(也是这篇文章最具备底气的一段话)。

协程是一种并发设计模式,您能够在 Android 平台上应用它来简化异步执行的代码。

敲黑板划重点:协程是一种并发的设计模式。

所以并不是一些人所说的什么线程的另一种体现。尽管协程的外部也应用到了线程。但它更大的作用是它的设计思维。将咱们传统的 Callback 回调形式进行打消。将异步编程趋近于同步对齐。

解释了这么多,最初咱们还是间接点,来看下它的长处

  1. 轻量:您能够在单个线程上运行多个协程,因为协程反对挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节俭内存,且反对多个并行操作。
  2. 内存泄露更少:应用结构化并发机制在一个作用域内执行多个操作。
  3. 内置勾销反对:勾销性能会主动通过正在运行的协程层次结构流传。
  4. Jetpack 集成:许多 Jetpack 库都蕴含提供全面协程反对的扩大。某些库还提供本人的协程作用域,可供您用于结构化并发。

suspend

suspend是协程的关键字,每一个被 suspend 润饰的办法都必须在另一个 suspend 函数或者 Coroutine 协程程序中进行调用。

第一次看到这个定义不晓得你们是否有疑难,反正小憩我是很纳闷,为什么 suspend 润饰的办法须要有这个限度呢?不加为什么就不能够,它的作用到底是什么?

当然,如果你有关注我之前的文章,应该就会有所理解,因为在重温 Retrofit 源码,笑看协程实现这篇文章中我曾经有简略的提及。

这里波及到一种机制俗称 CPS(Continuation-Passing-Style)。每一个suspend 润饰的办法或者 lambda 表达式都会在代码调用的时候为其额定增加 Continuation 类型的参数。

@GET("/v2/news")
suspend fun newsGet(@QueryMap params: Map<String, String>): NewsResponse

下面这段代码通过 CPS 转换之后真正的面目是这样的

@GET("/v2/news")
fun newsGet(@QueryMap params: Map<String, String>, c: Continuation<NewsResponse>): Any?

通过转换之后,原有的返回类型 NewsResponse 被增加到新增的 Continutation 参数中,同时返回了 Any? 类型。这里可能会有所疑难?返回类型都变了,后果不就出错了吗?

其实不是,Any?Kotlin 中比拟非凡,它能够代表任意类型。

suspend 函数被协程挂起时,它会返回一个非凡的标识 COROUTINE_SUSPENDED,而它实质就是一个Any;当协程不挂起进行执行时,它将返回执行的后果或者引发的异样。这样为了让这两种状况的返回都反对,所以应用了Kotlin 独有的 Any? 类型。

返回值搞明确了,当初来说说这个 Continutation 参数。

首先来看下 Continutation 的源码

public interface Continuation<in T> {
    /**
     * The context of the coroutine that corresponds to this continuation.
     */
    public val context: CoroutineContext

    /**
     * Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
     * return value of the last suspension point.
     */
    public fun resumeWith(result: Result<T>)
}

context是协程的上下文,它更多时候是 CombinedContext 类型,相似于协程的汇合,这个后续会详情阐明。

resumeWith是用来唤醒挂起的协程。后面曾经说过协程在执行的过程中,为了避免阻塞应用了挂起的个性,一旦协程外部的逻辑执行结束之后,就是通过该办法来唤起协程。让它在之前挂起的地位继续执行上来。

所以每一个被 suspend 润饰的函数都会获取下层的 Continutation,并将其作为参数传递给本人。既然是从下层传递过去的,那么Continutation 是由谁创立的呢?

其实也不难猜到,Continutation就是与协程创立的时候一起被创立的。

GlobalScope.launch {}

launch的时候就曾经创立了 Continutation 对象,并且启动了协程。所以在它外面进行挂起的协程传递的参数都是这个对象。

简略的了解就是协程应用 resumeWith 替换传统的 callback,每一个协程程序的创立都会随同Continutation 的存在,同时协程创立的时候都会主动回调一次 ContinutationresumeWith办法,以便让协程开始执行。

CoroutineContext

协程的上下文,它蕴含用户定义的一些数据汇合,这些数据与协程密切相关。它相似于 map 汇合,能够通过 key 来获取不同类型的数据。同时 CoroutineContext 的灵活性很强,如果其须要扭转只需应用以后的 CoroutineContext 来创立一个新的 CoroutineContext 即可。

来看下 CoroutineContext 的定义

public interface CoroutineContext {
    /**
     * Returns the element with the given [key] from this context or `null`.
     */
    public operator fun <E : Element> get(key: Key<E>): E?
 
    /**
     * Accumulates entries of this context starting with [initial] value and applying [operation]
     * from left to right to current accumulator value and each element of this context.
     */
    public fun <R> fold(initial: R, operation: (R, Element) -> R): R
 
    /**
     * Returns a context containing elements from this context and elements from  other [context].
     * The elements from this context with the same key as in the other one are dropped.
     */
    public operator fun plus(context: CoroutineContext): CoroutineContext = ...
 
    /**
     * Returns a context containing elements from this context, but without an element with
     * the specified [key].
     */
    public fun minusKey(key: Key<*>): CoroutineContext
 
    /**
     * Key for the elements of [CoroutineContext]. [E] is a type of element with this key.
     */
    public interface Key<E : Element>

    /**
     * An element of the [CoroutineContext]. An element of the coroutine context is a singleton context by itself.
     */
    public interface Element : CoroutineContext {..}
}

每一个 CoroutineContext 都有它惟一的一个 Key 其中的类型是 Element,咱们能够通过对应的Key 来获取对应的具体对象。说的有点形象咱们间接通过例子来理解。

var context = Job() + Dispatchers.IO + CoroutineName("aa")
LogUtils.d("$context, ${context[CoroutineName]}")
context = context.minusKey(Job)
LogUtils.d("$context")
// 输入
[JobImpl{Active}@158b42c, CoroutineName(aa), LimitingDispatcher@aeb0f27[dispatcher = DefaultDispatcher]], CoroutineName(aa)
[CoroutineName(aa), LimitingDispatcher@aeb0f27[dispatcher = DefaultDispatcher]]

JobDispatchersCoroutineName 都实现了 Element 接口。

如果须要联合不同的 CoroutineContext 能够间接通过 + 拼接,实质就是应用了 plus 办法。

    public operator fun plus(context: CoroutineContext): CoroutineContext =
        if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
            context.fold(this) { acc, element ->
                val removed = acc.minusKey(element.key)
                if (removed === EmptyCoroutineContext) element else {// make sure interceptor is always last in the context (and thus is fast to get when present)
                    val interceptor = removed[ContinuationInterceptor]
                    if (interceptor == null) CombinedContext(removed, element) else {val left = removed.minusKey(ContinuationInterceptor)
                        if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
                            CombinedContext(CombinedContext(left, element), interceptor)
                    }
                }
            }

plus的实现逻辑是将两个拼接的 CoroutineContext 封装到 CombinedContext 中组成一个拼接链,同时每次都将 ContinuationInterceptor 增加到拼接链的最尾部.

那么 CombinedContext 又是什么呢?

internal class CombinedContext(
    private val left: CoroutineContext,
    private val element: Element
) : CoroutineContext, Serializable {override fun <E : Element> get(key: Key<E>): E? {
        var cur = this
        while (true) {cur.element[key]?.let {return it}
            val next = cur.left
            if (next is CombinedContext) {cur = next} else {return next[key]
            }
        }
    }
    ...
}

留神看它的两个参数,咱们间接拿下面的例子来剖析

Job() + Dispatchers.IO
(Job, Dispatchers.IO)

Job对应于 leftDispatchers.IO 对应 element。如果再拼接一层CoroutineName(aa) 就是这样的

((Job, Dispatchers.IO),CoroutineName)

性能相似与链表,但不同的是你可能拿到上一个与你相连的 整体 内容。与之对应的就是 minusKey 办法,从汇合中移除对应 KeyCoroutineContext实例。

有了这个根底,咱们再看它的 get 办法就很清晰了。先从 element 中去取,没有再从之前的 left 中取。

那么这个 Key 到底是什么呢?咱们来看下CoroutineName

public data class CoroutineName(
    /**
     * User-defined coroutine name.
     */
    val name: String
) : AbstractCoroutineContextElement(CoroutineName) {
    /**
     * Key for [CoroutineName] instance in the coroutine context.
     */
    public companion object Key : CoroutineContext.Key<CoroutineName>
 
    /**
     * Returns a string representation of the object.
     */
    override fun toString(): String = "CoroutineName($name)"
}

很简略它的 Key 就是 CoroutineContext.Key<CoroutineName>,当然这样还不够,须要持续联合对于的operator get 办法,所以咱们再来看下 Elementget办法

public override operator fun <E : Element> get(key: Key<E>): E? =
    @Suppress("UNCHECKED_CAST")
    if (this.key == key) this as E else null

这里应用到了 Kotlinoperator操作符重载的个性。那么上面的代码就是等效的。

context.get(CoroutineName)
context[CoroutineName]

所以咱们就能够间接通过相似于 Map 的形式来获取整个协程中 CoroutineContext 汇合中对应 KeyCoroutineContext实例。

本篇文章次要介绍了 suspend 的工作原理与 CoroutineContext 的内部结构。心愿对学习协程的搭档们可能有所帮忙,敬请期待后续的协程剖析。

我的项目

android_startup: 提供一种在利用启动时可能更加简略、高效的形式来初始化组件,优化启动速度。不仅反对 Jetpack App Startup 的全副性能,还提供额定的同步与异步期待、线程管制与多过程反对等性能。

AwesomeGithub: 基于 Github 客户端,纯练习我的项目,反对组件化开发,反对账户明码与认证登陆。应用 Kotlin 语言进行开发,我的项目架构是基于 Jetpack&DataBindingMVVM;我的项目中应用了 ArouterRetrofitCoroutineGlideDaggerHilt等风行开源技术。

flutter_github: 基于 Flutter 的跨平台版本 Github 客户端,与 AwesomeGithub 绝对应。

android-api-analysis: 联合具体的 Demo 来全面解析 Android 相干的知识点, 帮忙读者可能更快的把握与了解所论述的要点。

daily_algorithm: 每日一算法,由浅入深,欢送退出一起共勉。

退出移动版