日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

kotlin?協程上下文異常處理詳解_Android

作者:aruba ? 更新時間: 2022-10-27 編程語言

引言

從前面我們可以大致了解了協程的玩法,如果一個協程中使用子協程,那么該協程會等待子協程執行結束后才真正退出,而達到這種效果的原因就是協程上下文,上下文貫穿了協程的生命周期,這套思想和我們app的上下文很像

在開始真正了解協程上下文之前,我們先來看看下面的例子

下面的圖代表了一個協程a的生命,就像一條從上至下的直線,它的生命只有100ms

當我們在a協程延遲函數100ms之前開啟一個子協程b,b做了200ms的事情,如果不考慮調度消耗的時間,那么a協程的生命也會延長成200ms

代碼驗證下:

fun `test context life`() = runBlocking {
    //定義一個作用域
    val a = CoroutineScope(Dispatchers.Default)
    val startTime = System.currentTimeMillis()
    //協程a開啟
    val jobA = a.launch {
        //子協程b開啟
        val jobB = launch {
            delay(200)
        }
        delay(100)
    }
    //等待協程a結束
    jobA.join()
    val endTime = System.currentTimeMillis()
    println(endTime - startTime)
}
fun main() {
    `test context life`()
}

結果:237

如果我們把子協程b增加到delay 300ms,那么結果也會相應的變為:

323

通過上面的列子,來對協程上下文的有一個初步概念:可以說協程的生命周期,就是上下文的生命周期

協程擁有很多新的概念,很多人一開始接觸就能難理解(包括我自己),這些概念都是在上下文的基礎上引申而來的,所以我一再強調它的重要性,協程的上下文必須理解透,才能玩好協程,接下來我們來真正了解協程上下文

一、協程上下文

1.CoroutineContext

協程上下文有以下幾項構成,它們都是實現了CoroutineContext.Element接口,有些是實現了AbstractCoroutineContextElement接口,而AbstractCoroutineContextElement繼承CoroutineContext.Element接口

1.Job:控制協程的生命周期,也是我們能拿到操作協程任務的唯一對象

2.CoroutineDispatcher:就是之前介紹的調度器

3.CoroutineName:協程的名字,一般輸出日志用的

4.CoroutineExceptionHandler:處理未捕獲的異常

協程上下文實現了運算符重載,我們可以用+號來組合一個CoroutineContext的元素

2.CorountineScope

一般情況下,協程體內所有的子協程,都繼承至根協程,協程的繼承的關系不是我們所了解的類的繼承關系,而是父協程和子協程的生命周期關系,還記得我們上面舉得例子么,除非在協程體內自己手動創建協程作用域,即:創建一個全新的協程上下文,我們之前已經介紹過了:

CorountineScope:創建協程作用域,新起線程,觀察源碼,內部實際實例化的是ContextScope,ContextScope被internal修飾,內部使用,我們實例化不了

其他的實際上都是繼承父協程上下文,或者內部實例化了ContextScope:

1.runBlocking:將主線程轉變為協程,會阻塞主線程,實際上用的是一個EmptyCoroutineContext作為上下文,它是一個主線程的協程上下文,靜態的全局變量,我們其實就可以理解成是主線程
2.GlobalScope:也是用的EmptyCoroutineContext
3.MainScope:使用ContextScope構造了新的上下文
4.coroutineScope:繼承的父協程上下文,不能算是全新的協程
等等

3.子協程繼承父協程

子協程繼承父協程時,除了Job會自動創建新的實例外,其他3項的不手動指定的話,都會自動繼承父協程的,Job對應的是協程任務,每次新的任務肯定都是新的Job對象

有了這些概念后,接下來通過代碼,再熟悉鞏固下

例子1:

fun `test context life1`() = runBlocking {
    //定義一個作用域
    val a = CoroutineScope(Dispatchers.Default)
    //協程a開啟
    val jobA = a.launch {
        delay(100)
        println("jobA finished")
    }
    println("main finished")
}

結果:
main finished

