今天咱们来聊聊Kotlin
的协程Coroutine
。android
若是你尚未接触过协程,推荐你先阅读这篇入门级文章What? 你还不知道Kotlin Coroutine?git
若是你已经接触过协程,相信你都有过如下几个疑问:github
suspend
有什么做用,工做原理是怎样的?Job
、Coroutine
、Dispatcher
、CoroutineContext
与CoroutineScope
)它们之间究竟是怎么样的关系?接下来的一些文章试着来分析一下这些疑问,也欢迎你们一块儿加入来讨论。算法
这个疑问很简单,只要你不是野路子接触协程的,都应该可以知道。由于官方文档中已经明确给出了定义。编程
下面来看下官方的原话(也是这篇文章最具备底气的一段话)。设计模式
协程是一种并发设计模式,您能够在 Android 平台上使用它来简化异步执行的代码。api
敲黑板划重点:协程是一种并发的设计模式。架构
因此并非一些人所说的什么线程的另外一种表现。虽然协程的内部也使用到了线程。但它更大的做用是它的设计思想。将咱们传统的Callback
回调方式进行消除。将异步编程趋近于同步对齐。并发
解释了这么多,最后咱们仍是直接点,来看下它的优势app
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
的存在,同时协程建立的时候都会自动回调一次Continutation
的resumeWith
方法,以便让协程开始执行。
协程的上下文,它包含用户定义的一些数据集合,这些数据与协程密切相关。它相似于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]]
Job
、Dispatchers
与CoroutineName
都实现了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
对应于left
,Dispatchers.IO
对应element
。若是再拼接一层CoroutineName(aa)
就是这样的
((Job, Dispatchers.IO),CoroutineName)
功能相似与链表,但不一样的是你可以拿到上一个与你相连的总体内容。与之对应的就是minusKey
方法,从集合中移除对应Key
的CoroutineContext
实例。
有了这个基础,咱们再看它的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
方法,因此咱们再来看下Element
的get
方法
public override operator fun <E : Element> get(key: Key<E>): E? = @Suppress("UNCHECKED_CAST") if (this.key == key) this as E else null
这里使用到了Kotlin
的operator
操做符重载的特性。那么下面的代码就是等效的。
context.get(CoroutineName) context[CoroutineName]
因此咱们就能够直接经过相似于Map
的方式来获取整个协程中CoroutineContext
集合中对应Key
的CoroutineContext
实例。
本篇文章主要介绍了suspend
的工做原理与CoroutineContext
的内部结构。但愿对学习协程的伙伴们可以有所帮助,敬请期待后续的协程分析。
android_startup: 提供一种在应用启动时可以更加简单、高效的方式来初始化组件,优化启动速度。不只支持Jetpack App Startup
的所有功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。
AwesomeGithub: 基于Github
客户端,纯练习项目,支持组件化开发,支持帐户密码与认证登录。使用Kotlin
语言进行开发,项目架构是基于Jetpack&DataBinding
的MVVM
;项目中使用了Arouter
、Retrofit
、Coroutine
、Glide
、Dagger
与Hilt
等流行开源技术。
flutter_github: 基于Flutter
的跨平台版本Github
客户端,与AwesomeGithub
相对应。
android-api-analysis: 结合详细的Demo
来全面解析Android
相关的知识点, 帮助读者可以更快的掌握与理解所阐述的要点。
daily_algorithm: 每日一算法,由浅入深,欢迎加入一块儿共勉。