今天咱們來聊聊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: 每日一算法,由淺入深,歡迎加入一塊兒共勉。