Kotlin協(xié)程的理解

關于Kotlin協(xié)程的文章特別多色难,多數(shù)是按照官方教程翻譯一遍沿癞,很多概念理解起來比較困惑,特別是協(xié)程的異常處理部分吮铭,看的是一頭霧水时迫。所以打算跟著官方文檔及優(yōu)秀的Kotlin協(xié)程文章,來系統(tǒng)學習一下谓晌。

首先來看Android官方對協(xié)程的定義:協(xié)程是一種并發(fā)設計模式掠拳,您可以在 Android 平臺上使用它來簡化異步執(zhí)行的代碼。協(xié)程是在版本 1.3 中添加到 Kotlin 的纸肉,它基于來自其他語言的既定概念溺欧。

特點

協(xié)程是我們在 Android 上進行異步編程的推薦解決方案。值得關注的特點包括:

  • 輕量:您可以在單個線程上運行多個協(xié)程柏肪,因為協(xié)程支持掛起姐刁,不會使正在運行協(xié)程的線程阻塞。掛起比阻塞節(jié)省內(nèi)存烦味,且支持多個并行操作聂使。
  • 內(nèi)存泄漏更少:使用結(jié)構(gòu)化并發(fā)機制在一個作用域內(nèi)執(zhí)行多項操作。
  • 內(nèi)置取消支持:取消操作會自動在運行中的整個協(xié)程層次結(jié)構(gòu)內(nèi)傳播。
  • Jetpack 集成:許多 Jetpack 庫都包含提供全面協(xié)程支持的擴展柏靶。某些庫還提供自己的協(xié)程作用域弃理,可供您用于結(jié)構(gòu)化并發(fā)。

上面是關于協(xié)程的概念和特點屎蜓,概念很簡單痘昌,理解起來卻有些生澀,那我們我們帶著兩個簡單的問題開始學習

協(xié)程是什么

協(xié)程并不是Kotlin創(chuàng)造的概念炬转, 在其他語言層面看到協(xié)程的實現(xiàn)控汉,協(xié)程是一種編程思想,并不局限于任何語言返吻。

協(xié)程最核心的作用就是用來簡化異步執(zhí)行的代碼,說的直白一點乎婿,協(xié)程將原本復雜的異步線程做了簡化處理测僵,邏輯更清晰,代碼更簡潔

這里谢翎,就必須拿出線程和協(xié)程一起對比捍靠,站在Android開發(fā)者的角度,去理解它們直接的關系:

  • 我們的代碼是在線程中運行的森逮,而線程是在進程中運行
  • 協(xié)程不是線程榨婆,它也是在線程中運行的,不論是單線程還是多線程
  • 單線程中褒侧,使用協(xié)程并不能減少線程的執(zhí)行時間

那么協(xié)程到底是怎么來簡化異步代碼的呢良风?下面從協(xié)程最經(jīng)典的使用場景來切入---線程控制

callback

在Android中,如果要處理異步任務闷供,最常見的就是使用callback

    public interface Callback<T> {

        void onSucceed(T result);

        void onFailed(int errCode, String errMsg);
    }

callback的特點很明顯

  • 優(yōu)勢:使用簡單
  • 缺點:如果業(yè)務多烟央,很容易陷入回調(diào)地獄,嵌套邏輯復雜歪脏,維護成很高

RxJava

那么有什么方法能夠解決呢疑俭?這時候很自然想到大名鼎鼎的RxJava

  • 優(yōu)勢:RxJava使用鏈式調(diào)用,實現(xiàn)線程切換婿失,消除回調(diào)

  • 劣勢:RxJava上手難度較大钞艇,而且各種操作符,很容易濫用豪硅,復雜度較高

而協(xié)程作為Kotlin自身的拓展庫哩照,使用更簡單,更方便

下面使用協(xié)程來進行網(wǎng)絡請求

launch {
      val result = get("https://developer.android.com")
      print(result)
       }                                      
}

suspend fun get(url: String) = withContext(Dispatchers.IO) {
            //network request
          }   

這里展示了代碼片段舟误, launch并不是頂層函數(shù)葡秒,我們先不關注,只關注{}內(nèi)的具體邏輯

通常做網(wǎng)絡請求,都是使用callback眯牧,回調(diào)結(jié)果后處理抑钟,而上面的兩行代碼革娄,分別執(zhí)行在兩個線程里,但是看起來和單線程一樣。

這里的get("https://developer.android.com")就是一個掛起函數(shù)项鬼,能保證請求結(jié)束后,才開始打印結(jié)果户誓,這就是協(xié)程中最核心的非阻塞式掛起

協(xié)程怎么用

