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


<<:  Day13 补位策略 Backfill

>>:  Day06 补充笔记2

网站架设攻略,初阶观念厘清!

现在高科技时代,拥有自己的网站,应该是每个企业或想发展个人品牌的人,必须要有的,但到底该如何架设网站...

RISC-V: Branch 指令

亲爱的,帮忙去超市买 1 颗苹果回来,如果他们有鸡蛋的话,买 6 颗。 Simple logic p...

TailwindCSS 从零开始 - 翻转卡片实战:TailwindCSS feat CSS

实作内容 此次会透过 TailwindCSS 与 SCSS 共同使用来完成此页面,并透过 CSS ...

Day 20 - Spring Boot & Session

Session 介绍 Session 是储存在Server (服务器)端的资料,当Client 端第...

D23 第九周作业的心得

文章说明 由於课程第十一周开始,我的课程进度已经很明显跟实际周次脱钩,所以时间线会以当时进行的课程进...