iT邦幫忙

2021 iThome 鐵人賽

DAY 6
1
Mobile Development

解鎖kotlin coroutine的各種姿勢-新手篇系列 第 6

day6 阿伯出事啦 exception

Coroutine支援kotlin一般的Exception處理 try/catch/finally, 或是runningCatch (內部依然使用try/catch), 為了避免有人是直接看這篇,在講一下coroutine怎麼傳播Exception的

Exception傳遞

某個child coroutine有excpetion,通知parent,parent取消他的sibling,往上一層通知parent,直到root scope
coroutine exception

以一個普通的coroutine來說,只要有throw Exception,從root開始的coroutine結果論都會被取消

儘管這個設計適用某些情境,但也有不適合的時候,比如有個ui relative coroutine throw Exception,那整個ui會無法響應,因為已取消的coroutine無發在開啟新的coroutine

supervisor

大家應該都還記得之前講過的supervisor部分吧,他可以向coroutine表示,我會處理這個Exception,所以你不必把其他的coroutine取消,在白話一點就是
[child] 我有Exception喔
[parent] ok

supervisor
而它的開啟方式有兩種

val scope = CoroutineScope(SupervisorJob())

scope.launch {
    
}
//或是

val scope = CoroutineScope(Job())
scope.launch {
    supervisorScope {
        launch {
            // Child 1
        }
        launch {
            // Child 2
        }
    }
}

supervisorJob或是supervisorScope只有在創建scope時傳入才有效
Remember that a SupervisorJob only works as described when it’s part of a scope: either created using supervisorScope or CoroutineScope(SupervisorJob()).

supervisorJob的scope之下建立的子coroutine,即使丟出Exception也不會影響其sibiling,在錯誤處理上和coroutoineScope不同,supervisorScope並不會因為其中一個網路請求回報錯誤而取消該作用域,也就代表著它可以拿到其他正常回報的網路請求; 但同等重要的,如果這個Exception沒有被處理,或是沒有CoroutineExceptionHandler,他會執行default thread的ExceptionHandler,在android就會爆掉~

E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-1
    Process: com.kenny.androidplayground, PID: 23611
    java.lang.Exception: I throw a Exception

? Uncaught exceptions will always be thrown regardless of the kind of Job you use

在launch, exception會再發生地當下立刻被丟出
圖源兼資料來源
catch them all

錯誤用法:詳情參考繼承篇

try catch

try catch的誤用
儘管try/catch看似很直觀,但在try裡面的Exception會被catch,對吧?

CoroutineScope(SupervisorJob()).launch {
    try {
        launch {
            throw Exception("I throw an Exception")
        }
    } catch (e:Exception){
        Timber.e(e)
    }
    launch {
        Timber.d("zero")
    }
}

爆惹

E/CoroutineFragment$test: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine is cancelling; job=StandaloneCoroutine{Cancelling}@3ee750d
    Caused by: java.lang.Exception: I throw an Exception

再改,又爆惹

val scope = CoroutineScope(SupervisorJob())
    scope.launch {
        // Child 1
        throw Exception("I throw an Exception")
    }

    scope.launch {
        Timber.d("ha")
        // Child 2
    }

meme

等等,官方的博文明明是這麼說的

// Scope handling coroutines for a particular layer of my app
val scope = CoroutineScope(SupervisorJob())

scope.launch {
    // Child 1
}
scope.launch {
    // Child 2
}
//In this case, if child#1 fails, neither scope nor child#2 will be cancelled.

不怕不怕,我們接著讀

正如我之前提過的,coroutine有一套自己的Exception傳播系統,但try/ catch也並非毫無用處,launch和async的Exception處理方式不同,所以我們要用不同的方式去catch
launch會再發生地當下立刻丟出,正確的作法是

val scope = CoroutineScope(SupervisorJob())
scope.launch {
    try {
        //somethingDanger()
        throw Exception("I throw an Exception")
    } catch (e:Exception){
        Timber.e(e)
    }
}

