
Kotlin 中的協程已經成為在網路請求中比較常用的一種方式,除了正常請求外,我們同樣需要處理請求中的例外情況,本篇文章將處理協程中的例外分為以下幾個部分:
一、try-catch
1.1 try-catch基礎使用
1.2 什么情況下try-catch會無效?
1.3 什么是協程的結構化并發?
二、CoroutineExceptionHandler
2.1 CoroutineExceptionHandler的介紹
2.2 CoroutineExceptionHandler的使用
2.3 CoroutineExceptionHandler的不足
三、SupervisorScope+async
四、結論
一、try-catch捕獲例外
1.1 try-catch基礎使用
一般來說,處理例外可以使用try-catch塊,在try中撰寫請求代碼,catch負責捕獲例外,
以一個普通的請求為例:
Api介面:
interface ProjectApi {
@GET("project/tree/json")
suspend fun loadProjectTree(): BaseResp<List<ProjectTree>>
@GET("project/tree/jsonError")
suspend fun loadProjectTreeError(): BaseResp<List<ProjectTree>>
}
Api介面里面列出了兩個介面,一個loadProjectTree()作為能夠請求成功的介面,loadProjectTreeError()則故意在path中多加了’Error’,模擬請求失敗的狀態,
具體呼叫:
suspend fun loadProjectTree() {
try {
val result = service.loadProjectTree()
val errorResult = service.loadProjectTreeError()
Log.d(TAG, "loadProjectTree: $result")
Log.d(TAG, "loadProjectTree errorResult: $errorResult")
} catch (e: Exception) {
Log.d(TAG, "loadProjectTree: Exception " + e.message)
e.printStackTrace()
}
}
我們看呼叫后的結果:
loadProjectTree: Exception HTTP 404 Not Found
由于故意將loadProjectTreeError介面中的path寫錯,執行流程理所當然的走進了catch里,報了404的錯誤,loadProjectTree和loadProjectTreeError兩個介面其實一個是成功,一個是失敗,但當兩個介面放在同一個trycatch塊中,只要有一個失敗,另外的請求即使是成功的,也不再執行,
如果我們想要彼此介面不影響,則需要為每個介面單獨設立try-catch塊,如下:
suspend fun loadProjectTree() {
try {
val errorResult = service.loadProjectTreeError()
Log.d(TAG, "loadProjectTree errorResult: $errorResult")
} catch (e: Exception) {
Log.d(TAG, "loadProjectTree: error Exception " + e.message)
e.printStackTrace()
}
try {
val result = service.loadProjectTree()
Log.d(TAG, "loadProjectTree: $result")
} catch (e: Exception) {
Log.d(TAG, "loadProjectTree: Exception " + e.message)
e.printStackTrace()
}
}
我們將loadProjectTreeError和loadProjectTree分別使用try-catch塊進行例外捕獲,運行結果也正如預期,介面請求互相不影響:
loadProjectTree: error Exception HTTP 404 Not Found
loadProjectTree: com.fuusy.common.network.BaseResp@57e153d
上述為一般狀況下處理協程例外的方法,但是在某些情況下,try-catch卻也存在捕獲不到例外的可能,
1.2 什么情況下try-catch會無效?
正常來說,try-catch塊中只有代碼塊存在例外,都將被捕獲到catch中,但是協程中的例外卻存在特殊情況,
例如在協程中開啟一個失敗的子協程,則無法捕獲,還是以上面的介面舉個例子:
fun loadProjectTree() {
viewModelScope.launch() {
try {
//子協程
launch {
//失敗的介面
service.loadProjectTreeError()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
在try-catch塊中創建了一個子協程,呼叫了一個百分百會失敗的介面,這個時候我們期望的是能將例外捕獲至catch中,但是真正運行后卻發現App崩潰退出了,這也驗證了try-catch作用無效,
至于try-catch為什么在協程中開啟一個失敗的子協程的情況下會失敗?這就不得不提到一個新的知識點,協程的結構化并發,
1.3 什么是協程的結構化并發?
在kotlin的協程中,全域的GlobalScope是一個作用域,每個協程自身也是一個作用域,新建的協程與它的父作用域存在一個級聯的關系,也就是一個父子關系層次結構,而這級聯關系主要在于:
-
父作用域的生命周期持續到所有子作用域執行完; -
當結束父作用域結束時,同時結束它的各個子作用域; -
子作用域未捕獲到的例外將不會被重新拋出,而是一級一級向父作用域傳遞,這種例外傳播將導致父父作用域失敗,進而導致其子作用域的所有請求被取消,
上面的三點也就是協程結構化并發的特性,
了解了什么是協程的結構化并發,那我們就又回到try-catch為什么在協程中開啟一個失敗的子協程的情況下會失敗?的問題上,很顯然,上面第3點就是這個問題的答案,子協程中未捕獲的例外不會被重新拋出,而是在父子層次結構中向上傳播,這種例外傳播將導致父Job失敗,
在這種情況下,我們就應該使用一個新的處理例外的方法:CoroutineExceptionHandler
二、CoroutineExceptionHandler全域捕獲例外
除了try-catch外,協程處理例外的第二個方法是使用CoroutineExceptionHandler,
2.1 CoroutineExceptionHandler的介紹
首先我們來了解一下什么是CoroutineExceptionHandler?
CoroutineExceptionHandler是用于全域“捕獲所有”行為的最后一種機制,您無法在CoroutineExceptionHandler中從例外中恢復,當處理程式被呼叫時,協程已經完成了相應的例外,通常,該處理程式用于記錄例外、顯示某種錯誤訊息、終止和/或重新啟動應用程式,
這是官方檔案中對CoroutineExceptionHandler的解釋,起初時我對于這個解釋是讀不懂的,后面仔細想了想,CoroutineExceptionHandler作為一個全域捕獲例外的方式,是由于協程結構化并發的特性的存在,子作用域的例外經過一級一級的傳遞,最后由CoroutineExceptionHandler進行處理,因為傳遞到CoroutineExceptionHandler時已經到達頂層作用域,這種情況下,子協程已經結束,也就是說在CoroutineExceptionHandler被呼叫時,所有子協程已經完成了相應的例外,
2.2 CoroutineExceptionHandler的使用
首先,我們在ViewModel中創建了一個exceptionHandler,
private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
Log.d(TAG, "CoroutineExceptionHandler exception : ${exception.message}")
}
接著將exceptionHandler附加給viewModelScope,
fun loadProjectTree() {
viewModelScope.launch(exceptionHandler) {
//失敗的介面
service.loadProjectTreeError()
}
}
根據協程的結構化并發的特性,當根協程通過launch{}啟動時,例外將被傳遞給已附加的CoroutineExceptionHandler,
2.3 CoroutineExceptionHandler的不足
協程中不使用try-catch,CoroutineExceptionHandler作為全域捕獲例外的機制,最后例外會在CoroutineExceptionHandler中處理,但是有兩點需要注意:
由于沒有try-catch來捕獲住例外,例外會向上傳播,直到它到達RootScope或SupervisorJob,根據協程的結構化并發的特性,例外向上傳播時,父協程會失敗,同時父協程所級聯的子協程和兄弟協程也都會失敗;
如果你想并行請求多個介面,并且需要他們彼此不影響任務的執行,也就是任何一個介面例外,其他任務將繼續執行,那么CoroutineExceptionHandler不是一個很好的選擇,而接下來說的supervisorScope更適合這種情況,
CoroutineExceptionHandler的作用在于全域捕獲例外,CoroutineExceptionHandler無法在代碼的特定部分處理例外,例如針對某一個失敗介面,無法在例外后進行重試或者其他特定操作,
如果你想在特定部分做例外處理的話,try-catch更適合,
三、SupervisorScope+async
上面2.3提到了CoroutineExceptionHandler的一個缺陷:子協程出現例外,父協程和其兄弟協程也都會跟著執行失敗,
針對此問題,kotlin協程中提出了另一個協程作用域:SupervisorScope
該作用域與async結合開啟協程時,子協程出現了例外,并不會影響其父協程以及其兄弟協程,所以更適合多個并行任務的執行,
舉個例子:
viewModelScope.launch() {
supervisorScope {
try {
//除數為0,拋例外
val deferredFail = async { 2 / 0 }
//成功
val deferred = async {
2 / 1
Log.d(TAG, "loadProjectTree: 2/1 ")
}
deferredFail.await()
deferred.await()
} catch (e: Exception) {
Log.d(TAG, "loadProjectTree:Exception ${e.message} ")
}
}
}
在supervisorScope作用域里包裹這兩個除法運算,一個除數為0,必定會拋出一個例外,另一個則模擬成功狀態,使用async分別開啟一個協程,運行后的結果如下:

從結果上看,除數為0時,拋出了例外,同時另外一個兄弟協程也能夠正常運行
四、結論
- 在代碼的特定部分處理例外,可使用try-catch;
- 全域捕獲例外,并且其中一個任務例外,其他任務不執行,可使用CoroutineExceptionHandler,節省資源消耗;
- 并行任務間互不干擾,任何一個任務失敗,其他任務照常運行,可使用SupervisorScope+async模式,
最后
我整理一些Android 開發相關的學習檔案、面試題,希望能幫助到大家學習提升,如有需要參考的可以點擊鏈接領取哦點擊這里免費領取


轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/287760.html
標籤:其他
上一篇:【327頁】超全的 Android 面試進階題庫!(包含Flutter、Kotlin、性能優化、Jetpack、RxJava...)
下一篇:來深入了解一波鴻蒙開發
