全網(wǎng)最詳細的Kotlin協(xié)程-異常篇講解與踩坑

前言

協(xié)程的使用中對異常的處理是非常抽象的一個過程,google了很多文檔,在官方文檔中對異常的處理并沒有講的很詳細,編寫過程中踩的坑似乎也沒有官方文檔的說明與解釋,網(wǎng)上也有很對對異常的處理文獻,但是看過之后發(fā)現(xiàn)都是零零散散,而且很多案例都是沒經(jīng)過代碼推敲的,甚至有些文獻里面的理解是錯誤的,所以奔著開發(fā)的理念仔細研究了一下協(xié)程的異常處理,以便更多的朋友看到這篇文章能帶來更好的理解,也對封裝框架設計有很大的幫助,以下案例均可以拷貝到編譯器進行自行驗證,如有理解不對的地方歡迎私信我進行交流學習并改正

概念

Try Catch能捕獲所有的異常嗎?

答案是不能,簡單的舉例說明:

  • 情況一:如果程序發(fā)生了異常并沒有進行拋出,這個時候會捕獲不到異常
  • 情況二:在java中如果程序拋出的是錯誤,而不是異常這種情況視捕獲的代碼形態(tài)決定能否捕獲到異常
  • 情況三:比如動態(tài)鏈接庫的加載錯誤,以及部分系統(tǒng)錯誤引起的異常不一定能捕獲到

協(xié)程異常了怎么辦?

當一個協(xié)程發(fā)生了異常,它將把異常傳播給它的父協(xié)程,父協(xié)程會做以下幾件事:

  1. 取消其他子協(xié)程
  2. 取消自己
  3. 將異常傳播給自己的父協(xié)程

所以要理解協(xié)程異常的處理需要弄清楚下面幾個關鍵點:

  • try-catch捕獲異常
  • CoroutineExceptionHandler
  • supervisorScope 和SupervisorJob

程序示例

看下面的代碼

  fun test() {
        try {
            Thread() {
                throw NullPointerException()
            }.start()
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

結果是:運行崩潰

這里如果有朋友覺得很不可思議的話可以進行自我測試,為什么try-catch中開啟代碼還是會崩潰呢?

==答案是try-catch 只能捕捉當前線程的堆棧信息。對于非當前線程無法實現(xiàn)捕捉==

既然這樣下面代碼應該會被捕捉到:

fun test() = runBlocking(Dispatchers.IO) {
        try {
            launch {
                throw NullPointerException()
            }
        } catch (e: Exception) {
            e.printStackTrace()
            Log.d("wangxuyang", "" + e.message)
        }
    }

結果是:運行崩潰

what f***?,這個協(xié)程是在當前線程開啟的,并進行了try-catch為什么還是會崩潰呢?

這里直接告訴結論是:==launch啟動的根協(xié)程,是不會傳播異常的==


什么叫傳播異常?

傳播異常,是指能夠將異常主動往外拋到啟動頂層協(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())
        }
    }

結果是:運行不會崩潰

代碼中try-catch住的代碼是:

doWork().await()

結論:==雖然向外拋出了異常,但是是在調用await()方法后拋出的,并且當async作為根協(xié)程時,被封裝到deferred對象中的異常才會在調用await時拋出,并且這個異常是可以被try-catch捕獲住的==

上面說到根協(xié)程并且這個根協(xié)程是調用了await()拋出異常,其實這里是一個大坑,筆者在測試過程中感到也很神奇,接下來看這段代碼:

  private val job0: Job = Job()
  private val scope0 = CoroutineScope(Dispatchers.Default + job0)
  private fun loadData0() = scope0.launch {
        val asy = async {
            Log.d("async 異常:", "開始準備拋出異常")
            delay(1000)
            throw NullPointerException("自定義空指針異常")
        }
        try {
            asy.await()
        } catch (e: Exception) {
            Log.d("async 異常: 捕獲的異常-", e.toString())
        }
        Log.d("async 異常:", "繼續(xù)執(zhí)行后續(xù)代碼")
    }