由于a是一個根協程,全新的上下文,runBlocking 是主線程的協程上下文,所以當a開啟任務時,不會阻塞主線程,當我們的進程都跑完了,jobA finished肯定不會打印了

例子2:

fun `test context life2`() = runBlocking {
    //定義一個作用域
    val a = CoroutineScope(Dispatchers.Default)
    //協程a開啟
    val jobA = a.launch {
        delay(100)
        println("jobA finished")
    }
    jobA.join()
    println("main finished")
}

結果:
jobA finished
main finished

我們在主協程(主線程的協程)中,手動調用jobA的join方法,那么主線程就會阻塞,直到jobA執行完畢。這個和我們的多線程操作是一樣的,主線程等待A線程執行完后再往后執行

例子3:

fun `test context life3`() = runBlocking {
    launch {
        delay(100)
        println("jobA finished")
    }
    println("main finished")
}

結果:
main finished
jobA finished

這回我們沒有構建新的協程作用域,而是在根協程中直接使用子協程的方式,當然了,協程的上下文繼承關系,使得我們的主協程等待子協程執行完畢后才結束生命

例子4:

fun `test context life4`() = runBlocking {
    launch(Dispatchers.IO + CoroutineName("jobA")) {
        delay(100)
        println("${coroutineContext[CoroutineName]}  finished")
    }
    println("main finished")
}

結果:
main finished
CoroutineName(jobA) finished

即使我們指定了子協程的調度器和協程名,也不會影響協程上下文繼承關系,主協程還是會等待子協程執行完畢后才結束生命

如果你已經完全理解了,那么就可以知道以上例子使用async啟動也是一樣的效果

二、協程的異常傳遞

1.協程的異常傳播

協程的異常傳播也是遵循了協程上下文的機制,除了取消異常(CancellationException)外,當一個協程有了異常,如果沒有主動捕獲異常,那么異常會向上傳播,直到根協程,子協程的異常都會導致根協程退出,自然其他子協程也會退出

例子1:

fun `test coroutineScope exception1`() = runBlocking {
    val job1 = launch {
        delay(2000)
        println("job finished")
    }
    val job2 = launch {
        delay(1000)
        println("job2 finished")
        throw IllegalArgumentException()
    }
    delay(3000)
    println("finished")
}

結果:

job2 finished
Exception in thread "main" java.lang.IllegalArgumentException
? ? at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt$test coroutineScope exception1$1$job2$1.invokeSuspend(exceptionTest.kt:46)
? ? at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
? ? at kotlinx.coroutines.ResumeModeKt.resumeMode(ResumeMode.kt:67)
? ? at kotlinx.coroutines.DispatchedKt.resume(Dispatched.kt:309)
? ? at kotlinx.coroutines.DispatchedKt.dispatch(Dispatched.kt:298)
? ? at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:250)
? ? at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:260)
? ? at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:332)
? ? at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.kt:298)
? ? at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.kt:116)
? ? at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:80)
? ? at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:54)
? ? at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
? ? at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:36)
? ? at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
? ? at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt.test coroutineScope exception1(exceptionTest.kt:37)
? ? at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt.main(exceptionTest.kt:54)
? ? at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt.main(exceptionTest.kt)

Process finished with exit code 1

job2 1000ms后就發生了異常,導致job1和父協程都直接退出

2.不同上下文(沒有繼承關系)之間協程異常會怎么樣?

例子1:

fun `test coroutineScope exception2`() = runBlocking {
    val job1 = launch {
        delay(2000)
        println("job finished")
    }
    val job2 = CoroutineScope(Dispatchers.IO).launch{
        delay(1000)
        println("job2 finished")
        throw IllegalArgumentException()
        println("new CoroutineScope finished")
    }
    delay(3000)
    println("finished")
}

結果:

job2 finished
Exception in thread "DefaultDispatcher-worker-2" java.lang.IllegalArgumentException
? ? at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt$test coroutineScope exception1$1$job2$1.invokeSuspend(exceptionTest.kt:46)
? ? at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
? ? at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
job finished
finished

