今天我們來聊聊Kotlin
的協(xié)程Coroutine
。
如果你還沒有接觸過協(xié)程舱污,推薦你先閱讀這篇入門級文章What? 你還不知道Kotlin Coroutine?
如果你已經(jīng)接觸過協(xié)程呀舔,相信你都有過以下幾個疑問:
- 協(xié)程到底是個什么東西?
- 協(xié)程的
suspend
有什么作用扩灯,工作原理是怎樣的媚赖? - 協(xié)程中的一些關(guān)鍵名稱(例如:
Job
、Coroutine
珠插、Dispatcher
惧磺、CoroutineContext
與CoroutineScope
)它們之間到底是怎么樣的關(guān)系? - 協(xié)程的所謂非阻塞式掛起與恢復(fù)又是什么丧失?
- 協(xié)程的內(nèi)部實現(xiàn)原理是怎么樣的豺妓?
- ...
接下來的一些文章試著來分析一下這些疑問惜互,也歡迎大家一起加入來討論布讹。
協(xié)程是什么
這個疑問很簡單琳拭,只要你不是野路子接觸協(xié)程的,都應(yīng)該能夠知道描验。因為官方文檔中已經(jīng)明確給出了定義白嘁。
下面來看下官方的原話(也是這篇文章最具有底氣的一段話)。
協(xié)程是一種并發(fā)設(shè)計模式膘流,您可以在 Android 平臺上使用它來簡化異步執(zhí)行的代碼絮缅。
敲黑板劃重點:協(xié)程是一種并發(fā)的設(shè)計模式。
所以并不是一些人所說的什么線程的另一種表現(xiàn)呼股。雖然協(xié)程的內(nèi)部也使用到了線程耕魄。但它更大的作用是它的設(shè)計思想。將我們傳統(tǒng)的Callback
回調(diào)方式進行消除彭谁。將異步編程趨近于同步對齊吸奴。
解釋了這么多,最后我們還是直接點缠局,來看下它的優(yōu)點
- 輕量:您可以在單個線程上運行多個協(xié)程则奥,因為協(xié)程支持掛起,不會使正在運行協(xié)程的線程阻塞狭园。掛起比阻塞節(jié)省內(nèi)存读处,且支持多個并行操作。
- 內(nèi)存泄露更少:使用結(jié)構(gòu)化并發(fā)機制在一個作用域內(nèi)執(zhí)行多個操作唱矛。
- 內(nèi)置取消支持:取消功能會自動通過正在運行的協(xié)程層次結(jié)構(gòu)傳播罚舱。
- Jetpack集成:許多 Jetpack 庫都包含提供全面協(xié)程支持的擴展。某些庫還提供自己的協(xié)程作用域绎谦,可供您用于結(jié)構(gòu)化并發(fā)管闷。
suspend
suspend
是協(xié)程的關(guān)鍵字,每一個被suspend
修飾的方法都必須在另一個suspend
函數(shù)或者Coroutine
協(xié)程程序中進行調(diào)用燥滑。
第一次看到這個定義不知道你們是否有疑問渐北,反正小憩我是很疑惑,為什么suspend
修飾的方法需要有這個限制呢铭拧?不加為什么就不可以赃蛛,它的作用到底是什么?
當(dāng)然搀菩,如果你有關(guān)注我之前的文章呕臂,應(yīng)該就會有所了解,因為在重溫Retrofit源碼肪跋,笑看協(xié)程實現(xiàn)這篇文章中我已經(jīng)有簡單的提及歧蒋。
這里涉及到一種機制俗稱CPS(Continuation-Passing-Style)
。每一個suspend
修飾的方法或者lambda
表達式都會在代碼調(diào)用的時候為其額外添加Continuation
類型的參數(shù)。
@GET("/v2/news")
suspend fun newsGet(@QueryMap params: Map<String, String>): NewsResponse
上面這段代碼經(jīng)過CPS
轉(zhuǎn)換之后真正的面目是這樣的
@GET("/v2/news")
fun newsGet(@QueryMap params: Map<String, String>, c: Continuation<NewsResponse>): Any?
經(jīng)過轉(zhuǎn)換之后谜洽,原有的返回類型NewsResponse
被添加到新增的Continutation
參數(shù)中萝映,同時返回了Any?
類型。這里可能會有所疑問阐虚?返回類型都變了序臂,結(jié)果不就出錯了嗎?
其實不是实束,Any?
在Kotlin
中比較特殊奥秆,它可以代表任意類型。
當(dāng)suspend
函數(shù)被協(xié)程掛起時咸灿,它會返回一個特殊的標識COROUTINE_SUSPENDED
构订,而它本質(zhì)就是一個Any
;當(dāng)協(xié)程不掛起進行執(zhí)行時避矢,它將返回執(zhí)行的結(jié)果或者引發(fā)的異常悼瘾。這樣為了讓這兩種情況的返回都支持,所以使用了Kotlin
獨有的Any?
類型谷异。
返回值搞明白了分尸,現(xiàn)在來說說這個Continutation
參數(shù)。
首先來看下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
是協(xié)程的上下文歹嘹,它更多時候是CombinedContext
類型箩绍,類似于協(xié)程的集合,這個后續(xù)會詳情說明尺上。
resumeWith
是用來喚醒掛起的協(xié)程材蛛。前面已經(jīng)說過協(xié)程在執(zhí)行的過程中,為了防止阻塞使用了掛起的特性怎抛,一旦協(xié)程內(nèi)部的邏輯執(zhí)行完畢之后卑吭,就是通過該方法來喚起協(xié)程。讓它在之前掛起的位置繼續(xù)執(zhí)行下去马绝。
所以每一個被suspend
修飾的函數(shù)都會獲取上層的Continutation
豆赏,并將其作為參數(shù)傳遞給自己。既然是從上層傳遞過來的富稻,那么Continutation
是由誰創(chuàng)建的呢掷邦?
其實也不難猜到,Continutation
就是與協(xié)程創(chuàng)建的時候一起被創(chuàng)建的椭赋。
GlobalScope.launch {
}
launch
的時候就已經(jīng)創(chuàng)建了Continutation
對象抚岗,并且啟動了協(xié)程。所以在它里面進行掛起的協(xié)程傳遞的參數(shù)都是這個對象哪怔。
簡單的理解就是協(xié)程使用resumeWith
替換傳統(tǒng)的callback
宣蔚,每一個協(xié)程程序的創(chuàng)建都會伴隨Continutation
的存在向抢,同時協(xié)程創(chuàng)建的時候都會自動回調(diào)一次Continutation
的resumeWith
方法,以便讓協(xié)程開始執(zhí)行胚委。
CoroutineContext
協(xié)程的上下文挟鸠,它包含用戶定義的一些數(shù)據(jù)集合,這些數(shù)據(jù)與協(xié)程密切相關(guān)篷扩。它類似于map
集合兄猩,可以通過key
來獲取不同類型的數(shù)據(jù)茉盏。同時CoroutineContext
的靈活性很強鉴未,如果其需要改變只需使用當(dāng)前的CoroutineContext
來創(chuàng)建一個新的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
鸠姨,我們可以通過對應(yīng)的Key
來獲取對應(yīng)的具體對象铜秆。說的有點抽象我們直接通過例子來了解。
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
都實現(xiàn)了Element
接口连茧。
如果需要結(jié)合不同的CoroutineContext
可以直接通過+
拼接,本質(zhì)就是使用了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
的實現(xiàn)邏輯是將兩個拼接的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]
}
}
}
...
}
注意看它的兩個參數(shù)祟峦,我們直接拿上面的例子來分析
Job() + Dispatchers.IO
(Job, Dispatchers.IO)
Job
對應(yīng)于left
罚斗,Dispatchers.IO
對應(yīng)element
。如果再拼接一層CoroutineName(aa)
就是這樣的
((Job, Dispatchers.IO),CoroutineName)
功能類似與鏈表宅楞,但不同的是你能夠拿到上一個與你相連的整體內(nèi)容针姿。與之對應(yīng)的就是minusKey
方法,從集合中移除對應(yīng)Key
的CoroutineContext
實例厌衙。
有了這個基礎(chǔ)距淫,我們再看它的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>
喻杈,當(dāng)然這樣還不夠彤枢,需要繼續(xù)結(jié)合對于的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
的方式來獲取整個協(xié)程中CoroutineContext
集合中對應(yīng)Key
的CoroutineContext
實例。
本篇文章主要介紹了suspend
的工作原理與CoroutineContext
的內(nèi)部結(jié)構(gòu)龄砰。希望對學(xué)習(xí)協(xié)程的伙伴們能夠有所幫助盟猖,敬請期待后續(xù)的協(xié)程分析讨衣。
項目
android_startup: 提供一種在應(yīng)用啟動時能夠更加簡單、高效的方式來初始化組件式镐,優(yōu)化啟動速度反镇。不僅支持Jetpack App Startup
的全部功能,還提供額外的同步與異步等待娘汞、線程控制與多進程支持等功能歹茶。
AwesomeGithub: 基于Github
客戶端,純練習(xí)項目你弦,支持組件化開發(fā)惊豺,支持賬戶密碼與認證登陸。使用Kotlin
語言進行開發(fā)禽作,項目架構(gòu)是基于Jetpack&DataBinding
的MVVM
尸昧;項目中使用了Arouter
、Retrofit
旷偿、Coroutine
烹俗、Glide
、Dagger
與Hilt
等流行開源技術(shù)萍程。
flutter_github: 基于Flutter
的跨平臺版本Github
客戶端幢妄,與AwesomeGithub
相對應(yīng)。
android-api-analysis: 結(jié)合詳細的Demo
來全面解析Android
相關(guān)的知識點, 幫助讀者能夠更快的掌握與理解所闡述的要點茫负。
daily_algorithm: 每日一算法蕉鸳,由淺入深,歡迎加入一起共勉朽褪。