運行結果是:程序崩潰

2022-03-22 19:51:02.074 25864-25903/com.example.coroutinestest D/async 異常:: 開始準備拋出異常
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ù)代碼

乍一看,跟上面代碼的邏輯走勢一樣,也是調用了await方法,也是try-catch了這個方法 ,打的日志也是捕獲到了,是正常的流程啊
但是我告訴大家這里并不是調用await方法后才拋出的異常,只是崩潰后這個異常被捕獲到了而已,是不是大家要覺得我很菜?可以這樣來印證這個猜想,講await方法屏蔽掉,再運行這個方法:

  try {
          // asy.await()
      } catch (e: Exception) {
            Log.d("async 異常: 捕獲的異常-", e.toString())
      }
        Log.d("async 異常:", "繼續(xù)執(zhí)行后續(xù)代碼")

結果是:程序崩潰,日志如下

 //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 異常:: 開始準備拋出異常

這里是不是印證了前面的猜想,崩潰原因其實不是在調用await方法之后引起的崩潰,是代碼執(zhí)行到 throw NullPointerException("自定義空指針異常")就拋出異常了,所以前面的結論是成立的

結論是:==async開啟一個根協(xié)程或者子協(xié)程,異常都會被拋出給線程,并且可以被try-catch捕獲到。async開啟一個根協(xié)程,在調用await方法時候會拋出異常,這個異??梢杂胻ry-catch捕獲不引起崩潰,如果這個協(xié)程不是根協(xié)程,那么是代碼執(zhí)行到 throw 異常的時候就拋出了異常與是否調用await方法無關這個異??梢杂胻ry-catch捕獲但是會引起崩潰,可以用CoroutineExceptionHandler進行捕獲解決崩潰問題==

CoroutineExceptionHandler的應用

上面印證了程序的崩潰與異常的拋出,但是這個異常怎么處理呢?這里就用到了官方提供的CoroutineExceptionHandler了

/**
 * Creates a [CoroutineExceptionHandler] instance.
 * @param handler a function which handles exception thrown by a coroutine
 */

==CoroutineExceptionHandler的官方解釋是:處理協(xié)程拋出的異常的函數(shù),官方又一個隱藏點沒說就是這個CoroutineExceptionHandler只能處理當前域內開啟的子協(xié)程或者當前協(xié)程拋出的異常==

所以解決上訴不是根協(xié)程引起的崩潰問題可以采用這樣的方式:

 private val coroutineExceptionHandler = CoroutineExceptionHandler { _, _ ->
        Log.d("async 異常:", "異常被內部CoroutineExceptionHandler處理掉了")
    }

    private fun loadData0() = scope0.launch(coroutineExceptionHandler) {
        val asy = async {
            Log.d("async 異常:", "開始準備拋出異常")
            delay(1000)
            throw NullPointerException("自定義空指針異常")
        }
        try {
            asy.await()
        } catch (e: Exception) {
            Log.d("async 異常: 捕獲的異常-", e.toString())
        }
        Log.d("async 異常:", "繼續(xù)執(zhí)行后續(xù)代碼")
    }

運行結果:不會崩潰,日志如下

    2022-03-22 20:02:31.121 27083-27166/com.example.coroutinestest D/async 異常:: 開始準備拋出異常
    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 異常:: 異常被內部CoroutineExceptionHandler處理掉了

看到了代碼即使是拋出了異常,但是被內部消耗了,并缺不會引起程序崩潰

前面提到了launch啟動的根協(xié)程,是不會傳播異常的

這里我們繼續(xù)印證這個結論:

例子1:

   private fun loadData1() = scope1.launch {
        try {
            throw NullPointerException("自定義空指針異常")
        } catch (e: Exception) {
            Log.d("try catch捕獲的異常:", e.toString())
        }
    }


結果:不會崩潰

例子2:

 private fun loadData1() = try {
        scope1.launch {
            throw NullPointerException("自定義空指針異常")
        }
    } catch (e: Exception) {
        Log.d("try catch捕獲的異常:", e.toString())
    }
    