Process finished with exit code 0

可以看出不同根協程的協程之間,異常并不會自動傳遞,我們的主線程上下文協程正常執行

再看例子2:

fun `test coroutineScope exception3`() = runBlocking {
    val job1 = launch {
        delay(2000)
        println("job finished")
    }
    val job2 = CoroutineScope(Dispatchers.IO).async{
        delay(1000)
        println("job2 finished")
        throw IllegalArgumentException()
        println("new CoroutineScope finished")
    }
    delay(3000)
    println("finished")
}

結果:
job2 finished
job finished
finished

和例子1的唯一區別是,使用了全新上下文的協程使用了async啟動,哈哈,這就奇怪了,為什么會這樣?

3.向用戶暴露異常

還記得async啟動的協程返回的是一個Deferred么,它可以使用await函數,來獲取協程運行結果。那么試想一下,如果我就是想要一個協程執行完返回一個異常呢?

所以async中的異常會作為返回值,返回給調用await函數

fun `test coroutineScope exception4`() = runBlocking {
    val job1 = launch {
        delay(2000)
        println("job finished")
    }
    val job2 = CoroutineScope(Dispatchers.IO).async{
        delay(1000)
        println("job2 finished")
        throw IllegalArgumentException()
        println("new CoroutineScope finished")
    }
    job2.await()
    delay(3000)
    println("finished")
}

結果:

job2 finished
Exception in thread "main" java.lang.IllegalArgumentException
? ? at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt$test coroutineScope exception4$1$job2$1.invokeSuspend(exceptionTest.kt:96)
? ? at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
? ? at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)

Process finished with exit code 1

await的時候出現異常了,當然會導致協程退出,我們可以在await的時候捕獲下這個異常,就不會影響主線程上下文的協程運行了

fun `test coroutineScope exception4`() = runBlocking {
    val job1 = launch {
        delay(2000)
        println("job finished")
    }
    val job2 = CoroutineScope(Dispatchers.IO).async {
        delay(1000)
        println("job2 finished")
        throw IllegalArgumentException()
        println("new CoroutineScope finished")
    }
    try {
        job2.await()
    } catch (e: Exception) {
        e.printStackTrace()
    }
    delay(3000)
    println("finished")
}

結果:

job2 finished
java.lang.IllegalArgumentException
? ? at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt$test coroutineScope exception4$1$job2$1.invokeSuspend(exceptionTest.kt:96)
? ? at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
? ? at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
job finished
finished

Process finished with exit code 0

值得注意的是,同一繼承關系下的協程使用await并無法捕獲異常,還是會遵循第一條,導致整個協程生命周期結束

fun `test coroutineScope exception5`() = runBlocking {
    val job2 = CoroutineScope(Dispatchers.IO).launch {
        val job1 = launch {
            delay(2000)
            println("job finished")
        }
        val job3 = async {
            delay(1000)
            println("job3 finished")
            throw IllegalArgumentException()
        }
        try {
            job3.await()
        } catch (e: Exception) {
            e.printStackTrace()
        }
        delay(2000)
        println("job2 finished")
    }
    job2.join()
    println("finished")
}

結果:

job3 finished
java.lang.IllegalArgumentException
? ? at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt$test coroutineScope exception5$1$job2$1$job3$1.invokeSuspend(exceptionTest.kt:119)
? ? at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
? ? at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
Exception in thread "DefaultDispatcher-worker-1" java.lang.IllegalArgumentException
? ? at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt$test coroutineScope exception5$1$job2$1$job3$1.invokeSuspend(exceptionTest.kt:119)
? ? at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
? ? at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
finished

Process finished with exit code 0

可以發現job3.await()的try catch并沒有生效,所以向用戶暴露異常只適用于不同上下文(沒有繼承關系)的協程

三、協程的異常處理

使用SupervisorJob

如果想要一個協程出現異常后,不影響其繼承關系中的其他協程,可以使用SupervisorJob

