原文做者:Manuel Vivo html
原文地址:Exceptions in Coroutinesandroid
譯者:秉心說git
本文是 協程的取消和異常 系列的第三篇,往期目錄以下:github
在閱讀本文以前,強烈建議回顧一下以前兩篇文章。實在沒有時間的話,至少讀一下第一篇文章。async
下面開始正文。編輯器
做爲開發者,咱們一般會花費大量時間來完善咱們的應用。可是,當發生異常致使應用不按預期執行時儘量的提供良好的用戶體驗也是一樣重要的。一方面,應用 Crash 對用戶來講是很糟糕的體驗;另外一方面,當用戶操做失敗時,提供正確的信息也是必不可少的。函數
優雅的異常處理對用戶來講是很重要的。在這篇文章中,我會介紹在協程中異常是怎麼傳播的,以及如何使用各類方式控制異常的傳播。測試
若是你更喜歡視頻,能夠觀看 Florina Muntenescu 和我 在 KotlinConf'19 上的演講,地址以下:
https://www.youtube.com/watch?v=w0kfnydnFWI&feature=emb_logo
爲了幫你更好的理解本文的剩餘內容,建議首先閱讀該系列的第一篇文章 Coroutines: First things first
當一個協程發生了異常,它將把異常傳播給它的父協程,父協程會作如下幾件事:
異常最終將傳播至繼承結構的根部。經過該 CoroutineScope
建立的全部協程都將被取消。
在某些場景下,這樣的異常傳播是適用的。可是,也有一些場景並不合適。
想象一個 UI 相關的 CoroutineScope
,它負責處理用戶交互。若是它的一個子協程拋出了異常,那麼這個 UI Scope 將被取消。因爲被取消的做用域沒法啓動更多協程,整個 UI 組件將沒法響應用戶交互。
若是你不想要這樣怎麼辦?或許,在建立協程做用域的 CoroutineContext
時,你能夠選擇不同的 Job
實現 —— SupervisorJob
。
經過 SupervisorJob,子協程的失敗不會影響其餘的子協程。此外,SupervisorJob
也不會傳播異常,而是讓子協程本身處理。
你能夠這樣建立協程做用域 val uiScope = CoroutineScope(SupervisorJob())
,來保證不傳播異常。
若是異常沒有被處理,CoroutineContext
也沒有提供異常處理器 CoroutineExceptionHandler (稍後會介紹),將會使用默認的異常處理器。在 JVM 上,異常會被打印到控制檯;在 Android 上,不管發生在什麼調度器上,你的應用都會崩潰。
💥 不管你使用哪一種類型的 Job,未捕獲異常最終都會被拋出。
一樣的行爲準則也適用於協程做用域構建器 coroutineScope 和 supervisorScope 。它們都會建立一個子做用域(以 Job 或者 SupervisorJob 做爲 Parent),來幫助你給協程從邏輯上分組(若是你想進行並行計算,或者它們是否會相互影響)。
警告:
SupervisorJob
僅在屬於下面兩種做用域時才起做用:使用 supervisorScope 或者 CoroutineScope(SupervisorJob()) 建立的做用域。
何時使用 Job ?何時使用 SupervisorJob ?
當你不想讓異常致使父協程和兄弟協程被取消時,使用 SupervisorJob
或者 supervisorScope
。
看看下面這個示例:
// Scope handling coroutines for a particular layer of my app
val scope = CoroutineScope(SupervisorJob()) scope.launch { // Child 1 } scope.launch { // Child 2 } 複製代碼
在這樣的狀況下,child#1
失敗了,scope
和 child#2
都不會被取消。
另外一個示例:
// Scope handling coroutines for a particular layer of my app
val scope = CoroutineScope(Job()) scope.launch { supervisorScope { launch { // Child 1 } launch { // Child 2 } } } 複製代碼
在這種狀況下,supervisorScope
建立了一個攜帶 SupervisorJob
的子做用域。若是 child#1
失敗,child#2
也不會被取消。可是若是使用 coroutineScope
來代替 supervisorScope
的話,異常將會傳播並取消做用域。
經過下面的代碼段,你能肯定 child#1
的父級是哪種 Job 嗎?
val scope = CoroutineScope(Job())
scope.launch(SupervisorJob()) { // new coroutine -> can suspend launch { // Child 1 } launch { // Child 2 } } 複製代碼
child#1
的父 Job 是 Job
類型 !但願你回答正確!儘管第一眼看上去,你可能認爲是 SupervisorJob
,但並非。由於在這種狀況下,每一個新的協程老是被分配一個新的 Job,這個新的 Job 覆蓋了 SupervisorJob
。SupervisorJob
是父協程經過 scope.launch
建立的。也就是說,在上面的例子中,SupervisorJob
沒有發揮任何做用。
因此,不管是 child#1 仍是 child#2 發生了異常,都將傳播到 scope,並致使全部由其啓動的協程被取消。
記住 SupervisorJob
僅在屬於下面兩種做用域時才起做用:使用 supervisorScope 或者 CoroutineScope(SupervisorJob()) 建立的做用域。 將 SupervisorJob
做爲參數傳遞給協程構建器並不會產生你所預期的效果。
關於異常,若是子協程拋出了異常,SupervisorJob
不會進行傳播並讓子協程本身去處理。
若是你好奇 Job
的工做原理,能夠在 JobSupport.kt
文件中查看 childCancelled 和 notifyCancelling 這兩個函數的實現。
對於 SupervisorJob
的實現,childCancelled()
方法僅僅只是返回 false
,表示它不會傳播異常,同時也不會處理異常。
在協程中,可使用常規語法來處理異常:try/catch
或者內置的函數 runCatching
(內部使用了 try/catch) 。
咱們以前說過 未捕獲的異常始終會被拋出 。可是不一樣的協程構建器對於異常有不一樣的處理方式。
在 launch 中,異常一旦發生就會立馬被拋出 。所以,你可使用 try/catch
包裹會發生異常的代碼。以下所示:
scope.launch {
try { codeThatCanThrowExceptions() } catch(e: Exception) { // Handle exception } } 複製代碼
在 launch 中,異常一旦發生就會立馬被拋出 。
當 async
在根協程 (CoroutineScope
實例或者 supervisorJob
的直接子協程) 使用時,異常不會被自動拋出,而是直到你調用 .await()
時才拋出。
爲了處理 async
拋出的異常,你能夠在 try/catch
中調用 await
。
supervisorScope {
val deferred = async { codeThatCanThrowExceptions() } try { deferred.await() } catch(e: Exception) { // Handle exception thrown in async } } 複製代碼
在上面的例子中,async
的調用處永遠不會拋出異常,因此這裏並不須要包裹 try/catch
。await()
方法將會拋出 async 內部發生的異常。
注意上面的代碼中咱們使用的是 supervisorScope
來調用 async
和 await
。就像以前說過的那樣,SupervisorJob
讓協程本身處理異常。與之相反的,Job
會傳播異常,因此 catch
代碼塊不會被調用。
coroutineScope {
try { val deferred = async { codeThatCanThrowExceptions() } deferred.await() } catch(e: Exception) { // Exception thrown in async WILL NOT be caught here // but propagated up to the scope } } 複製代碼
此外,由其餘協程建立的協程若是發生了異常,也將會自動傳播,不管你的協程構建器是什麼。
舉個例子:
val scope = CoroutineScope(Job())
scope.launch { async { // If async throws, launch throws without calling .await() } } 複製代碼
在上面的例子中,若是 async
發生了異常,會當即被拋出。由於 scope 的直接子協程是由 scope.launch
啓動的,async
繼承了協程上下文中的 Job
,致使它會自動向父級傳播異常。
⚠️ 經過 coroutineScope 構建器或者由其餘協程啓動的協程拋出的異常,不會被
try/catch
捕獲!
在 SupervisorJob
那一節,咱們提到了 CoroutineExceptionHandler
。如今讓咱們來深刻了解它。
協程異常處理器 CoroutineExceptionHandler 是 CoroutineContext
中的一個可選元素,它能夠幫助你 處理未捕獲異常 。
下面的代碼展現瞭如何定義一個 CoroutineExceptionHandler
。不管異常什麼時候被捕獲,你都會獲得關於發生異常的 CoroutineContext
的信息,和異常自己的信息。
val handler = CoroutineExceptionHandler {
context, exception -> println("Caught $exception") } 複製代碼
若是知足如下要求,異常將會被捕獲:
launch
,而不是
async
)
CoroutineScope
的直接子協程或者
supervisorScope
)
讓咱們看兩個 CoroutineExceptionHandler
的使用例子。
在下面的例子中,異常會被 handler
捕獲:
val scope = CoroutineScope(Job())
scope.launch(handler) { launch { throw Exception("Failed coroutine") } } 複製代碼
下面的另外一個例子中,handler
在一個內部協程中使用,它不會捕獲異常:
val scope = CoroutineScope(Job())
scope.launch { launch(handler) { throw Exception("Failed coroutine") } } 複製代碼
因爲 handler
沒有在正確的協程上下文中使用,因此異常沒有被捕獲。內部 launch 啓動的協程一旦發生異常會自動傳播到父協程,而父協程並不知道 handler 的存在,因此異常會被直接拋出。
即便你的應用由於異常沒有按照預期執行,優雅的異常處理對於良好的用戶體驗也是很重要的。
當你要避免因異常自動傳播形成的協程取消時,記住使用 SupervisorJob
,不然請使用 Job
。
未捕獲異常將會被傳播,捕獲它們,提供良好的用戶體驗!
這篇文章就到這裏了,這個系列還剩最後一篇了。
在以前提到協程的取消時,介紹了 viewModelScope
等跟隨生命週期自動取消的協程做用域。可是不想取消時,應該怎麼作?下一篇將會爲你解答。
我是秉心說,關注我,不迷路!
本文使用 mdnice 排版