今天我們來聊聊Kotlin
的協程Coroutine
。
如果你還沒有接觸過協程,推薦你先閱讀這篇入門級文章What? 你還不知道Kotlin Coroutine?
如果你已經接觸過協程,相信你都有過以下幾個疑問:
- 協程到底是個什么東西?
- 協程的
suspend
有什么作用,工作原理是怎樣的? - 協程中的一些關鍵名稱(例如:
Job
、Coroutine
、Dispatcher
、CoroutineContext
與CoroutineScope
)它們之間到底是怎么樣的關系? - 協程的所謂非阻塞式掛起與恢復又是什么?
- 協程的內部實現原理是怎么樣的?
- ...
接下來的一些文章試著來分析一下這些疑問,也歡迎大家一起加入來討論。
協程是什么
這個疑問很簡單,只要你不是野路子接觸協程的,都應該能夠知道。因為官方文檔中已經明確給出了定義。
下面來看下官方的原話(也是這篇文章最具有底氣的一段話)。
協程是一種并發設計模式,您可以在 Android 平臺上使用它來簡化異步執行的代碼。
敲黑板劃重點:協程是一種并發的設計模式。
所以并不是一些人所說的什么線程的另一種表現。雖然協程的內部也使用到了線程。但它更大的作用是它的設計思想。將我們傳統的Callback
回調方式進行消除。將異步編程趨近于同步對齊。
解釋了這么多,最后我們還是直接點,來看下它的優點
- 輕量:您可以在單個線程上運行多個協程,因為協程支持掛起,不會使正在運行協程的線程阻塞。掛起比阻塞節省內存,且支持多個并行操作。
- 內存泄露更少:使用結構化并發機制在一個作用域內執行多個操作。
- 內置取消支持:取消功能會自動通過正在運行的協程層次結構傳播。
- 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
的存在,同時協程創建的時候都會自動回調一次Continutation
的resumeWith
方法,以便讓協程開始執行。
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]]
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: 每日一算法,由淺入深,歡迎加入一起共勉。