Kotlin協(xié)程實(shí)現(xiàn)原理:CoroutineScope&Job

今天我們來聊聊Kotlin的協(xié)程Coroutine

如果你還沒有接觸過協(xié)程矮固,推薦你先閱讀這篇入門級文章What? 你還不知道Kotlin Coroutine?

如果你已經(jīng)接觸過協(xié)程棕硫,但對協(xié)程的原理存在疑惑搓萧,那么在閱讀本篇文章之前推薦你先閱讀下面的文章,這樣能讓你更全面更順暢的理解這篇文章奋单。

Kotlin協(xié)程實(shí)現(xiàn)原理:Suspend&CoroutineContext

如果你已經(jīng)接觸過協(xié)程锉试,相信你都有過以下幾個疑問:

  1. 協(xié)程到底是個什么東西?
  2. 協(xié)程的suspend有什么作用辱匿,工作原理是怎樣的键痛?
  3. 協(xié)程中的一些關(guān)鍵名稱(例如:Job炫彩、CoroutineDispatcher絮短、CoroutineContextCoroutineScope)它們之間到底是怎么樣的關(guān)系江兢?
  4. 協(xié)程的所謂非阻塞式掛起與恢復(fù)又是什么?
  5. 協(xié)程的內(nèi)部實(shí)現(xiàn)原理是怎么樣的丁频?
  6. ...

接下來的一些文章試著來分析一下這些疑問杉允,也歡迎大家一起加入來討論。

CoroutineScope

CoroutineScope是什么席里?如果你覺得陌生叔磷,那么GlobalScopelifecycleScopeviewModelScope相信就很熟悉了吧(當(dāng)然這個是針對于Android開發(fā)者)奖磁。它們都實(shí)現(xiàn)了CoroutineScope接口改基。

public interface CoroutineScope {
    /**
     * The context of this scope.
     * Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
     * Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
     *
     * By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
     */
    public val coroutineContext: CoroutineContext
}

CoroutineScope中只包含一個待實(shí)現(xiàn)的變量CoroutineContext,至于CoroutineContext之前的文章已經(jīng)分析了它的內(nèi)部結(jié)構(gòu)咖为,這里就不再累贅了秕狰。

通過它的結(jié)構(gòu),我們可以認(rèn)為它是提供CoroutineContext的容器躁染,保證CoroutineContext能在整個協(xié)程運(yùn)行中傳遞下去鸣哀,約束CoroutineContext的作用邊界。

例如吞彤,在Android中使用協(xié)程來請求數(shù)據(jù)我衬,當(dāng)接口還沒有請求完成時Activity就已經(jīng)退出了,這時如果不停止正在運(yùn)行的協(xié)程將會造成不可預(yù)期的后果饰恕。所以在Activity中我們都推薦使用lifecycleScope來啟動協(xié)程挠羔,lifecycleScope可以讓協(xié)程具有與Activity一樣的生命周期意識。

下面是lifecycleScope源碼:

val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope
    
val Lifecycle.coroutineScope: LifecycleCoroutineScope
    get() {
        while (true) {
            val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?
            if (existing != null) {
                return existing
            }
            val newScope = LifecycleCoroutineScopeImpl(
                this,
                SupervisorJob() + Dispatchers.Main.immediate
            )
            if (mInternalScopeRef.compareAndSet(null, newScope)) {
                newScope.register()
                return newScope
            }
        }
    }

它創(chuàng)建了一個LifecycleCoroutineScopeImpl實(shí)例懂盐,它實(shí)現(xiàn)了CoroutineScope接口褥赊,同時傳入SupervisorJob() + Dispatchers.Main作為它的CoroutineContext糕档。

我們再來看它的register()方法

internal class LifecycleCoroutineScopeImpl(
    override val lifecycle: Lifecycle,
    override val coroutineContext: CoroutineContext
) : LifecycleCoroutineScope(), LifecycleEventObserver {
    init {
        // in case we are initialized on a non-main thread, make a best effort check before
        // we return the scope. This is not sync but if developer is launching on a non-main
        // dispatcher, they cannot be 100% sure anyways.
        if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
            coroutineContext.cancel()
        }
    }
 
    fun register() {
        // TODO use Main.Immediate once it is graduated out of experimental.
        launch(Dispatchers.Main) {
            if (lifecycle.currentState >= Lifecycle.State.INITIALIZED) {
                lifecycle.addObserver(this@LifecycleCoroutineScopeImpl)
            } else {
                coroutineContext.cancel()
            }
        }
    }
 
    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        if (lifecycle.currentState <= Lifecycle.State.DESTROYED) {
            lifecycle.removeObserver(this)
            coroutineContext.cancel()
        }
    }