fun `test SupervisorJob exception`() = runBlocking {
    val job1 = launch {
        delay(2000)
        println("job finished")
    }
    val job2 = async(SupervisorJob()) {
        delay(1000)
        println("job2 finished")
        throw IllegalArgumentException()
    }
    delay(3000)
    println("finished")
}

結果:
job2 finished
job finished
finished

可以看到,job2的異常并沒有影響其他繼承關系的協程的執行

SupervisorScope,這個我們前面已經用過了,就不重復介紹了

異常捕獲器CoroutineExceptionHandler

協程上下文的4項之一,可以用CrashHandler理解,不過它并不能阻止協程的退出,只能夠獲取異常的信息

它使用有兩個條件:

1.異常是自動拋出異常(launch)

2.實例化CoroutineScope的時候指定異常捕獲器 或者 在一個根協程中

例子1:

fun `test SupervisorHandler exception1`() = runBlocking {
    val handler = CoroutineExceptionHandler { _, throwable ->
        println("caught: $throwable")
    }
    val scope = CoroutineScope(handler)
    val job1 = scope.launch {
        val job2 = launch {
            delay(1000)
            println("job2 finished")
            throw IllegalArgumentException()
        }
        delay(2000)
        println("job finished")
    }
    delay(4000)
    println("finished")
}

結果:
job2 finished
caught: java.lang.IllegalArgumentException
finished

job2拋出了異常,被捕獲到了,但是scope的其他協程隨之生命周期也都結束了

例子2:

fun `test SupervisorHandler exception2`() = runBlocking {
    val handler = CoroutineExceptionHandler { _, throwable ->
        println("caught: $throwable")
    }
    val scope = CoroutineScope(Dispatchers.Default)
    val job1 = scope.launch(handler) {
        val job2 = launch {
            delay(1000)
            println("job2 finished")
            throw IllegalArgumentException()
        }
        delay(2000)
        println("job finished")
    }
    delay(4000)
    println("finished")
}

結果:
job2 finished
caught: java.lang.IllegalArgumentException
finished

和例子1相同,因為我們handler指定在了根協程

例子3:

fun `test SupervisorHandler exception3`() = runBlocking {
    val handler = CoroutineExceptionHandler { _, throwable ->
        println("caught: $throwable")
    }
    val scope = CoroutineScope(Dispatchers.Default)
    val job1 = scope.launch {
        val job2 = launch(handler) {
            delay(1000)
            println("job2 finished")
            throw IllegalArgumentException()
        }
        delay(2000)
        println("job finished")
    }
    delay(4000)
    println("finished")
}

結果:

job2 finished
Exception in thread "DefaultDispatcher-worker-4" java.lang.IllegalArgumentException
? ? at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt$test SupervisorHandler exception$1$job1$1$job2$1.invokeSuspend(exceptionTest.kt:161)
? ? at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
? ? at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
finished

Process finished with exit code 0

handler不是在根協程中,不能捕獲

如果一個子協程會拋出異常,那么對它進行等待時(join或await),包裹一層try catch 會出現意料之外的事

例子4:

fun `test SupervisorHandler exception4`() = runBlocking {
    val handler = CoroutineExceptionHandler { _, throwable ->
        println("caught: $throwable")
    }
    val scope = CoroutineScope(Dispatchers.Default)
    val job1 = scope.launch(handler) {
        val job2 = launch {
            delay(1000)
            println("job2 finished")
            throw IllegalArgumentException()
        }
        try {
            job2.join()
        }catch (e:Exception){
        }
//        val job3 = scope.launch {
//            println("job3 finished")
//        }
        println("job delay")
        delay(2000)
        for(i in 0..10){
            println(i)
        }
        println("job finished")
    }
    delay(4000)
    println("finished")
}

結果:
job2 finished
job delay
caught: java.lang.IllegalArgumentException
finished

如果把scope根協程中的delay函數注釋掉,會怎么樣呢?

