kotlin 协程异常处理机制颠覆三观
轉載請標明出處:http://blog.csdn.net/zhaoyanjun6/article/details/121938761
本文出自【趙彥軍的博客】
文章目錄
- 協程樹與結構化并發
- 協程作用域的cancel
- 協程的cancel狀態
- 清理
- 協程的返回值
- 協程異常的處理
- 結構化并發的異常處理
- 協程的異常處理
- CoroutineExceptionHandler
- 實戰一:停不下來的協程
- 實戰二:可以停下來的協程
協程的異常處理與 OKhttp、 RxJava這些框架的處理方式都不太一樣,因為異步代碼的異常處理,往往是比較麻煩的,而到了同步化處理的協程框架下,異常就變得比較容易進行管理了。
要完全理解協程的異常,我們需要先理解協程的樹形結構和結構化并發,在這基礎上,就能很容易的理解協程是如果管理異常的了。
協程樹與結構化并發
在協程作用域中,可以創建一個協程,同時,一個協程中還可以繼續創建協程,所以這就形成了一個樹形結構。借助這樣的樹形結構,協程可以很容易的控制結構化并發,父協程可以控制子協程的生命周期,而子協程可以從父協程繼承協程上下文。
在代碼中,可以通過coroutineScope {}來顯示的創建一個協程作用域,它和測試時常用的runBlocking {}一樣,都是協程的作用域構建器。
協程作用域的cancel
借助協程作用域的管理,我們可以輕松的控制該協程作用域下的所有協程,一旦取消一個協程作用域,那么這個協程作用域下的所有協程都將被取消。
val job1 = scope.launch {...} val job2 = scope.launch {...} scope.cancel()如上所示,調用scope的cancel之后,job1和job2都將被取消。
而如果只想取消某個單獨的協程,那么可以通過該協程的句柄Job對象來取消。
val job1 = scope.launch { … } val job2 = scope.launch { … }job1.cancel()如上所示,這樣就只取消了Job1的協程,而Job2不受影響。
這就是協程結構化并發的兩個特點:
- 取消一個協程作用域,將取消該協程作用域下的所有子協程
- 被取消的子協程,不會影響其它同級的協程
在Android開發中,大部分場景下我們不需要考慮協程的cancel,借助ViewModelScope、LifecycleScope和MainScope這些場景的協程作用域,我們可以很方便的避免內存泄漏,在cancel時結束所有的子協程。
協程的cancel狀態
協程的 cancel 與線程的 cancel 類似,協程一旦開始執行(代碼占用CPU),只有執行完畢才會被 cancel,當協程調用 cancel,只是將協程的Job 生命周期設置為了 Canceling,直到協程執行完畢才會被置為 Canceled 。
如果一定要及時取消掉協程的執行,那么可以和線程做類似的操作,在協程代碼內及時判斷協程的狀態來控制代碼的執行。
所以,協程推薦開發者在使用協程時,以協作的方式來使用,即隨時判斷當前協程的生命周期,避免浪費計算資源。
協程提供了兩種方式來進行協作式的 cancel:
- Job.isActive或者ensureActive()
- yield
ensureActive()是Job.isActive的封裝實現,借助這個方法,就是在協程內代碼執行前,對當前協程的狀態進行一次判斷。
清理
通常, 當協程被取消時, 需要做一些清理工作, 此時, 可以把協程中運行的代碼用try {} fininaly {}塊包住, 這樣當協程被取消時, 會執行 fininaly 塊中的清理工作。但是fininaly塊中不能直接調用掛起函數,否則會拋出CancellationException異常,因為它已經被取消了,而你又要在fininaly塊中執行掛起函數把它掛起,顯然與要求矛盾。然而,如果非要這么做,也不是不可以,當你需要掛起一個被取消的協程,你可以將相應的代碼包裝在withContext(NonCancellable) {}中,并使用withContext函數以及NonCancellable上下文,代碼如下所示。
fun main() = runBlocking {val job = launch {try {repeat(1000) { i ->println("job: I'm sleeping $i ...")delay(500L)}} finally {withContext(NonCancellable) { // 重點注意這里println("job: I'm running finally")delay(1000L) // 這里調用了掛起函數!println("job: And I've just delayed for 1 sec because I'm non-cancellable")}}}delay(1300L) // 延遲一段時間println("main: I'm tired of waiting!")job.cancelAndJoin() // 取消該作業并等待它結束println("main: Now I can quit.") } job: I'm sleeping 0 ... job: I'm sleeping 1 ... job: I'm sleeping 2 ... main: I'm tired of waiting! job: I'm running finally job: And I've just delayed for 1 sec because I'm non-cancellable main: Now I can quit.協程的返回值
協程獲取返回值有兩種方式:
- launch返回的Job實例可以調用Join方法(Join函數會掛起協程直到協程執行完成)
- async返回的Deferred實例(Job 的子類)可以調用await方法
如果在調用Join后再調用cancel,那么協程將在執行完成后被Cancel,如果先cancel再調用Join,那么協程也將執行完成
協程異常的處理
當協程作用域中的一個協程發生異常時,此時的異常流程如下所示:
- 發生異常的協程被cancel
- 異常傳遞到它的父協程
- 父協程 cancel(取消其所有子協程)
- 將異常在協程樹上進一步向上傳播
這種行為實際上是符合協程結構化并發的規則的,但是在實際使用中,這種結構化的異常處理,會讓異常的處理有些暴力,大部分場景下,業務需求都是希望異常不影響正常的業務流程。
結構化并發的異常處理
所以,協程提出了 SupervisorJob 的新概念,它是Job的子類。
SupervisorJob 的作用就是將協程中的異常「掐死」在協程內部,切斷其向上傳播的路徑。使用 SupervisorJob 后,子協程的異常退出不會影響到其他子協程,同時 SupervisorJob 也不會傳播異常而是讓異常發生的協程自己處理。
SupervisorJob 可以在創建 CoroutineScope 的時候作為參數傳進來,也可以使用 supervisorScope 來創建一個自定義的協程作用域,所以SupervisorJob 只有下面兩種使用方式。
- supervisorScope{}
- CoroutineScope(SupervisorJob())
但是要注意的是,不論是SupervisorJob還是Job,如果協程內部發生異常,這個異常是肯定會被拋出的,只是是否會崩潰。
這里有個誤區,那就是大家不要以為使用SupervisorJob之后,協程就不會崩潰,不管你用什么Job,該崩潰的還是要崩潰的,它們的差別在于是否會影響到別的協程,例如下面這個例子。
val coroutineScope = CoroutineScope(Job()) coroutineScope.launch {throw Exception("test") } coroutineScope.launch {Log.d("xys", "test") }使用Job的時候,第二個協程是無法執行的,但你改為SupervisorJob()之后,第二個協程就可以執行了,因為第一個協程的崩潰,并沒有影響到第二個協程的執行。
所以說,SupervisorJob的目的是為了在結構化并發中找到一個特殊處理的方式,并沒有將異常隱藏起來。
SupervisorJob最多的使用場景就是多協程的并發處理,讓某個協程的異常不干擾其它正常的協程。而CoroutineScope也很有用,因為你可以在一個協程發生異常時,取消其關聯的所有協程,做為統一的處理。
從異常流動方向上來看,coroutineScope是雙向的,而supervisorScope則是單向的。
平時常見的MainScope,就是使用的SupervisorJob,所以MainScope中的子協程之間互相不會影響。
協程的異常處理
前面我們說了,協程中的異常是一定會拋出的,所以在一個協程內部,我們到底怎么處理異常呢?
launch:通過launch啟動的異常可以通過try catch來進行異常捕獲,或者使用協程封裝的拓展函數runCatching來捕獲,其內部也是使用的try catch。
async:async的異常處理比較麻煩,我們下面詳細的說下。
首先,當async被用作構建根協程(由協程作用域直接管理的協程)時,異常不會主動拋出,而是在調用.await()時拋出。
來看下這個例子:
MainScope().launch {supervisorScope {val deferred = async {throw Exception("test")}try {deferred.await()} catch (e: Exception) {e.printStackTrace()}} }執行這個例子后,異常將被捕獲,從上面的代碼可以看出,異常只會發生在執行await的時候,調用async是不會發生異常的,不過,細心的朋友可能發現了,這里使用的是supervisorScope,如果我們改成coroutineScope呢?
執行代碼后我們會發現,異常并沒有被捕獲,這就是我們前面說到的SupervisorJob和Job的區別。
再看一個例子:
ainScope().launch {try {async {throw Exception("test")}} catch (e: Exception) {e.printStackTrace()} }我們去掉了supervisorScope,所以async的父協程是Job,所以這個時候,即使是調用async,也會發生異常,同時也不會被捕獲。
綜上,async的異常,只能在supervisorScope中,使用try catch進行捕獲。
CoroutineExceptionHandler
CoroutineExceptionHandler 類似 Android 中的全局異常處理,當異常在協程樹中傳遞時,如果沒有設置 CoroutineExceptionHandler ,那么異常將被繼續傳遞直到拋出,但如果設置了 CoroutineExceptionHandler ,那么則可以在這里處理未捕獲的異常, CoroutineExceptionHandler 的創建如下所示。
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->Log.d("xys", "---${coroutineContext} ${throwable.printStackTrace()}") }我們來看下面的這個例子,在父協程中設置CoroutineExceptionHandler,當它的子協程發生異常時,即使不使用try catch,異常也會被捕獲。
MainScope().launch(exceptionHandler) {async {throw Exception("test")} }但是考慮下這樣一個場景,讓發生異常的協程使用CoroutineExceptionHandler,代碼如下所示。
MainScope().launch {async(exceptionHandler) {throw Exception("test")} }很遺憾,這樣就不能捕獲異常,因為 CoroutineExceptionHandler 屬于異常拋出的協程,它本身無法處理。
所以,CoroutineExceptionHandler 的使用也有這樣的限制,即CoroutineExceptionHandler 必須在發生異常的父協程中設置,其原因就是協程的結構化并發,異常會傳遞到父協程中進行處理,所以,這里必須是父協程中設置 CoroutineExceptionHandler 才能生效。
要注意的是,CoroutineExceptionHandler 只是協程處理異常「最后的倔強」,此時協程已經完全Cancel,只是給你個通知,協程異常了,所以這里只能對異常做記錄,無法再操作協程。
實戰一:停不下來的協程
class MainActivity : AppCompatActivity() {private var job: Job? = nulloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)val binding = ActivityMainBinding.inflate(layoutInflater)setContentView(binding.root)job = GlobalScope.launch {while (true) {Log.d("yyy-", "${System.currentTimeMillis()}")}}//取消binding.downloadBtn.setOnClickListener {job?.cancel()}} }發現取消協程后,日志還在瘋狂輸出,根本沒有停下來。
實戰二:可以停下來的協程
class MainActivity : AppCompatActivity() {private var job: Job? = nulloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)val binding = ActivityMainBinding.inflate(layoutInflater)setContentView(binding.root)job = GlobalScope.launch {while (isActive) {Log.d("yyy-", "${System.currentTimeMillis()}")}}binding.downloadBtn.setOnClickListener {job?.cancel()}} }取消協程后,日志停止輸出。
或者用 ensureActive() ,也可以達到同樣的效果:
class MainActivity : AppCompatActivity() {private var job: Job? = nulloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)val binding = ActivityMainBinding.inflate(layoutInflater)setContentView(binding.root)job = GlobalScope.launch {while (true) {ensureActive()Log.d("yyy-", "${System.currentTimeMillis()}")}}binding.downloadBtn.setOnClickListener {job?.cancel()}} }ensureActive() 的工作原理是,如果沒有停止,就拋出一個 CancellationException
總結
以上是生活随笔為你收集整理的kotlin 协程异常处理机制颠覆三观的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android StateFlow详解
- 下一篇: Kotlin协程重新认知 Corouti