那么協(xié)程中的掛起尚洽,到底掛起了什么呢?我們先來看看協(xié)程怎么用绒疗,跟著用法來分析

協(xié)程基礎知識

上面提到侵歇,launch不是頂層函數(shù),那么真正創(chuàng)建協(xié)程的方式是什么呢吓蘑?

// 方法一惕虑,使用 runBlocking 頂層函數(shù)
runBlocking {
    get(url)
}

// 方法二,自行通過 CoroutineContext 創(chuàng)建一個 CoroutineScope 對象磨镶,通過launch開啟協(xié)程
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
    get(url)
}

// 方法三溃蔫,使用 GlobalScope 單例對象,GlobalScope 實際是CoroutineScope的子類,本質(zhì)是CoroutineScope
GlobalScope.launch {
    get(url)
}

//方法四,使用async開啟協(xié)程
GlobalScope.async {
      get(url)
}

  • 方法一通常適用于單元測試的場景琳猫,而業(yè)務開發(fā)中不會用到這種方法伟叛,因為它是線程阻塞的。
  • 方法二是標準用法脐嫂,我們可以通過 context 參數(shù)去管理和控制協(xié)程的生命周期(這里的 context 和 Android 里的不是一個東西统刮,是一個更通用的概念,會有一個 Android 平臺的封裝來配合使用)账千,CoroutineScope來創(chuàng)建協(xié)程的作用域
  • 方法三GlobalScopeCoroutineScope的子類网沾,使用場景先不考究。
  • 方法四和方法三的區(qū)別蕊爵,就在于launchasync的區(qū)別辉哥,這個稍后再分析

CoroutineScope

CoroutineScope是協(xié)程的作用域,所有協(xié)程都需要在作用域中啟動

CoroutineContext

協(xié)程的持久上下文攒射, 定義協(xié)程以下的行為:

下面就是一個標準的協(xié)程

val ctxHandler = CoroutineExceptionHandler {context , exception ->
}
val context = Job() + Dispatchers.IO + EmptyCoroutineContext + ctxHandler
CoroutineScope(context).launch {
  get(url)
}

suspend fun get(url: String) {

}

這個 launch 函數(shù)咧最,它具體的含義是:我要創(chuàng)建一個新的協(xié)程捂人,并在指定的線程上運行它御雕。這個被創(chuàng)建、被運行的所謂「協(xié)程」是誰滥搭?就是你傳給 launch 的那些代碼酸纲,這一段連續(xù)代碼叫做一個協(xié)程

我們也能換個思路理解,協(xié)程的概念由三方面組成: CoroutineScope + CoroutineContext+ 協(xié)程

協(xié)程是抽象的概念瑟匆, 而協(xié)程launch 或者 async 函數(shù)閉包的代碼塊闽坡,是并發(fā)的具體實現(xiàn),我們提到的協(xié)程就是它

使用協(xié)程

協(xié)程最常用的功能是并發(fā)愁溜,而并發(fā)的典型場景就是多線程疾嗅。可以使用 Dispatchers.IO 參數(shù)把任務切到 IO 線程執(zhí)行:

coroutineScope.launch(Dispatchers.IO) {
    ...
}

使用Dispatchers.Main 切換到主線程

coroutineScope.launch(Dispatchers.Main) {
    ...
}

什么時候使用協(xié)程呢冕象?當你需要切線程或者指定線程的時候代承。你要在后臺執(zhí)行任務?切渐扮!

coroutineScope.launch(Dispatchers.IO) {
   val result = get(url)
}

然后需要在前臺更新界面次泽?再切!

coroutineScope.launch(Dispatchers.IO) {
    val result = get(url)
    launch(Dispatchers.Main) {
        showToast(result)
    }
}

乍一看席爽,還是有嵌套啊

如果只是使用 launch 函數(shù),協(xié)程并不能比線程做更多的事啊片。不過協(xié)程中卻有一個很實用的函數(shù):withContext 只锻。這個函數(shù)可以切換到指定的線程,并在閉包內(nèi)的邏輯執(zhí)行結(jié)束之后紫谷,自動把線程切回去繼續(xù)執(zhí)行齐饮。