register方法中通過經(jīng)典的launch來創(chuàng)建一個協(xié)程莉恼,而launch使用到的CoroutineContext就是CoroutineSope中的CoroutineContext。然后在協(xié)程中結(jié)合JetpackLifecycle特性來監(jiān)聽Activiyt的生命周期速那。

如果對Lifecycle的使用與特性還不是很了解的俐银,推薦閱讀這篇入門級文章Android Architecture Components Part3:Lifecycle

意思就是說在Activity銷毀的時候會調(diào)用下面的方法取消協(xié)程的運(yùn)行。

coroutineContext.cancel()

這里就使用到了CoroutineContext端仰,經(jīng)過上篇文章的分析我們很容易知道CoroutineContext自身是沒有cancel方法的捶惜,所以這個cancel方法是CoroutineContext的擴(kuò)展方法。

public fun CoroutineContext.cancel(): Unit {
    this[Job]?.cancel()
}

所以真正的邏輯是從CoroutineContex集合中取出KeyJob的實(shí)例荔烧,這個對應(yīng)的就是上面創(chuàng)建LifecycleCoroutineScopeImpl實(shí)例時傳入的SupervisorJob吱七,它是CoroutineContext的其中一個子類汽久。

這時再來看lifecycleScope相關(guān)的一些方法

lifecycleScope.launchWhenCreated {  }
lifecycleScope.launchWhenStarted {  }
lifecycleScope.launchWhenResumed {  }

這些方法的內(nèi)部邏輯就很明顯了,也就是通過Lifecycle來追蹤Activity的生命周期踊餐,從而約束協(xié)程運(yùn)行的時機(jī)景醇。

我們也可以不使用lifecycleScope,自己實(shí)現(xiàn)一個CoroutineScope吝岭,讓它在Activity達(dá)到同樣的效果三痰。

 class MyActivity : AppCompatActivity(), CoroutineScope {
     lateinit var job: Job
     override val coroutineContext: CoroutineContext
         get() = Dispatchers.Main + job

     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         job = Job()
     }
 
     override fun onDestroy() {
         super.onDestroy()
         job.cancel() // Cancel job on activity destroy. After destroy all children jobs will be cancelled automatically
     }
 
     /*
      * Note how coroutine builders are scoped: if activity is destroyed or any of the launched coroutines
      * in this method throws an exception, then all nested coroutines are cancelled.
      */
     fun loadDataFromUI() = launch { // <- extension on current activity, launched in the main thread
        val ioData = async(Dispatchers.IO) { // <- extension on launch scope, launched in IO dispatcher
            // blocking I/O operation
        }
        // do something else concurrently with I/O
        val data = ioData.await() // wait for result of I/O
        draw(data) // can draw in the main thread
     }
 }

上面的實(shí)現(xiàn)也能夠保證當(dāng)前Activiyt中的協(xié)程在Activity銷毀的時候終止協(xié)程的運(yùn)行。

到這里CoroutineScope的作用就呼之欲出了窜管,它就是用來約束協(xié)程的邊界散劫,能夠很好的提供對應(yīng)的協(xié)程取消功能,保證協(xié)程的運(yùn)行范圍幕帆。

當(dāng)然這又引申出另外一個話題

Job是什么获搏?

Job

基本上每啟動一個協(xié)程就會產(chǎn)生對應(yīng)的Job,例如

lifecycleScope.launch {
}

launch返回的就是一個Job失乾,它可以用來管理協(xié)程颜凯,一個Job中可以關(guān)聯(lián)多個子Job,同時它也提供了通過外部傳入parent的實(shí)現(xiàn)

public fun Job(parent: Job? = null): Job = JobImpl(parent)

這個很好理解仗扬,當(dāng)傳入parent時症概,此時的Job將會作為parent的子Job

既然Job是來管理協(xié)程的早芭,那么它提供了六種狀態(tài)來表示協(xié)程的運(yùn)行狀態(tài)彼城。

  1. New: 創(chuàng)建
  2. Active: 運(yùn)行
  3. Completing: 已經(jīng)完成等待自身的子協(xié)程
  4. Completed: 完成
  5. Cancelling: 正在進(jìn)行取消或者失敗
  6. Cancelled: 取消或失敗