scope.launch {
    Timber.d("ha")
    // Child 2
}
E/CoroutineFragment$test: java.lang.Exception: I throw an Exception
        at com.kenny.androidplayground.coroutineUi.CoroutineFragment$test$1.invokeSuspend(CoroutineFragment.kt:39)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
D/CoroutineFragment$test: ha

try/catch包住可能會出錯的Code,而不是包住外面的launch

async就要考慮狀況,當他是root coroutine (coroutines that are a direct child of a CoroutineScope instance or supervisorScope), Exception不會自動發出,而是會等你呼叫await()

a SupervisorJob lets the coroutine handle the exception
注意一下,我們是在supervisorScope 裡面呼叫async and await的,因為Exception會到await呼叫時才會丟出,所以他不用包再try/ catch裡面

supervisorScope {
    val deferred = async {
        codeThatCanThrowExceptions()
    }
    try {
        deferred.await()
    } catch(e: Exception) {
        // Handle exception thrown in async
    }
}

如果是用Job,即使用try/catch也會爆,原因和launch一樣,coroutine有自己傳遞Exception的規則

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

如果這樣寫,不用呼叫await,Exception就會往上傳
The reason is that async (with a Job in its CoroutineContext) will automatically propagate the exception up to its parent (launch) that will throw the exception.

scope.launch {
    async {
        // If async throws, launch throws without calling .await()
    }
}

⚠️ Exceptions thrown in a coroutineScope builder or in coroutines created by other coroutines won’t be caught in a try/catch!

Warning: A SupervisorJob only works as described when it’s part of a scope: either created using supervisorScope or CoroutineScope(SupervisorJob()).

coroutine exception handler

https://cloud.tencent.com/developer/article/1605877
那除了try/catch我們還有其他的處理方法,那就是在CoroutineContext講過的CoroutineExceptionHandler

whenever an exception is caught, you have information about the CoroutineContext where the exception happened and the exception itself
用法大概這樣

val mHandler = CoroutineExceptionHandler {
    context, exception -> println("Caught $exception")
}

CoroutineExceptionHandler有幾個特點

  1. 僅適用於Exception會自動丟出時,也就是僅適用launch
  2. 僅用於root Coroutine或CoroutineScope

When ⏰: The exception is thrown by a coroutine that automatically throws exceptions (works with launch, not with async).
Where ?: If it’s in the CoroutineContext of a CoroutineScope or a root coroutine (direct child of CoroutineScope or a supervisorScope).

看不懂?給你一個例子

val scope = CoroutineScope(Job())
scope.launch(mHandler) {
    launch {
        throw Exception("Failed coroutine")
    }
}

再給一個錯誤例子

val scope = CoroutineScope(Job())
scope.launch {
    launch(mHandler) {
        throw Exception("Failed coroutine")
    }
}

The exception isn’t caught because the handler is not installed in the right CoroutineContext. The inner launch will propagate the exception up to the parent as soon as it happens, since the parent doesn’t know anything about the handler, the exception will be thrown.
基本使用

lifecycleScope.launch (mHandler){
    throw Exception("I throw an Exception")
}

//Caught java.lang.Exception: I throw an Exception

那如果我們同時用Handler和try/catch呢?

lifecycleScope.launch (mHandler){
    try {
        throw Exception("I throw an Exception")
    } catch(e: Exception) {
        Timber.e("try/catch got $e")
    }
}

//try/catch got java.lang.Exception: I throw an Exception

合理

必看官方博文
https://www.kotlincn.net/docs/reference/coroutines/exception-handling.html


上一篇
day 5 knock, knock我要開始coroutine
下一篇
day7 我不要了,這不是肯德基 cancel
系列文
解鎖kotlin coroutine的各種姿勢-新手篇30

尚未有邦友留言

立即登入留言