前言
協(xié)程的使用中對異常的處理是非常抽象的一個過程笛坦,google了很多文檔仔拟,在官方文檔中對異常的處理并沒有講的很詳細(xì)姆泻,編寫過程中踩的坑似乎也沒有官方文檔的說明與解釋咒唆,網(wǎng)上也有很對對異常的處理文獻(xiàn),但是看過之后發(fā)現(xiàn)都是零零散散茶鹃,而且很多案例都是沒經(jīng)過代碼推敲的,甚至有些文獻(xiàn)里面的理解是錯誤的艰亮,所以奔著開發(fā)的理念仔細(xì)研究了一下協(xié)程的異常處理闭翩,以便更多的朋友看到這篇文章能帶來更好的理解,也對封裝框架設(shè)計有很大的幫助迄埃,以下案例均可以拷貝到編譯器進(jìn)行自行驗證疗韵,如有理解不對的地方歡迎私信我進(jìn)行交流學(xué)習(xí)并改正
概念
Try Catch能捕獲所有的異常嗎?
答案是不能侄非,簡單的舉例說明:
- 情況一:如果程序發(fā)生了異常并沒有進(jìn)行拋出蕉汪,這個時候會捕獲不到異常
- 情況二:在java中如果程序拋出的是錯誤,而不是異常這種情況視捕獲的代碼形態(tài)決定能否捕獲到異常
- 情況三:比如動態(tài)鏈接庫的加載錯誤逞怨,以及部分系統(tǒng)錯誤引起的異常不一定能捕獲到
協(xié)程異常了怎么辦者疤?
當(dāng)一個協(xié)程發(fā)生了異常,它將把異常傳播給它的父協(xié)程叠赦,父協(xié)程會做以下幾件事:
- 取消其他子協(xié)程
- 取消自己
- 將異常傳播給自己的父協(xié)程
所以要理解協(xié)程異常的處理需要弄清楚下面幾個關(guān)鍵點:
- try-catch捕獲異常
- CoroutineExceptionHandler
- supervisorScope 和SupervisorJob
程序示例
看下面的代碼
fun test() {
try {
Thread() {
throw NullPointerException()
}.start()
} catch (e: Exception) {
e.printStackTrace()
}
}
結(jié)果是:運行崩潰
這里如果有朋友覺得很不可思議的話可以進(jìn)行自我測試,為什么try-catch中開啟代碼還是會崩潰呢驹马?
==答案是try-catch 只能捕捉當(dāng)前線程的堆棧信息。對于非當(dāng)前線程無法實現(xiàn)捕捉==
既然這樣下面代碼應(yīng)該會被捕捉到:
fun test() = runBlocking(Dispatchers.IO) {
try {
launch {
throw NullPointerException()
}
} catch (e: Exception) {
e.printStackTrace()
Log.d("wangxuyang", "" + e.message)
}
}
結(jié)果是:運行崩潰
what f***?,這個協(xié)程是在當(dāng)前線程開啟的,并進(jìn)行了try-catch為什么還是會崩潰呢糯累?
這里直接告訴結(jié)論是:==launch啟動的根協(xié)程算利,是不會傳播異常的==
什么叫傳播異常?
傳播異常泳姐,是指能夠?qū)惓V鲃油鈷伒絾禹攲訁f(xié)程所在的線程效拭。因為launch啟動的協(xié)程,是不會將異常拋到線程胖秒,所以try-catch無法捕捉缎患,為了讓這種異常能夠捕捉到。協(xié)程引入了CoroutineExceptionHandler
啟動協(xié)程還有一種方式是async扒怖,那這種會不會向線程拋出異常呢较锡?代碼運行如下:
private val job: Job = Job()
private val scope = CoroutineScope(Dispatchers.Default + job)
private fun doWork(): Deferred<String> = scope.async { throw NullPointerException("自定義空指針異常") }
private fun loadData() = scope.launch {
try {
doWork().await()
} catch (e: Exception) {
Log.d("try catch捕獲的異常:", e.toString())
}
}
結(jié)果是:運行不會崩潰
代碼中try-catch住的代碼是:
doWork().await()
結(jié)論:==雖然向外拋出了異常,但是是在調(diào)用await()方法后拋出的盗痒,并且當(dāng)async作為根協(xié)程時蚂蕴,被封裝到deferred對象中的異常才會在調(diào)用await時拋出,并且這個異常是可以被try-catch捕獲住的==
上面說到根協(xié)程并且這個根協(xié)程是調(diào)用了await()拋出異常俯邓,其實這里是一個大坑骡楼,筆者在測試過程中感到也很神奇,接下來看這段代碼:
private val job0: Job = Job()
private val scope0 = CoroutineScope(Dispatchers.Default + job0)
private fun loadData0() = scope0.launch {
val asy = async {
Log.d("async 異常:", "開始準(zhǔn)備拋出異常")
delay(1000)
throw NullPointerException("自定義空指針異常")
}
try {
asy.await()
} catch (e: Exception) {
Log.d("async 異常: 捕獲的異常-", e.toString())
}
Log.d("async 異常:", "繼續(xù)執(zhí)行后續(xù)代碼")
}
運行結(jié)果是:程序崩潰
2022-03-22 19:51:02.074 25864-25903/com.example.coroutinestest D/async 異常:: 開始準(zhǔn)備拋出異常
2022-03-22 19:51:03.085 25864-25905/com.example.coroutinestest D/async 異常: 捕獲的異常-: java.lang.NullPointerException: 自定義空指針異常
2022-03-22 19:51:03.085 25864-25905/com.example.coroutinestest D/async 異常:: 繼續(xù)執(zhí)行后續(xù)代碼
乍一看稽鞭,跟上面代碼的邏輯走勢一樣鸟整,也是調(diào)用了await方法,也是try-catch了這個方法 朦蕴,打的日志也是捕獲到了篮条,是正常的流程啊
但是我告訴大家這里并不是調(diào)用await方法后才拋出的異常,只是崩潰后這個異常被捕獲到了而已吩抓,是不是大家要覺得我很菜涉茧?可以這樣來印證這個猜想,講await方法屏蔽掉疹娶,再運行這個方法:
try {
// asy.await()
} catch (e: Exception) {
Log.d("async 異常: 捕獲的異常-", e.toString())
}
Log.d("async 異常:", "繼續(xù)執(zhí)行后續(xù)代碼")
結(jié)果是:程序崩潰伴栓,日志如下
//2022-03-22 19:55:05.460 26378-26415/com.example.coroutinestest D/async 異常:: 繼續(xù)執(zhí)行后續(xù)代碼
//2022-03-22 19:55:05.461 26378-26415/com.example.coroutinestest D/async 異常:: 開始準(zhǔn)備拋出異常
這里是不是印證了前面的猜想,崩潰原因其實不是在調(diào)用await方法之后引起的崩潰雨饺,是代碼執(zhí)行到 throw NullPointerException("自定義空指針異常")就拋出異常了钳垮,所以前面的結(jié)論是成立的
結(jié)論是:==async開啟一個根協(xié)程或者子協(xié)程,異常都會被拋出給線程额港,并且可以被try-catch捕獲到饺窿。async開啟一個根協(xié)程,在調(diào)用await方法時候會拋出異常锹安,這個異扯碳觯可以用try-catch捕獲不引起崩潰倚舀,如果這個協(xié)程不是根協(xié)程,那么是代碼執(zhí)行到 throw 異常的時候就拋出了異常與是否調(diào)用await方法無關(guān)這個異橙趟危可以用try-catch捕獲但是會引起崩潰痕貌,可以用CoroutineExceptionHandler進(jìn)行捕獲解決崩潰問題==
CoroutineExceptionHandler的應(yīng)用
上面印證了程序的崩潰與異常的拋出,但是這個異常怎么處理呢糠排?這里就用到了官方提供的CoroutineExceptionHandler了
/**
* Creates a [CoroutineExceptionHandler] instance.
* @param handler a function which handles exception thrown by a coroutine
*/
==CoroutineExceptionHandler的官方解釋是:處理協(xié)程拋出的異常的函數(shù)舵稠,官方又一個隱藏點沒說就是這個CoroutineExceptionHandler只能處理當(dāng)前域內(nèi)開啟的子協(xié)程或者當(dāng)前協(xié)程拋出的異常==
所以解決上訴不是根協(xié)程引起的崩潰問題可以采用這樣的方式:
private val coroutineExceptionHandler = CoroutineExceptionHandler { _, _ ->
Log.d("async 異常:", "異常被內(nèi)部CoroutineExceptionHandler處理掉了")
}
private fun loadData0() = scope0.launch(coroutineExceptionHandler) {
val asy = async {
Log.d("async 異常:", "開始準(zhǔn)備拋出異常")
delay(1000)
throw NullPointerException("自定義空指針異常")
}
try {
asy.await()
} catch (e: Exception) {
Log.d("async 異常: 捕獲的異常-", e.toString())
}
Log.d("async 異常:", "繼續(xù)執(zhí)行后續(xù)代碼")
}
運行結(jié)果:不會崩潰,日志如下
2022-03-22 20:02:31.121 27083-27166/com.example.coroutinestest D/async 異常:: 開始準(zhǔn)備拋出異常
2022-03-22 20:02:32.134 27083-27167/com.example.coroutinestest D/async 異常: 捕獲的異常-: java.lang.NullPointerException: 自定義空指針異常
2022-03-22 20:02:32.134 27083-27167/com.example.coroutinestest D/async 異常:: 繼續(xù)執(zhí)行后續(xù)代碼
2022-03-22 20:02:32.135 27083-27166/com.example.coroutinestest D/async 異常:: 異常被內(nèi)部CoroutineExceptionHandler處理掉了
看到了代碼即使是拋出了異常入宦,但是被內(nèi)部消耗了哺徊,并缺不會引起程序崩潰
前面提到了launch啟動的根協(xié)程,是不會傳播異常的
這里我們繼續(xù)印證這個結(jié)論:
例子1:
private fun loadData1() = scope1.launch {
try {
throw NullPointerException("自定義空指針異常")
} catch (e: Exception) {
Log.d("try catch捕獲的異常:", e.toString())
}
}
結(jié)果:不會崩潰
例子2:
private fun loadData1() = try {
scope1.launch {
throw NullPointerException("自定義空指針異常")
}
} catch (e: Exception) {
Log.d("try catch捕獲的異常:", e.toString())
}
結(jié)果:會崩潰
例子3:
private fun doWork1() = scope1.launch { throw NullPointerException("自定義空指針異常") }
private fun loadData1() = scope1.launch {
try {
doWork1()
} catch (e: Exception) {
Log.d("try catch捕獲的異常:", e.toString())
}
}
結(jié)果:會崩潰
==從例1與例2可以看出異常在協(xié)程內(nèi)部可以被捕獲乾闰,但是在外部不能被捕獲落追,這里印證了launch不向外拋出異常的結(jié)論,再從例3與例1對比可以看出這個協(xié)程涯肩,這個協(xié)程并不是只有根協(xié)程才不向線程拋出異常轿钠,而是只要launch開啟的協(xié)程,無論是根還是子都不會向線程中拋出異常==
同樣可以使用上訴方法來解決這個崩潰問題:
private val job2: Job = Job()
private val scope2 = CoroutineScope(Dispatchers.Default + job2)
private fun loadData2() = scope2.launch(CoroutineExceptionHandler { _, exception ->
{
Log.d("Handler捕獲的異常", exception.toString())
}
}) {
try {
//無論launch有幾層都不會崩潰
launch { launch { throw NullPointerException("自定義空指針異常") } }
} catch (e: Exception) {
Log.d("try catch捕獲的異常:", e.toString())
}
}
再來印證前面所說的:CoroutineExceptionHandler只能處理當(dāng)前域內(nèi)開啟的子協(xié)程或者當(dāng)前協(xié)程拋出的異常
運行下面的代碼:
private val job3: Job = Job()
private val scope3 = CoroutineScope(Dispatchers.Default + job3)
private fun doWork3() = scope3.launch { throw NullPointerException("自定義空指針異常") }
private fun loadData3() = scope3.launch(CoroutineExceptionHandler { _, exception ->
{
Log.d("Handler捕獲的異常", exception.toString())
}
}) {
try {
doWork3()
} catch (e: Exception) {
Log.d("try catch捕獲的異常:", e.toString())
}
}
結(jié)果是:崩潰
因為doWork3方法開啟的協(xié)程不是在當(dāng)前域下開啟的協(xié)程而是scope3開啟的病苗,只是在當(dāng)前域下運行而已疗垛,這里就印證了上面的說法
但是可以通過增加一個CoroutineExceptionHandler來解決上面的問題,代碼如下:
private val job4: Job = Job()
private val scope4 =
CoroutineScope(Dispatchers.Default + job4 + CoroutineExceptionHandler { _, exception ->
{
Log.d("Handler捕獲的異常", exception.toString())
}
})
//無論launch有幾層都不會崩潰
private fun doWork4() = scope4.launch { launch { throw NullPointerException("自定義空指針異常") } }
private fun loadData4() = scope4.launch {
try {
doWork4()
} catch (e: Exception) {
Log.d("try catch捕獲的異常:", e.toString())
}
}
結(jié)果是:不會崩潰
supervisorScope 和 SupervisorJob
前面講到了CoroutineExceptionHandler可以捕獲異常并且處理掉異常硫朦,程序不會崩潰贷腕,這里還有一種方式就是使用supervisorScope 和 SupervisorJob
supervisorScope 和 SupervisorJob的原理是:將異常不傳播給自己的父協(xié)程
首先我們來看一個例子:
private val handler7 = CoroutineExceptionHandler { _, _ ->
Log.d("kobe", "CoroutineExceptionHandler")
}
private fun coroutineBuildRunBlock7() = runBlocking(Dispatchers.IO) {
CoroutineScope(Job() + handler7)
.launch {
launch {
Log.d("kobe", "start job1 delay")
delay(1000)
Log.d("kobe", "end job1 delay")
}
launch {
Log.d("kobe", "job2 throw execption")
throw NullPointerException()
}
}
}
結(jié)果是:不崩潰,日志如下
2022-03-22 15:24:34.022 20373-20411/com.example.coroutinestest D/kobe: start job1 delay
2022-03-22 15:24:34.025 20373-20412/com.example.coroutinestest D/kobe: job2 throw execption
2022-03-22 15:24:34.029 20373-20412/com.example.coroutinestest D/kobe: CoroutineExceptionHandler
看到一個現(xiàn)象就是:子協(xié)程崩潰會引起兄弟協(xié)程的執(zhí)行錯誤咬展,這就是文章前面所說的取消其他子協(xié)程泽裳,這當(dāng)然不是我們想看到的情況,互不影響才是最優(yōu)解破婆,所以有了下面的方法:
private val handler8 = CoroutineExceptionHandler { _, _ ->
Log.d("kobe", "CoroutineExceptionHandler")
}
private fun coroutineBuildRunBlock8() = runBlocking(Dispatchers.IO) {
CoroutineScope(Job() + handler8)
.launch {
launch {
delay(2000)
Log.d("kobe", "start job3 delay")
}
supervisorScope {
launch {
Log.d("kobe", "start job1 delay")
delay(1000)
Log.d("kobe", "end job1 delay")
}
launch {
Log.d("kobe", "job2 throw execption")
throw NullPointerException()
}
}
}
}
結(jié)果是:不崩潰诡壁,日志如下
2022-03-22 15:48:07.384 21777-21818/com.example.coroutinestest D/kobe: start job1 delay
2022-03-22 15:48:07.384 21777-21820/com.example.coroutinestest D/kobe: job2 throw execption
2022-03-22 15:48:07.385 21777-21820/com.example.coroutinestest D/kobe: CoroutineExceptionHandler
2022-03-22 15:48:08.391 21777-21818/com.example.coroutinestest D/kobe: end job1 delay
2022-03-22 15:48:09.389 21777-21818/com.example.coroutinestest D/kobe: start job3 delay
按照前面的邏輯異常捕獲了,使用了supervisorScope所以一個子協(xié)程的異常不會會影響另一個子協(xié)程的運行,并且不會影響這個域外的兄弟協(xié)程荠割,所以日志全
所以supervisorScope中開啟協(xié)程,無論多少個子協(xié)程都互不影響旺矾,這是我們想要的處理情況
那我們再來看下SupervisorJob蔑鹦,運行下面代碼:
private val supervisorJob9 = SupervisorJob()
private val handler9 = CoroutineExceptionHandler { _, _ ->
Log.d("kobe", "CoroutineExceptionHandler")
}
private val handler99 = CoroutineExceptionHandler { _, _ ->
Log.d("kobe", "頂層異常處理")
}
private fun coroutineBuildRunBlock9() = runBlocking(Dispatchers.IO) {
CoroutineScope(handler99 ).launch {
CoroutineScope( handler9+supervisorJob9)
.launch {
launch {
Log.d("kobe", "start job1 delay")
delay(1000)
Log.d("kobe", "end job1 delay")
}
launch {
Log.d("kobe", "job2 throw execption")
throw NullPointerException()
}
}
}
}
結(jié)果是:不會崩潰,日志如下
2022-03-23 17:32:25.771 8593-8638/com.example.coroutinestest D/kobe: job2 throw execption
2022-03-23 17:32:25.772 8593-8642/com.example.coroutinestest D/kobe: start job1 delay
2022-03-23 17:32:25.785 8593-8642/com.example.coroutinestest D/kobe: CoroutineExceptionHandler
我們這次來分析日志箕宙,日志中沒有“頂層異常處理”所以這個異澈啃啵肯定就沒有傳播出去,也沒有打出“end job1 delay”來表示影響了這個協(xié)程內(nèi)部的兄弟協(xié)程
所以結(jié)論是: ==SupervisorJob這個任務(wù)是阻止異常不會向外傳播柬帕,因此不會影響其父親/兄弟協(xié)程哟忍,也不會被其兄弟協(xié)程拋出的異常影響狡门,但是他內(nèi)部生成的各種協(xié)程是依然會像job一樣互相影響,并且這個異常必須使用CoroutineExceptionHandler處理掉锅很,不然會引起程序崩潰==
看到這里可能又有人會問這個很正常其馏,因為異常被handler9處理掉了,所以就沒有傳遞到父親協(xié)程爆安,那這里我們可以這樣處理叛复,我們?nèi)サ暨@個handler9:
private fun coroutineBuildRunBlock9() = runBlocking(Dispatchers.IO) {
CoroutineScope(handler99 ).launch {
CoroutineScope(supervisorJob9)
.launch {
launch {
Log.d("kobe", "start job1 delay")
delay(1000)
Log.d("kobe", "end job1 delay")
}
launch {
Log.d("kobe", "job2 throw execption")
throw NullPointerException()
}
}
}
}
結(jié)果:程序崩潰,并且沒有打印出“頂層異常處理”扔仓,所以前面的結(jié)論是正確的
我們再來印證以下兄弟協(xié)程是否被影響褐奥,運行代碼:
private val supervisorJob10 = SupervisorJob()
private val handler10 = CoroutineExceptionHandler { _, _ ->
Log.d("kobe", "CoroutineExceptionHandler")
}
private val coroutineContext10 = handler10 + supervisorJob10
private fun coroutineBuildRunBlock10() = runBlocking(Dispatchers.IO) {
CoroutineScope(coroutineContext10)
.launch {
launch {
Log.d("kobe", "start job1 delay")
delay(1000)
Log.d("kobe", "end job1 delay")
}
launch {
Log.d("kobe", "start job2 delay")
delay(1000)
Log.d("kobe", "end job2 delay")
}
CoroutineScope(coroutineContext10).launch {
launch {
Log.d("kobe", "start job3 delay")
delay(1000)
Log.d("kobe", "end job3 delay")
}
launch {
Log.d("kobe", "job4 throw execption")
throw NullPointerException()
}
}
}
}
結(jié)果是:不會崩潰,日志如下
2022-03-22 15:45:20.807 21611-21653/com.example.coroutinestest D/kobe: start job1 delay
2022-03-22 15:45:20.809 21611-21652/com.example.coroutinestest D/kobe: start job2 delay
2022-03-22 15:45:20.814 21611-21651/com.example.coroutinestest D/kobe: start job3 delay
2022-03-22 15:45:20.815 21611-21654/com.example.coroutinestest D/kobe: job4 throw execption
2022-03-22 15:45:20.817 21611-21654/com.example.coroutinestest D/kobe: CoroutineExceptionHandler
2022-03-22 15:45:21.820 21611-21654/com.example.coroutinestest D/kobe: end job1 delay
2022-03-22 15:45:21.820 21611-21651/com.example.coroutinestest D/kobe: end job2 delay
結(jié)果是:兄弟協(xié)程并不影響翘簇,前面的結(jié)論正確
結(jié)論
**1. try-catch 只能捕捉當(dāng)前線程的堆棧信息撬码。對于非當(dāng)前線程無法實現(xiàn)捕捉
- launch啟動的根協(xié)程,是不會傳播異常的
- async開啟一個根協(xié)程或者子協(xié)程版保,異常都會被拋出給線程呜笑,并且可以被try-catch捕獲到。async開啟一個根協(xié)程找筝,在調(diào)用await方法時候會拋出異常蹈垢,這個異常可以用try-catch捕獲不引起崩潰袖裕,如果這個協(xié)程不是根協(xié)程曹抬,那么是代碼執(zhí)行到 throw 異常的時候就拋出了異常與是否調(diào)用await方法無關(guān)這個異常可以用try-catch捕獲但是會引起崩潰急鳄,可以用CoroutineExceptionHandler進(jìn)行捕獲解決崩潰問題
- CoroutineExceptionHandler的官方解釋是:處理協(xié)程拋出的異常的函數(shù)谤民,官方又一個隱藏點沒說就是這個CoroutineExceptionHandler只能處理當(dāng)前域內(nèi)開啟的子協(xié)程或者當(dāng)前協(xié)程拋出的異常
- SupervisorJob這個任務(wù)是阻止異常不會向外傳播,因此不會影響其父親/兄弟協(xié)程疾宏,也不會被其兄弟協(xié)程拋出的異常影響张足,但是他內(nèi)部生成的各種協(xié)程是依然會像job一樣互相影響,并且這個異常必須使用CoroutineExceptionHandler處理掉坎藐,不然會引起程序崩潰**
最后
協(xié)程的異常處理是很復(fù)雜的一個過程为牍,里面融合了結(jié)構(gòu)化并發(fā)的思想,這個開發(fā)思想伴隨了kotlin的后續(xù)開發(fā)岩馍,并且協(xié)程的異常處理中有很多坑需要一一去踩碉咆,在官方文檔與網(wǎng)上的零散碎片知識中很難找到這些坑點,如果能認(rèn)真看完上訴的講解蛀恩,肯定對協(xié)程的異常有了一個新的認(rèn)知疫铜,更希望讀者將上面的案例放在自己的代碼中去運行總結(jié),若有不對的地方歡迎指出改正