這六種狀態(tài)Job對外暴露了三種狀態(tài),它們隨時可以通過Job來獲取

public val isActive: Boolean
public val isCompleted: Boolean
public val isCancelled: Boolean

所以如果你需要自己來手動管理協(xié)程退个,可以通過下面的方式來判斷當(dāng)前協(xié)程是否在運(yùn)行募壕。

while (job.isActive) {
// 協(xié)程運(yùn)行中            
}

一般來說,協(xié)程創(chuàng)建的時候就處在Active狀態(tài)语盈,但也有特例舱馅。

例如我們通過launch啟動協(xié)程的時候可以傳遞一個start參數(shù)

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    ...
}

如果這個start傳遞的是CoroutineStart.LAZY,那么它將處于New狀態(tài)刀荒〈停可以通過調(diào)用start或者join來喚起協(xié)程進(jìn)入Active狀態(tài)。

下面我們來看一張簡圖缠借,就能很清晰的了解Job中的六個狀態(tài)間的轉(zhuǎn)化過程干毅。

                                        wait children
  +-----+ start  +--------+ complete   +-------------+  finish  +-----------+
  | New | -----> | Active | ---------> | Completing  | -------> | Completed |
  +-----+        +--------+            +-------------+          +-----------+
                   |  cancel / fail       |
                   |     +----------------+
                   |     |
                   V     V
               +------------+                           finish  +-----------+
               | Cancelling | --------------------------------> | Cancelled |
               +------------+                                   +-----------+

上面已經(jīng)提及到一個Job可以有多個子Job,所以一個Job的完成都必須等待它內(nèi)部所有的子Job完成泼返;對應(yīng)的cancel也是一樣的硝逢。

默認(rèn)情況下,如果內(nèi)部的子Job發(fā)生異常,那么它對應(yīng)的parent Job與它相關(guān)連的其它子Job都將取消運(yùn)行渠鸽。俗稱連鎖反應(yīng)叫乌。

我們也可以改變這種默認(rèn)機(jī)制,Kotlin提供了SupervisorJob來改變這種機(jī)制徽缚。這種情況還是很常見的综芥,例如用協(xié)程請求兩個接口,但并不想因?yàn)槠渲幸粋€接口失敗導(dǎo)致另外的接口也不請求猎拨,這時就可以使用SupervisorJob來改變協(xié)程的這種默認(rèn)機(jī)制膀藐。

使用很簡單,在我們創(chuàng)建CoroutineContext的時候加入SupervisorJob即可红省。例如在上面提到過的lifecycleScope额各,內(nèi)部就使用到了SupervisorJob

val newScope = LifecycleCoroutineScopeImpl(
    this,
    SupervisorJob() + Dispatchers.Main
)

你也可以嘗試運(yùn)行下面的這個例子,然后將它的SupervisorJob替換成別的CoroutineContext再來看下效果吧恃。

fun main() = runBlocking {
    val supervisor = SupervisorJob()
    with(CoroutineScope(coroutineContext + supervisor)) {
        // 啟動第一個子作業(yè)——這個示例將會忽略它的異常(不要在實(shí)踐中這么做O豪病)
        val firstChild = launch(CoroutineExceptionHandler { _, _ ->  }) {
            println("The first child is failing")
            throw AssertionError("The first child is cancelled")
        }
        // 啟動第二個子作業(yè)
        val secondChild = launch {
            firstChild.join()
            // 取消了第一個子作業(yè)且沒有傳播給第二個子作業(yè)
            println("The first child is cancelled: ${firstChild.isCancelled}, but the second one is still active")
            try {
                delay(Long.MAX_VALUE)
            } finally {
                // 但是取消了監(jiān)督的傳播
                println("The second child is cancelled because the supervisor was cancelled")
            }
        }
        // 等待直到第一個子作業(yè)失敗且執(zhí)行完成
        firstChild.join()
        println("Cancelling the supervisor")
        supervisor.cancel()
        secondChild.join()
    }
}

如果有些任務(wù)你并不想被手動取消,可以使用NonCancellable作為任務(wù)的CoroutineContext

如果需要Job獲取協(xié)程的返回結(jié)果痕寓,可以通過Deferred來實(shí)現(xiàn)傲醉,它是Job的一個子類,所以也擁有Job所用功能呻率。同時額外提供await方法來等待協(xié)程結(jié)果的返回硬毕。

Deferred可以通過CoroutineScope.async創(chuàng)建。

