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