fun `test SupervisorHandler exception4`() = runBlocking {
    val handler = CoroutineExceptionHandler { _, throwable ->
        println("caught: $throwable")
    }
    val scope = CoroutineScope(Dispatchers.Default)
    val job1 = scope.launch(handler) {
        val job2 = launch {
            delay(1000)
            println("job2 finished")
            throw IllegalArgumentException()
        }
        try {
            job2.join()
        }catch (e:Exception){
        }
//        val job3 = scope.launch {
//            println("job3 finished")
//        }
        println("job delay")
//        delay(2000)
        for(i in 0..10){
            println(i)
        }
        println("job finished")
    }
    delay(4000)
    println("finished")
}

結果:
job2 finished
job delay
0
1
2
3
4
5
6
7
8
9
10
job finished
caught: java.lang.IllegalArgumentException

如果不包裹try catch 那么println("job delay")都不會執行

由例子4和例子5,我們可以推斷,如果子協程有異常發生了,我們在等待時捕獲異常后,根協程執行了掛起函數,那么它會直接中斷,不執行掛起函數以下的代碼,如果沒有掛起函數,那么后面的代碼還是會執行

為了加強驗證這點,我們使用Thread.sleep(2000)替換delay函數測試下:

fun `test SupervisorHandler exception4`() = runBlocking {
    val handler = CoroutineExceptionHandler { _, throwable ->
        println("caught: $throwable")
    }
    val scope = CoroutineScope(Dispatchers.Default)
    val job1 = scope.launch(handler) {
        val job2 = launch {
            delay(1000)
            println("job2 finished")
            throw IllegalArgumentException()
        }
        try {
            job2.join()
        }catch (e:Exception){
        }
//        val job3 = scope.launch {
//            println("job3 finished")
//        }
        println("job delay")
//        delay(2000)
        Thread.sleep(2000)
        for(i in 0..10){
            println(i)
        }
        println("job finished")
    }
    delay(4000)
    println("finished")
}

結果還是和例子5一樣:
job2 finished
job delay
0
1
2
3
4
5
6
7
8
9
10
job finished
caught: java.lang.IllegalArgumentException
finished

Process finished with exit code 0

其實出現這個情況,和我們之前取消協程是一樣的,出現異常后會開始取消協程,但是CPU密集型的代碼還會執行,但是遇到掛起函數就會拋一個CancellationException,導致協程結束運行,如果我們在掛起函數加上try catch打印,那么我們就可以看到CancellationException了

例子6,把job3的注釋放開:

fun `test SupervisorHandler exception4`() = runBlocking {
    val handler = CoroutineExceptionHandler { _, throwable ->
        println("caught: $throwable")
    }
    val scope = CoroutineScope(Dispatchers.Default)
    val job1 = scope.launch(handler) {
        val job2 = launch {
            delay(1000)
            println("job2 finished")
            throw IllegalArgumentException()
        }
        try {
            job2.join()
        }catch (e:Exception){
        }
        val job3 = scope.launch {
            println("job3 finished")
        }
        println("job delay")
        delay(2000)
//        Thread.sleep(2000)
        for(i in 0..10){
            println(i)
        }
        println("job finished")
    }
    delay(4000)
    println("finished")
}

結果:

job2 finished
job delay
caught: java.lang.IllegalArgumentException
Exception in thread "DefaultDispatcher-worker-1" java.lang.IllegalArgumentException
? ? at com.aruba.mykotlinapplication.coroutine.ExceptionTestKt$test SupervisorHandler exception4$1$job1$1$job2$1.invokeSuspend(exceptionTest.kt:227)
? ? at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
? ? at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
? ? at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
finished

Process finished with exit code 0

顯然有異常沒有被捕獲,很明顯這個異常是調用job3時輸出的,由此又可以推斷出,如果在等待任務結束時,任務出現異常并且手動捕獲異常后,再啟動子協程時,也會拋出異常,并且不可捕獲
注意:新版本kotlin已修復這個bug,不會拋出異常了

Android中全局異常的處理

最后,感謝動腦學院Jason老師出的kotlin協程教程,得到了很多理解和啟發

原文鏈接:https://www.jianshu.com/p/a933a2d980c7

欄目分類
最近更新