結果:會崩潰

例子3:

    private fun doWork1() = scope1.launch { throw NullPointerException("自定義空指針異常") }


    private fun loadData1() = scope1.launch {
        try {
            doWork1()
        } catch (e: Exception) {
            Log.d("try catch捕獲的異常:", e.toString())
        }
    }
    
結果:會崩潰

==從例1與例2可以看出異常在協(xié)程內部可以被捕獲,但是在外部不能被捕獲,這里印證了launch不向外拋出異常的結論,再從例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只能處理當前域內開啟的子協(xié)程或者當前協(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())
        }
    }

結果是:崩潰
因為doWork3方法開啟的協(xié)程不是在當前域下開啟的協(xié)程而是scope3開啟的,只是在當前域下運行而已,這里就印證了上面的說法

但是可以通過增加一個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())
        }
    }

結果是:不會崩潰

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()
                }
            }
    }

結果是:不崩潰,日志如下

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é)程,這當然不是我們想看到的情況,互不影響才是最優(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()
                    }
                }
            }
    }

結果是:不崩潰,日志如下

 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()
                    }
                }
        }
    }


結果是:不會崩潰,日志如下

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é)程內部的兄弟協(xié)程

所以結論是: ==SupervisorJob這個任務是阻止異常不會向外傳播,因此不會影響其父親/兄弟協(xié)程,也不會被其兄弟協(xié)程拋出的異常影響,但是他內部生成的各種協(xié)程是依然會像job一樣互相影響,并且這個異常必須使用CoroutineExceptionHandler處理掉,不然會引起程序崩潰==

看到這里可能又有人會問這個很正常,因為異常被handler9處理掉了,所以就沒有傳遞到父親協(xié)程,那這里我們可以這樣處理,我們去掉這個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()
                    }
                }
        }
    }

結果:程序崩潰,并且沒有打印出“頂層異常處理”,所以前面的結論是正確的

我們再來印證以下兄弟協(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()
                    }
                }
            }
    }

結果是:不會崩潰,日志如下

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

結果是:兄弟協(xié)程并不影響,前面的結論正確

結論

**1. try-catch 只能捕捉當前線程的堆棧信息。對于非當前線程無法實現(xiàn)捕捉

  1. launch啟動的根協(xié)程,是不會傳播異常的
  2. async開啟一個根協(xié)程或者子協(xié)程,異常都會被拋出給線程,并且可以被try-catch捕獲到。async開啟一個根協(xié)程,在調用await方法時候會拋出異常,這個異??梢杂胻ry-catch捕獲不引起崩潰,如果這個協(xié)程不是根協(xié)程,那么是代碼執(zhí)行到 throw 異常的時候就拋出了異常與是否調用await方法無關這個異??梢杂胻ry-catch捕獲但是會引起崩潰,可以用CoroutineExceptionHandler進行捕獲解決崩潰問題
  3. CoroutineExceptionHandler的官方解釋是:處理協(xié)程拋出的異常的函數(shù),官方又一個隱藏點沒說就是這個CoroutineExceptionHandler只能處理當前域內開啟的子協(xié)程或者當前協(xié)程拋出的異常
  4. SupervisorJob這個任務是阻止異常不會向外傳播,因此不會影響其父親/兄弟協(xié)程,也不會被其兄弟協(xié)程拋出的異常影響,但是他內部生成的各種協(xié)程是依然會像job一樣互相影響,并且這個異常必須使用CoroutineExceptionHandler處理掉,不然會引起程序崩潰**

最后

協(xié)程的異常處理是很復雜的一個過程,里面融合了結構化并發(fā)的思想,這個開發(fā)思想伴隨了kotlin的后續(xù)開發(fā),并且協(xié)程的異常處理中有很多坑需要一一去踩,在官方文檔與網(wǎng)上的零散碎片知識中很難找到這些坑點,如果能認真看完上訴的講解,肯定對協(xié)程的異常有了一個新的認知,更希望讀者將上面的案例放在自己的代碼中去運行總結,若有不對的地方歡迎指出改正

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容