coroutineScope.launch(Dispatchers.Main) {  //主線程啟動    val result = withContext(Dispatchers.IO) { //切換到IO線程,執(zhí)行完畢自動切回主線程          get(url)  //在IO線程執(zhí)行    }        showToast(result) //恢復到主線程}

這種寫法看上去好像和剛才那種區(qū)別不大笤昨,但如果你需要頻繁地進行線程切換祖驱,這種寫法的優(yōu)勢就會體現(xiàn)出來÷髦希可以參考下面的對比:

// 第一種寫法coroutineScope.launch(Dispatchers.IO) {    ...    launch(Dispatchers.Main){        ...        launch(Dispatchers.IO) {            ...            launch(Dispatchers.Main) {                ...            }        }    }}// 通過第二種寫法來實現(xiàn)相同的邏輯coroutineScope.launch(Dispatchers.Main) {    ...    withContext(Dispatchers.IO) {        ...    }    ...    withContext(Dispatchers.IO) {        ...    }    ...}

根據(jù)withContext自動切回的特性捺僻,可以將withContext 抽取到一個單獨的函數(shù)

coroutineScope.launch(Dispatchers.Main) {  //主線程啟動    val result = get(url)  //在IO線程執(zhí)行      showToast(result) //恢復到主線程}fun get(url: String) = withContext(Dispatchers.IO) {        // to do network request        url    }

這樣代碼邏輯就清晰多了

與基于回調(diào)的等效實現(xiàn)相比,withContext() 不會增加額外的開銷崇裁。此外匕坯,在某些情況下,還可以優(yōu)化 withContext() 調(diào)用拔稳,比使用回調(diào)表現(xiàn)更好葛峻。例如,如果某個函數(shù)對一個網(wǎng)絡調(diào)用十次巴比,您可以使用外部 withContext() 讓 Kotlin 只切換一次線程术奖。這樣礁遵,即使網(wǎng)絡庫多次使用 withContext(),它也會留在同一調(diào)度程序上采记,并避免切換線程佣耐。

細心的你會發(fā)現(xiàn),我們上面的示例挺庞,都缺少一個關鍵字suspend, 真正執(zhí)行時晰赞,會報錯:

fun get(url: String) = withContext(Dispatchers.IO) {    // IDE 報錯 Suspend function'withContext' should be called only from a coroutine or another suspend funcion}

意思是說,withContext 是一個 suspend 函數(shù)选侨,調(diào)用 suspend 函數(shù)掖鱼,只能從其他 suspend 函數(shù)進行調(diào)用,或通過使用協(xié)程構(gòu)建器(例如 launch)來啟動新的協(xié)程

suspend

suspend 是 Kotlin 協(xié)程最核心的關鍵字援制,代碼執(zhí)行到 suspend 函數(shù)的時候會掛起戏挡,并且這個掛起是非阻塞式的,它不會阻塞你當前的線程晨仑。

所以上面代碼: 加上suspend就能通過編譯:

suspend fun get(url: String) = withContext(Dispatchers.IO) {    ...}

suspend 具體是什么褐墅?它又是如何實現(xiàn)非阻塞式掛起的呢?

協(xié)程的掛起

協(xié)程到底掛起的是什么呢洪己?是如何將線程掛起嗎妥凳?

實際上掛起的就是協(xié)程本身,具體一點呢答捕?

前面講過逝钥,協(xié)程其實就是 launch 或者 async 函數(shù)中閉包的代碼塊。

當協(xié)程執(zhí)行到suspend函數(shù)時拱镐,協(xié)程會被suspend艘款,也就是被掛起。

那協(xié)程從哪里掛起呢沃琅?當前的線程

掛起后做了什么呢哗咆?離開當前運行的線程,在指定的線程開始執(zhí)行益眉,執(zhí)行完畢后再恢復協(xié)程晌柬。

協(xié)程并不是停下來了,是脫離當前線程郭脂,兵分兩路空繁,互不干擾,那么脫離后各自做了什么呢朱庆?

  • 線程

    當線程中代碼執(zhí)行到協(xié)程的suspend函數(shù)盛泡,暫時不執(zhí)行協(xié)程剩余代碼,跳出協(xié)程代碼塊娱颊,繼續(xù)運行

    • 如果線程是后臺線程:
    *   如果有其他后臺任務傲诵,則執(zhí)行
    *   如果沒有其他任務,則無事可做拴竹,等待被回收
    
    • 如果是主線程:
    則繼續(xù)執(zhí)行工作悟衩,刷新界面
    
  • 協(xié)程

    線程的代碼在到達 suspend 函數(shù)的時候被掐斷,接下來協(xié)程會從這個 suspend 函數(shù)開始繼續(xù)往下執(zhí)行栓拜,不過是在指定的線程座泳。

    誰指定的?是 suspend 函數(shù)指定的幕与,比如函數(shù)內(nèi)部的 withContext 傳入的 Dispatchers.IO 所指定的 IO 線程挑势。

    Dispatchers 調(diào)度器,它可以將協(xié)程限制在一個特定的線程執(zhí)行啦鸣,或者將它分派到一個線程池潮饱,或者讓它不受限制地運行,關于 Dispatchers后續(xù)再詳細講解

    suspend 函數(shù)執(zhí)行完成之后诫给,協(xié)程為我們做的最爽的事就來了:會自動幫我們把線程再切回來香拉。

    我們的協(xié)程原本是運行在主線程的,當代碼遇到 suspend 函數(shù)的時候中狂,發(fā)生線程切換凫碌,根據(jù) Dispatchers 切換到了對應線程執(zhí)行;

    當這個函數(shù)執(zhí)行完畢后胃榕,線程又切了回來盛险,也就是協(xié)程會幫我再 post 一個 Runnable,讓我剩下的代碼繼續(xù)回到主線程去執(zhí)行勤晚。

協(xié)程掛起的實質(zhì):就是切個線程

不過協(xié)程的掛起,比起我們使用Handler或者Rxjava的區(qū)別在于泉褐, 掛起函數(shù)執(zhí)行完畢后赐写,協(xié)程會自動切回原來的線程。

這個切回來的動作膜赃,也就是協(xié)程中的恢復resume, 必須在協(xié)程中挺邀,才能實現(xiàn)恢復功能

這也說明為什么掛起函數(shù)需要在協(xié)程或者另一個掛起函數(shù)中調(diào)用,最終都是為了讓suspend 函數(shù)切換線程之后能夠再切回來

協(xié)程怎么掛起

suspend 函數(shù)是怎么被掛起的呢跳座? 是 suspend 指令做到的嗎端铛?下面寫個 suspend 函數(shù)嘗試一下:

suspend fun printThreadInfo() {    print(Thread.currentThread().name)}I/System.out:main

顯示在主線程, 有點奇怪疲眷,明明定義了 suspend 函數(shù)禾蚕,為什么協(xié)程沒有掛起呢?

對比之前的例子:

suspend fun get(url: String) = withContext(Dispatchers.IO) {    ...}

發(fā)現(xiàn)區(qū)別在于withContext函數(shù)狂丝。查看 withContext 源碼可以發(fā)現(xiàn)换淆,它本身就是suspend函數(shù)哗总,它接收一個 Dispatcher 參數(shù),依賴這個 Dispatcher 參數(shù)的指示倍试,你的協(xié)程被掛起讯屈,然后切到別的線程。

所以 suspend并不能掛起協(xié)程县习,真正掛起協(xié)程的涮母,是協(xié)程框架,要想掛起協(xié)程躁愿,必須要直接或間接使用協(xié)程框架的 suspend函數(shù)

suspend的作用

suspend 關鍵字叛本,不是真正實現(xiàn)掛起,那它的作用是什么攘已?

它其實是一個提醒炮赦。

對函數(shù)的使用者的提醒:我是一個耗時函數(shù),我被我的創(chuàng)建者用掛起的方式放在后臺運行样勃,所以請在協(xié)程里調(diào)用我吠勘。

為什么 suspend 關鍵字并沒有實際去操作掛起,但 Kotlin 卻把它提供出來峡眶?

因為它本來就不是用來操作掛起的剧防。

掛起的操作 —— 也就是切線程,依賴的是掛起函數(shù)里面的實際代碼辫樱,而不是這個關鍵字峭拘。

所以這個關鍵字,只是一個提醒狮暑。

并且鸡挠, 定義了suspend函數(shù),但不包含掛起邏輯時搬男,會提醒:redundant suspend modifier拣展,告訴你這個 suspend 是多余的、

所以缔逛,創(chuàng)建一個 suspend 函數(shù)备埃,為了讓它包含真正掛起的邏輯,要在它內(nèi)部直接或間接調(diào)用 Kotlin 自帶的 suspend 函數(shù)褐奴,你的這個 suspend 才是有意義的按脚。

自定義 suspend 函數(shù)的使用原則: 某個函數(shù)只要是耗時的,就可以寫成suspend 函數(shù)

學習了協(xié)程的掛起敦冬,還有一個概念有疑惑辅搬,那就是協(xié)程的非阻塞式掛起,其中非阻塞式到底是什么

非阻塞式掛起

非阻塞式是相對阻塞式來說的

阻塞式很容易理解脖旱,一條馬路堵車了伞辛,前面車輛不開動烂翰,后面車輛全部被阻塞,后面車想開過去蚤氏,要么等前車離開甘耿,要么開辟一條路,從新路開走

這和代碼中線程很相似:

道路被阻塞—耗時任務 等前車離開—耗時任務結(jié)束 開新的道路—切換到其他線程

從語義上理解非阻塞式掛起竿滨,講的是非阻塞式是掛起的一個特點佳恬,協(xié)程的掛起是非阻塞式的,沒有表達其他概念

阻塞的本質(zhì)

首先于游,所有的代碼本質(zhì)上都是阻塞式的毁葱,而只有比較耗時的代碼才會導致人類可感知的等待,比如在主線程上做一個耗時 50 ms 的操作會導致界面卡掉幾幀贰剥,這種是我們?nèi)搜勰苡^察出來的倾剿,而這就是我們通常意義所說的「阻塞」。

舉個例子蚌成,當你開發(fā)的 app 在性能好的手機上很流暢前痘,在性能差的老手機上會卡頓,就是在說同一行代碼執(zhí)行的時間不一樣担忧。

視頻中講了一個網(wǎng)絡 IO 的例子芹缔,IO 阻塞更多是反映在「等」這件事情上,它的性能瓶頸是和網(wǎng)絡的數(shù)據(jù)交換瓶盛,你切多少個線程都沒用最欠,該花的時間一點都少不了。

而這跟協(xié)程半毛錢關系沒有惩猫,切線程解決不了的事情芝硬,協(xié)程也解決不了。

所以轧房,總結(jié)一下協(xié)程

  • 協(xié)程就是切線程
  • 掛起就是可以自動切回來的切線程
  • 非阻塞式是用看起來阻塞的代碼實現(xiàn)非阻塞的操作

協(xié)程并沒有創(chuàng)造新的東西拌阴,只是將多線程開發(fā)變的更簡單,原理依然是切換線程并回調(diào)到原本的線程

協(xié)程的進階用法

launchasync

前面講到的 launchasync锯厢,現(xiàn)在來對比一下

用法很相似皮官,都能啟動一個協(xié)程

  • launch啟動新協(xié)程但不返回結(jié)果脯倒。任何被視為“一勞永逸”的工作都可以使用 launch 來啟動
  • async會啟動一個新的協(xié)程实辑,并使用一個名為 await 的掛起函數(shù)并在稍后返回結(jié)果

舉例:例如我們要顯示一個列表,數(shù)據(jù)源從兩個接口獲取藻丢,如果用launch啟動協(xié)程剪撬,我們會啟動兩個請求,在任一請求結(jié)束時悠反,檢查另一個請求的結(jié)果残黑,等兩個請求結(jié)束和馍佑,開始合并數(shù)據(jù)源,進行顯示

如果我們使用async

        val listOne = async { fetchList(1) }        val listTwo = async { fetchList(2) }        mergeList(listOne.await(), listTwo.await())// mergeList 為自定義合并函數(shù)        

通過對每個延遲引用調(diào)用 await()梨水,我們可以保證這兩項 async 完成之后拭荤,開始合并,而不需要考慮任何先后問題

還可以對集合使用 awaitAll()

        val deferreds = listOf(                 async { fetchList(1)},             async { fetchList(2)}           )        mergeList(deferreds.awaitAll())

常規(guī)情況疫诽,只需要使用launch啟動協(xié)程舅世,當使用async時,需要注意:async 期望您最終會調(diào)用 await 來獲取結(jié)果(或異常)奇徒,因此默認情況下它不會拋出異常雏亚。

Dispatchers

Kotlin 提供了三個可用于線程調(diào)度的 Dispatcher。

a b
Dispatchers.Main Android主線程摩钙,用于和用戶交互
Dispatchers.IO 適合 IO 密集型的任務罢低,比如:讀寫文件,操作數(shù)據(jù)庫以及網(wǎng)絡請求
Dispatchers.Default 針對 CPU 密集型工作進行了優(yōu)化胖笛,比如計算/JSON解析等

異常傳播及處理

JobSupervisorJob

通常情況网持,我們使用launch或者async創(chuàng)建協(xié)程,會默認創(chuàng)建使用Job來處理匀钧,一個任務失敗翎碑,會影響他的子協(xié)程和父協(xié)程。

異常會到達層級的根部之斯,而且當前 CoroutineScope 啟動的所有協(xié)程都會被取消日杈。

如果我們不想因為一個任務的失敗而影響其他任務, 子協(xié)程運行失敗不影響其他子協(xié)程和父協(xié)程佑刷,那么可以在創(chuàng)建協(xié)程時在 CoroutineScopeCoroutineContext 中使用 Job 的另一個擴展: SupervisorJob

當子協(xié)程任務出錯或失敗時莉擒,SupervisorJob 不會取消它和它自己的子級,也不會傳播異常并傳遞給它的父級瘫絮,它會讓子協(xié)程自己處理異常

coroutineScopesupervisorScope

通過 launchasync, 能很輕松啟動一個線程涨冀,請求網(wǎng)絡并獲取數(shù)據(jù)

但是有時候,你的需求比較復雜麦萤,需要在一個協(xié)程中執(zhí)行多個網(wǎng)絡請求鹿鳖,那就意味著你要啟動更多協(xié)程。

在掛起函數(shù)中創(chuàng)建更多的協(xié)程壮莹,可以使用名為 coroutineScope 的構(gòu)建器或 supervisorScope 來啟動更多的協(xié)程翅帜。

 suspend fun fetchTwoDocs() {    coroutineScope {        launch { fetchList(1) }        async { fetchList(2) }    }}

注意:coroutineScopeCoroutineScope 是不同的東西,盡管它們的名字只有一個字符不同命满,CoroutineScope 是協(xié)程作用域涝滴,而coroutineScope 是在掛起函數(shù)中創(chuàng)建新協(xié)程的一個掛起函數(shù),它接受CoroutineScope 作為參數(shù),并在CoroutineScope 中創(chuàng)建協(xié)程

coroutineScopesupervisorScope 最主要的不同在哪呢歼疮?在于子協(xié)程出錯時的處理

coroutineScope 是繼承外部Job的上下文創(chuàng)建作用域杂抽,其內(nèi)部的取消操作是雙向傳播的,子協(xié)程未捕獲的異常也會向上傳遞給父協(xié)程韩脏。任何一個子協(xié)程異常退出,那么整體都將退出赡矢。

supervisorScope 同樣繼承外部作用域的上下文,但其內(nèi)部的取消操作是單向傳播的济竹,父協(xié)程向子協(xié)程傳播,反過來則不然梦谜,這意味著子協(xié)程出了異常并不會影響父協(xié)程以及其他兄弟協(xié)程。

所以唁桩,當處理多并發(fā)任務時耸棒,如果不想因為一個任務的失敗而影響其他任務,就可以使用supervisorScope 創(chuàng)建協(xié)程与殃,反之使用coroutineScope

注意: SupervisorJob 只有作為supervisorScopeCoroutineScope(SupervisorJob())的一部分時,才會按照上面的描述工作幅疼。

協(xié)程異常處理

協(xié)程的異常米奸,一般使用try/catch或者runCatching內(nèi)置函數(shù)來處理(內(nèi)部也是使用try/catch),在try中編寫請求代碼爽篷,catch負責捕獲異常悴晰。

例如

        GlobalScope.launch {            val scope = CoroutineScope(Job())                scope.launch {                    try {                        throw Exception("Failed")                    } catch (e: Exception) {                      //捕獲到異常                    }                }        }

正常來說,try-catch塊中只有代碼塊存在異常逐工,都將被捕獲到catch中铡溪。但是協(xié)程中的異常卻存在特殊情況。

例如在協(xié)程中開啟一個失敗的子協(xié)程泪喊,則無法捕獲棕硫。還是上面的例子:

        GlobalScope.launch {            val scope = CoroutineScope(Job())            try { //try catch 在launch 作用域之外                scope.launch {                    throw Exception("Failed")                }            } catch (e: Exception) {                e.printStackTrace()                 //無法捕獲異常,程序崩潰            }        }

try-catch塊中創(chuàng)建了一個子協(xié)程窘俺,拋出一個異常饲帅,這個時候我們期望的是能將異常捕獲至catch中,但是真正運行后卻發(fā)現(xiàn)App崩潰退出了瘤泪。這也驗證了try-catch作用無效灶泵。

這就涉及到協(xié)程中異常傳播問題

異常傳播

在kotlin的協(xié)程中,每個協(xié)程是一個作用域对途,新建的協(xié)程與它的父作用域存在一個層次結(jié)構(gòu)赦邻。而這級聯(lián)關系主要在于:

協(xié)程中的任務,一旦因為異常而運行失敗实檀,它會立即將這個異常傳遞給它的父級惶洲,由父級來決定處理:

  • 取消它自己的子級;
  • 取消它自己膳犹;
  • 將異常傳播并傳遞給它的父級

這也是為什么我們try-catch子協(xié)程為什么會失敗恬吕,因為子協(xié)程中異常會向上傳播,但父任務未處理異常须床,導致父任務失敗铐料。

如果將上面例子再次修改:

        GlobalScope.launch {            val scope = CoroutineScope(Job())            val job = scope.async { //將launch改為async                throw Exception("Failed")            }            try {                job.await()            } catch (e: Exception) {                e.printStackTrace()                //成功捕獲異常            }        }

為什么async使用try-catch能捕獲異常呢豺旬?當 async 被用作根協(xié)程時在調(diào)用 **.await() **時會拋出異常族阅。這里的根協(xié)程指的是CoroutineScope(SupervisorJob()) 實例或 supervisorScope 的直接子協(xié)程

所以try-catch包裹.await()時可以捕獲異常

如果 async 被不用作根協(xié)程愧沟,例如:

            val scope = CoroutineScope(Job())            scope.launch { //根協(xié)程                val job = async { //async 開啟子協(xié)程                    throw Exception("Failed") //異常會立即拋出                }                try {                    job.await()                } catch (e: Exception) {                    e.printStackTrace()                    //無法捕獲異常央渣,程序崩潰                }            }       

這時候芽丹,try-catch無法捕獲異常拔第,程序崩潰,因為 launch 用作根協(xié)程逛万,子協(xié)程的異常必定會傳播給父協(xié)程得封,無論子協(xié)程是launch還是async忙上,異常都不會拋出疫粥,所以無法捕獲

如果async創(chuàng)建的子協(xié)程產(chǎn)生的異常不向上傳遞项秉,是不是就可以避免異常影響父協(xié)程伙狐,導致應用崩潰呢贷屎?

        val scope = CoroutineScope(Job())        scope.launch {            supervisorScope { //在supervisorScope中創(chuàng)建子協(xié)程                    val job = async { //async 相當于                        throw Exception("Failed")                    }                try {                    job.await()                } catch (e: Exception) {                    e.printStackTrace()                    //成功捕獲異常唉侄,程序無崩潰                }            }        }

或者

        val scope = CoroutineScope(Job())        scope.launch {            coroutineScope {                    val job = async(SupervisorJob()) { //async 開啟子協(xié)程                        throw Exception("Failed")                    }                try {                    job.await()                } catch (e: Exception) {                    e.printStackTrace()                    //成功捕獲異常,程序無崩潰                }            }        }

實際上同眯,上面兩個例子须蜗,分別使用supervisorScopeCoroutineScope(SupervisorJob())明肮,將異常不向上傳遞柿估,由當前協(xié)程拋出秫舌,try-catch來捕獲

那么如果未使用supervisorScopeCoroutineScope(SupervisorJob()),異常未能捕獲嫂粟,一直向上傳遞到根層級的根部,導致父級失敗飒房,該如何處理狠毯?

CoroutineExceptionHandler

協(xié)程處理異常的第二個方法是使用CoroutineExceptionHandler

針對協(xié)程中嚼松,自動拋出的(launch創(chuàng)建的協(xié)程)未捕獲的異常献酗,我們可以使用CoroutineExceptionHandler來處理

CoroutineExceptionHandler是用于全局“捕獲所有”行為的最后一種機制罕偎。您無法在CoroutineExceptionHandler中從異常中恢復颜及。當處理程序被調(diào)用時俏站,協(xié)程已經(jīng)完成了相應的異常肄扎。通常,該處理程序用于記錄異常赞哗、顯示某種錯誤消息肪笋、終止和/或重新啟動應用程序。

這段話讀起來有點難以理解惭墓,換個思路理解 CoroutineExceptionHandler是全局捕獲異常的方式腊凶,說明異常經(jīng)子作用域一級級向上傳遞钧萍,到達最頂層的作用域风瘦,說明子作用域都全部取消了万搔,CoroutineExceptionHandler 被調(diào)用時瞬雹,所有子協(xié)程已經(jīng)傳遞了相應異常刽虹,不會有新的異常傳遞了

所以CoroutineExceptionHandler必須設置在最頂層作用域才能捕獲異常意敛,不然捕獲失敗膛虫。

CoroutineExceptionHandler的使用

下面是如何聲明一個CoroutineExceptionHandler 的例子稍刀。

        val exHandler = CoroutineExceptionHandler{context, exception ->            println(exception)         }        val scope = CoroutineScope(Job())        scope.launch {            launch(exHandler) {                throw  Exception("Failed") //異常捕獲失敗            }        }

異常不會被捕獲的原因是因為 exHandler 沒有給父級账月。內(nèi)部協(xié)程會在異常出現(xiàn)時傳播異常并傳遞給它的父級剧劝,由于父級并不知道 handler 的存在讥此,異常就沒有被拋出萄喳。

改成下面例子他巨,就能正常捕獲異常

        val exHandler = CoroutineExceptionHandler{context, exception ->            println(exception)        }        val scope = CoroutineScope(Job())        scope.launch(exHandler) {//最上層協(xié)程捕獲            launch {                throw  Exception("Failed")            }        }

CoroutineExceptionHandler的不足
  • 由于沒有try-catch來捕獲住異常捻爷,異常會向上傳播役衡,直到它到達根協(xié)程,根據(jù)協(xié)程的結(jié)構(gòu)化并發(fā)的特性俐芯,異常向上傳播時吧史,父協(xié)程會失敗贸营,同時父協(xié)程所級聯(lián)的子協(xié)程和兄弟協(xié)程也都會失敗捕儒;

  • CoroutineExceptionHandler的作用在于全局捕獲異常阎毅,CoroutineExceptionHandler無法在代碼的特定部分處理異常点弯,例如針對某一個失敗接口,無法在異常后進行重試或者其他特定操作肃拜。

  • 如果你想在特定部分做異常處理的話士聪,try-catch更適合剥悟。

總結(jié)

協(xié)程的異常捕獲機制,主要就是兩點: 局部異常捕獲全局異常捕獲

異常發(fā)生的作用域:

  • 作用域內(nèi)慈缔,直接try-catch藐鹤,則可以直接捕獲異常,進行處理

  • 作用域外

    • launch啟動的的作用域無法捕獲異常肄满,會立即雙向傳遞稠歉,最終拋出

    • async啟動的作用域:

      • 如果asyncCoroutineScope(SupervisorJob) 實例或 supervisorScope中啟動協(xié)程,則異常不會向上傳遞瞬测,可以在async.await()時捕獲異常
      • 如果async在非SupervisorJob實例或supervisorScope的直接子協(xié)程中啟動横媚,則異常雙向傳播,在async.await()時無法捕獲異常

supervisorScope 中異常月趟,不會向上傳遞灯蝴,只會影響自己

coroutineScope中異常,會向雙向傳遞孝宗,影響自己和父級

CoroutineExceptionHandler只能捕獲launch中的異常穷躁,launch產(chǎn)生的異常會立即傳遞給父級,而且CoroutineExceptionHandler必須給最上層launch才會生效

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市问潭,隨后出現(xiàn)的幾起案子梳虽,更是在濱河造成了極大的恐慌禀挫,老刑警劉巖砰左,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件适室,死亡現(xiàn)場離奇詭異汽畴,居然都是意外死亡罢坝,警方通過查閱死者的電腦和手機娱仔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來滨达,“玉大人,你說我怎么就攤上這事〗糇洌” “怎么了博个?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵瘩缆,是天一觀的道長熟尉。 經(jīng)常有香客問我往果,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任嗦篱,我火速辦了婚禮,結(jié)果婚禮上嫁乘,老公的妹妹穿的比我還像新娘。我一直安慰自己殉挽,他們只是感情好,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布蝗敢。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪劈狐。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機與錄音熙兔,去河邊找鬼打毛。 笑死,一個胖子當著我的面吹牛俩功,可吹牛的內(nèi)容都是我干的幻枉。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼诡蜓,長吁一口氣:“原來是場噩夢啊……” “哼展辞!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起万牺,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤罗珍,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后脚粟,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體覆旱,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年核无,在試婚紗的時候發(fā)現(xiàn)自己被綠了扣唱。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡团南,死狀恐怖噪沙,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情吐根,我是刑警寧澤正歼,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站拷橘,受9級特大地震影響局义,放射性物質(zhì)發(fā)生泄漏喜爷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一萄唇、第九天 我趴在偏房一處隱蔽的房頂上張望檩帐。 院中可真熱鬧,春花似錦另萤、人聲如沸湃密。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽泛源。三九已至,卻和暖如春目养,著一層夾襖步出監(jiān)牢的瞬間俩由,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工癌蚁, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留幻梯,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓努释,卻偏偏與公主長得像碘梢,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子伐蒂,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

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

  • 一煞躬、前言: 1、什么是協(xié)程逸邦? 協(xié)程可以理解就是一種用戶空間線程(另外一種線程)恩沛,他的調(diào)度是由程序員自己寫程序來管理...
    因為我的心閱讀 885評論 3 1
  • 協(xié)程怎么理解 一種在程序中處理并發(fā)任務的方案;也是該方案的一個組件 協(xié)程和線程屬于一個層級的概念 協(xié)程中不存在線程...
    念故淵閱讀 612評論 0 0
  • 一缕减、Kotlin 協(xié)程概念 Kotlin 協(xié)程提供了一種全新處理并發(fā)的方式雷客,你可以在 Android 平臺上使用它...
    4e70992f13e7閱讀 1,710評論 0 2
  • 在今年的三月份,我因為需要為項目搭建一個新的網(wǎng)絡請求框架開始接觸 Kotlin 協(xié)程桥狡。那時我司項目中同時存在著兩種...
    業(yè)志陳閱讀 1,039評論 0 5
  • 摘要 協(xié)程更像是一種自動幫我們切換線程的工具搅裙,對于操作系統(tǒng)是透明的。此外裹芝,利用協(xié)程來寫異步方法部逮,也可以避免回調(diào)地獄...
    JalorOo閱讀 687評論 0 0