最后我們再來介紹下Job的幾個方法礼仗,startcancel就不多說了吐咳,分別是啟動與取消。

invokeOnCompletion

這個方法是Job的回調(diào)通知元践,當(dāng)Job執(zhí)行完后會調(diào)用這個方法

public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
 
public typealias CompletionHandler = (cause: Throwable?) -> Unit

這個cause有三種情況分別為:

  1. is null: 協(xié)程正常執(zhí)行完畢
  2. is CancellationException: 協(xié)程正常取消韭脊,并非異常導(dǎo)致的取消
  3. Otherwise: 協(xié)程發(fā)生異常

同時它的返回值DisposableHandle可以用來取消回調(diào)的監(jiān)聽。

join

public suspend fun join()

注意這是一個suspend函數(shù)单旁,所以它只能在suspend或者coroutine中進(jìn)行調(diào)用沪羔。

它的作用是暫停當(dāng)前運(yùn)行的協(xié)程任務(wù),立刻執(zhí)行自身Job的協(xié)程任務(wù)象浑,直到自身執(zhí)行完畢之后才恢復(fù)之前的協(xié)程任務(wù)繼續(xù)執(zhí)行蔫饰。

本篇文章主要介紹了CoroutineScope的作用與Job的相關(guān)狀態(tài)演化與運(yùn)用。希望對學(xué)習(xí)協(xié)程的伙伴們能夠有所幫助融柬,敬請期待后續(xù)的協(xié)程分析死嗦。

項(xiàng)目

android_startup: 提供一種在應(yīng)用啟動時能夠更加簡單柄粹、高效的方式來初始化組件俯渤,優(yōu)化啟動速度敌买。不僅支持Jetpack App Startup的全部功能态辛,還提供額外的同步與異步等待外盯、線程控制與多進(jìn)程支持等功能摘盆。

AwesomeGithub: 基于Github客戶端,純練習(xí)項(xiàng)目饱苟,支持組件化開發(fā)孩擂,支持賬戶密碼與認(rèn)證登陸。使用Kotlin語言進(jìn)行開發(fā)箱熬,項(xiàng)目架構(gòu)是基于Jetpack&DataBindingMVVM类垦;項(xiàng)目中使用了ArouterRetrofit城须、Coroutine蚤认、GlideDaggerHilt等流行開源技術(shù)糕伐。

flutter_github: 基于Flutter的跨平臺版本Github客戶端砰琢,與AwesomeGithub相對應(yīng)。

android-api-analysis: 結(jié)合詳細(xì)的Demo來全面解析Android相關(guān)的知識點(diǎn), 幫助讀者能夠更快的掌握與理解所闡述的要點(diǎn)良瞧。

daily_algorithm: 每日一算法陪汽,由淺入深,歡迎加入一起共勉褥蚯。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末挚冤,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子赞庶,更是在濱河造成了極大的恐慌你辣,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件尘执,死亡現(xiàn)場離奇詭異舍哄,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)誊锭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進(jìn)店門表悬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人丧靡,你說我怎么就攤上這事蟆沫。” “怎么了温治?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵饭庞,是天一觀的道長。 經(jīng)常有香客問我熬荆,道長舟山,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮累盗,結(jié)果婚禮上寒矿,老公的妹妹穿的比我還像新娘。我一直安慰自己若债,他們只是感情好符相,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蠢琳,像睡著了一般啊终。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上傲须,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天蓝牲,我揣著相機(jī)與錄音,去河邊找鬼躏碳。 笑死搞旭,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的菇绵。 我是一名探鬼主播肄渗,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼咬最!你這毒婦竟也來了翎嫡?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤永乌,失蹤者是張志新(化名)和其女友劉穎惑申,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體翅雏,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡圈驼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了望几。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绩脆。...
    茶點(diǎn)故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖橄抹,靈堂內(nèi)的尸體忽然破棺而出靴迫,到底是詐尸還是另有隱情,我是刑警寧澤楼誓,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布玉锌,位于F島的核電站,受9級特大地震影響疟羹,放射性物質(zhì)發(fā)生泄漏主守。R本人自食惡果不足惜禀倔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望丸逸。 院中可真熱鬧蹋艺,春花似錦剃袍、人聲如沸黄刚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽憔维。三九已至,卻和暖如春畏邢,著一層夾襖步出監(jiān)牢的瞬間业扒,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工舒萎, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留程储,地道東北人。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓臂寝,卻偏偏與公主長得像章鲤,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子咆贬,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容