kotlin协程的异常处理
在上一篇《Android kotlin协程入门(二):kotlin协程的关键知识点初步讲解》中我们提到这节将会讲解协程的异常处理。
但是笔者在写这篇文章的时候遇到了些问题,主要是讲解的深度怎么去把控,因为要处理异常,首先得知道异常是如何产生,那么必然就涉及到协程创建->启动->执行->调度->恢复->完成(取消)
流程。这其中每一步都能罗列出一堆需要讲解东西,所以笔者最终决定,我们在这章节中只查看关键点位置,其中涉及到的一些跳出关键点的位置,我们只做一个基本提点,不做延伸。
当然基于前两篇文章的反馈,有读者提到文章文字和代码信息太多,从头到尾看下来很累,想让笔者中间安排一些骚图缓解下紧张的学习气氛。
所以笔者在这篇文章中尝试加入一些元素,如果有不合适的地方,麻烦批评指正。
协程异常的产生流程
我们在开发Android应用时,出现未捕获的异常就会导致程序退出。同样的协程出现未捕获异常,也会导致应用退出。我们要处理异常,那就得先看看协程中的异常产生的流程是什么样的,协程中出现未捕获的异常时会出现哪些信息,如下:
private fun testCoroutineExceptionHandler(){
GlobalScope.launch {
val job = launch {
Log.d("${Thread.currentThread().name}", " 抛出未捕获异常")
throw NullPointerException("异常测试")
}
job.join()
Log.d("${Thread.currentThread().name}", "end")
}
}
复制代码
我们抛出了一个NullPointerException
异常但没有去捕获,所以会导致了应用崩溃退出。
D/DefaultDispatcher-worker-2: 抛出未捕获异常
E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-1
Process: com.carman.kotlin.coroutine, PID: 22734
java.lang.NullPointerException: 异常测试
at com.carman.kotlin.coroutine.MainActivity$testException$1$job$1.invokeSuspend(MainActivity.kt:251)
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)
复制代码
我们看到这个异常是在在CoroutineScheduler
中产生的,虽然我们不知道CoroutineScheduler
是个什么东西。但是我们可以从日志上运行的方法名称先大概的分析一下流程:
它先是创建一个CoroutineScheduler
的一个Worker
对象,接着运行Worker
对象的run
方法,然后runWorker
方法调用了executeTask
,紧接着又在executeTask
里面执行了runSafely
,再接着通过runSafely
运行了DispatchedTask
的run
方法,最后DispatchedTask.run
调用了continuation
的resumeWith
方法,resumeWith
方法中在执行invokeSuspend
的时候抛出了异常。
再来个通熟一点的,你们应该就能猜出大概意思来。雇主先是找包工头CoroutineScheduler
要了一个工人Worker
,然后给这个工人安排了一个搬砖任务DispatchedTask
,同时告诉这个工人他要安全runSafely
的搬砖,然后雇主就让工人Worker
开始工作runWorker
,工人Worker
就开始执行executeTask
雇主吩咐的任务DispatchedTask
,最后通过resumeWith
来执行invokeSuspend
的时候告诉雇主出现了问题(抛出了异常).
别着急,仔细想一想,有没有发现这个跟ThreadPoolExecutor
线程池和Thread
线程的运行很像。包工头就像是ThreadPoolExecutor
线程池,工人就是Thread
线程。
我们通过线程池(CoroutineScheduler
)创建了一个Thread
线程(Worker
),然后开始执行线程(runWorker
),线程里面通过executeTask
执行一个任务DispatchedTask
,在执行任务的时候我们通过try..catch
来保证任务安全执行runSafely
,然后在DispatchedTask
执行任务的时候,因为运行出现异常,所以在catch
中通过resumeWith
来告知结果线程出问题了。咦,逻辑好像突然变得清晰很多。
这么看的话,这个协程异常的产生是不是基本原理就出来了。那么我们接下里看看是不是正如我们所想的,我们先找到CoroutineScheduler
看看他的实现:
internal class CoroutineScheduler(...) : Executor, Closeable {
@JvmField
val globalBlockingQueue = GlobalQueue()
fun runSafely(task: Task) {
try {
task.run()
} catch (e: Throwable) {
val thread = Thread.currentThread()
thread.uncaughtExceptionHandler.uncaughtException(thread, e)
} finally {
unTrackTask()
}
}
//省略...
internal inner class Worker private constructor() : Thread() {
override fun run() = runWorker()
private fun runWorker() {
var rescanned = false
while (!isTerminated && state != WorkerState.TERMINATED) {
val task = findTask(mayHaveLocalTasks)
if (task != null) {
rescanned = false
minDelayUntilStealableTaskNs = 0L
executeTask(task)
continue
} else {
mayHaveLocalTasks = false
}
//省略...
continue
}
}
private fun executeTask(task: Task) {
//省略...
runSafely(task)
//省略...
}
fun findTask(scanLocalQueue: Boolean): Task? {
if (tryAcquireCpuPermit()) return findAnyTask(scanLocalQueue)
val task = if (scanLocalQueue) {
localQueue.poll() ?: globalBlockingQueue.removeFirstOrNull()
} else {
globalBlockingQueue.removeFirstOrNull()
}
return task ?: trySteal(blockingOnly = true)
}
//省略...
}
//省略...
}
复制代码
哎呀呀,不得了,跟我们上面想的一模一样。CoroutineScheduler
继承Executor
,Worker
继承Thread
,同时runWorker
也是线程的run
方法。在runWorker
执行了executeTask(task)
,接着在executeTask
调用中runSafely(task)
,然后我们看到runSafely
使用try..catch
了这个task
任务的执行,最后在catch
中抛出了未捕获的异常。那么很明显这个task肯定就是我们的DispatchedTask
,那就到这里结束了么
很明显并没有,我们看到catch
中抛出的是个线程的uncaughtExceptionHandler
,这个我们就很熟了,在Android开发中都是通过这个崩溃信息。但是这个明显不是我们这次的目标。
继续往下分析,我们看看这个task
到底是不是DispatchedTask
。回到executeTask(task)
的调用出,我们看到这个task
是通过findTask
获取的,而这个task
又是在findTask
中通过CoroutineScheduler
线程池中的globalBlockingQueue
队列中取出的,我们看看这个GlobalQueue
:
internal class GlobalQueue : LockFreeTaskQueue(singleConsumer = false)
复制代码
internal actual typealias SchedulerTask = Task
复制代码
我可以看到这个队列里面存放的就是Task
,又通过kotlin语言中的typealias给Task
取了一个SchedulerTask
的别名。而DispatchedTask
继承自SchedulerTask
,那么DispatchedTask
的来源就解释清楚了。
internal abstract class DispatchedTask(
@JvmField public var resumeMode: Int
) : SchedulerTask() {
//省略...
internal open fun getExceptionalResult(state: Any?): Throwable? =
(state as? CompletedExceptionally)?.cause
public final override fun run() {
assert { resumeMode != MODE_UNINITIALIZED }
val taskContext = this.taskContext
var fatalException: Throwable? = null
try {
val delegate = delegate as DispatchedContinuation
val continuation = delegate.continuation
withContinuationContext(continuation, delegate.countOrElement) {
val context = continuation.context
val state = takeState()
val exception = getExceptionalResult(state)
val job = if (exception == null && resumeMode.isCancellableMode) context[Job] else null
if (job != null && !job.isActive) {
val cause = job.getCancellationException()
cancelCompletedResult(state, cause)
continuation.resumeWithStackTrace(cause)
} else {
if (exception != null) {
continuation.resumeWithException(exception)
} else {
continuation.resume(getSuccessfulResult(state))
}
}
}
} catch (e: Throwable) {
fatalException = e
} finally {
val result = runCatching { taskContext.afterTask() }
handleFatalException(fatalException, result.exceptionOrNull())
}
}
}
复制代码
接着我们继续看DispatchedTask
的run
方法,前面怎么获取exception
的我们先不管,直接看当exception
不为空时,通过continuation
的resumeWithException
返回了异常。我们在上面提到过continuation
,在挂起函数的挂起以后,会通过Continuation
调用resumeWith
函数恢复协程的执行,同时返回Result
类型的成功或者失败。实际上resumeWithException
调用的是resumeWith
,只是它是个扩展函数,只是它只能返回Result.failure
。同时异常就这么被Continuation
无情抛出。
public inline fun Continuation.resumeWithException(exception: Throwable): Unit =
resumeWith(Result.failure(exception))
复制代码
诶,不对啊,我们在这里还没有执行invokeSuspend
啊,你是不是说错了。
是滴,这里只是一种可能,我们现在回到调用continuation
的地方,这里的continuation
在前面通过DispatchedContinuation
得到的,而实际上DispatchedContinuation
是个BaseContinuationImpl
对象(这里不扩展它是怎么来的,不然又得从头去找它的来源)。
val delegate = delegate as DispatchedContinuation
val continuation = delegate.continuation
复制代码
internal abstract class BaseContinuationImpl(
public val completion: Continuation?
) : Continuation, CoroutineStackFrame, Serializable {
public final override fun resumeWith(result: Result) {
var current = this
var param = result
while (true) {
probeCoroutineResumed(current)
with(current) {
val completion = completion!! // fail fast when trying to resume continuation
val outcome: Result =
try {
val outcome = invokeSuspend(param)
if (outcome === COROUTINE_SUSPENDED) return
Result.success(outcome)
} catch (exception: Throwable) {
Result.failure(exception)
}
releaseIntercepted() // this state machine instance is terminating
if (completion is BaseContinuationImpl) {
current = completion
param = outcome
} else {
completion.resumeWith(outcome)
return
}
}
}
}
}
复制代码
可以看到最终这里面invokeSuspend
才是真正调用我们协程的地方。最后也是通过Continuation
调用resumeWith
函数恢复协程的执行,同时返回Result
类型的结果。和我们上面说的是一样的,只是他们是在不同阶段。
那、那、那、那下面那个finally
它又是有啥用,我们都通过resumeWithException
把异常抛出去了,为啥下面又还有个handleFatalException
,这货又是干啥用的???
handleFatalException
主要是用来处理kotlinx.coroutines
库的异常,我们这里大致的了解下就行了。主要分为两种:
-
kotlinx.coroutines
库或编译器有错误,导致的内部错误问题。 -
ThreadContextElement
也就是协程上下文错误,这是因为我们提供了不正确的ThreadContextElement
实现,导致协程处于不一致状态。
public interface ThreadContextElement : CoroutineContext.Element {
public fun updateThreadContext(context: CoroutineContext): S
public fun restoreThreadContext(context: CoroutineContext, oldState: S)
}
复制代码
我们看到handleFatalException
实际是调用了handleCoroutineException
方法。handleCoroutineException
是kotlinx.coroutines
库中的顶级函数
public fun handleFatalException(exception: Throwable?, finallyException: Throwable?) {
//省略....
handleCoroutineException(this.delegate.context, reason)
}
复制代码
public fun handleCoroutineException(context: CoroutineContext, exception: Throwable) {
try {
context[CoroutineExceptionHandler]?.let {
it.handleException(context, exception)
return
}
} catch (t: Throwable) {
handleCoroutineExceptionImpl(context, handlerException(exception, t))
return
}
handleCoroutineExceptionImpl(context, exception)
}
复制代码
我们看到handleCoroutineException
会先从协程上下文拿CoroutineExceptionHandler
,如果我们没有定义的CoroutineExceptionHandler
话,它将会调用handleCoroutineExceptionImpl
抛出一个uncaughtExceptionHandler
导致我们程序崩溃退出。
internal actual fun handleCoroutineExceptionImpl(context: CoroutineContext, exception: Throwable) {
for (handler in handlers) {
try {
handler.handleException(context, exception)
} catch (t: Throwable) {
val currentThread = Thread.currentThread()
currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, handlerException(exception, t))
}
}
val currentThread = Thread.currentThread()
currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, exception)
}
复制代码
不知道各位是否理解了上面的流程,笔者最开始的时候也是被这里来来回回的。绕着晕乎乎的。如果没看懂的话,可以休息一下,揉揉眼睛,倒杯热水,再回过头捋一捋。
好滴,到此处为止。我们已经大概的了解kotlin协程中异常是如何抛出的,下面我们就不再不过多延伸。下面我们来说说异常的处理。
协程的异常处理
kotlin协程异常处理我们要分成两部分来看,通过上面的分解我们知道一种异常是通过resumeWithException
抛出的,还有一种异常是直接通过CoroutineExceptionHandler
抛出,那么我们现在就开始讲讲如何处理异常。
第一种:当然就是我们最常用的try..catch
大法啦,只要有异常崩溃我就先try..catch
下,先不管流程对不对,我先保住我的程序不能崩溃。
private fun testException(){
GlobalScope.launch{
launch(start = CoroutineStart.UNDISPATCHED) {
Log.d("${Thread.currentThread().name}", " 我要开始抛异常了")
try {
throw NullPointerException("异常测试")
} catch (e: Exception) {
e.printStackTrace()
}
}
Log.d("${Thread.currentThread().name}", "end")
}
}
复制代码
D/DefaultDispatcher-worker-1: 我要开始抛异常了
W/System.err: java.lang.NullPointerException: 异常测试
W/System.err: at com.carman.kotlin.coroutine.MainActivity$testException$1$1.invokeSuspend(MainActivity.kt:252)
W/System.err: at com.carman.kotlin.coroutine.MainActivity$testException$1$1.invoke(Unknown
//省略...
D/DefaultDispatcher-worker-1: end
复制代码
诶嘿,这个时候我们程序没有崩溃,只是输出了警告日志而已。那如果遇到try..catch
搞不定的怎么办,或者遗漏了需要try..catch
的位置怎么办。比如:
private fun testException(){
var a:MutableList = mutableListOf(1,2,3)
GlobalScope.launch{
launch {
Log.d("${Thread.currentThread().name}","我要开始抛异常了" )
try {
launch{
Log.d("${Thread.currentThread().name}", "${a[1]}")
}
a.clear()
} catch (e: Exception) {
e.printStackTrace()
}
}
Log.d("${Thread.currentThread().name}", "end")
}
}
复制代码
D/DefaultDispatcher-worker-1: end
D/DefaultDispatcher-worker-2: 我要开始抛异常了
E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-2
Process: com.carman.kotlin.coroutine, PID: 5394
java.lang.IndexOutOfBoundsException: Index: 1, Size: 0
at java.util.ArrayList.get(ArrayList.java:437)
at com.carman.kotlin.coroutine.MainActivity$testException$1$1$1.invokeSuspend(MainActivity.kt:252)
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)
复制代码
当你以为使用try..catch
就能捕获的时候,然而实际并没有。这是因为我们的try..catch
使用方式不对,我们必须在使用a[1]
时候再用try..catch
捕获才行。那就有人会想那我每次都记得使用try..catch
就好了。
是,当然没问题。但是你能保证你每次都能记住吗,你的同一战壕里的战友会记住吗。而且当你的逻辑比较复杂的时候,你使用那么多try..catch
你代码阅读性是不是降低了很多后,你还能记住哪里有可能会出现异常吗。
这个时候就需要使用协程上下文中的CoroutineExceptionHandler
。我们在上一篇文章讲解协程上下文的时候提到过,它是协程上下文中的一个Element
,是用来捕获协程中未处理的异常。
public interface CoroutineExceptionHandler : CoroutineContext.Element {
public companion object Key : CoroutineContext.Key
public fun handleException(context: CoroutineContext, exception: Throwable)
}
复制代码
我们稍作修改:
private fun testException(){
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.d("exceptionHandler", "${coroutineContext[CoroutineName]} :$throwable")
}
GlobalScope.launch(CoroutineName("异常处理") + exceptionHandler){
val job = launch{
Log.d("${Thread.currentThread().name}","我要开始抛异常了" )
throw NullPointerException("异常测试")
}
Log.d("${Thread.currentThread().name}", "end")
}
}
复制代码
D/DefaultDispatcher-worker-1: 我要开始抛异常了
D/exceptionHandler: CoroutineName(异常处理) :java.lang.NullPointerException: 异常测试
D/DefaultDispatcher-worker-2: end
复制代码
这个时候即使我们没有使用try..catch
去捕获异常,但是异常还是被我们捕获处理了。是不是感觉异常处理也没有那么难。那如果按照上面的写,我们是不是得在每次启动协程的时候,也需要跟try..catch
一样都需要加上一个CoroutineExceptionHandler
呢? 这个时候我们就看出来,各位是否真的有吸收前面讲解的知识:
第一种:我们上面讲解的
协程作用域
部分你已经消化吸收,那么恭喜你接下来的你可以大概的过一遍或者选择跳过了。因为接下来的部分和协程作用域
中说到的内容大体一致。第二种:除第一种的,都是第二种。那你接下来你就得认证仔细的看了。
我们之前在讲到协同作用域
和主从(监督)作用域
的时候提到过,异常传递的问题。我们先来看看协同作用域
:
-
协同作用域
如果子协程抛出未捕获的异常时,会将异常传递给父协程处理,如果父协程被取消,则所有子协程同时也会被取消。
容我盗个官方图
默认情况下,当协程因出现异常失败时,它会将异常传播到它的父级,父级会取消其余的子协程,同时取消自身的执行。最后将异常在传播给它的父级。当异常到达当前层次结构的根,在当前协程作用域启动的所有协程都将被取消。
我们在前一个案例的基础上稍作做一下修改,只在父协程上添加CoroutineExceptionHandler
,照例上代码:
private fun testException(){
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.d("exceptionHandler", "${coroutineContext[CoroutineName]} 处理异常 :$throwable")
}
GlobalScope.launch(CoroutineName("父协程") + exceptionHandler){
val job = launch(CoroutineName("子协程")) {
Log.d("${Thread.currentThread().name}","我要开始抛异常了" )
for (index in 0..10){
launch(CoroutineName("孙子协程$index")) {
Log.d("${Thread.currentThread().name}","${coroutineContext[CoroutineName]}" )
}
}
throw NullPointerException("空指针异常")
}
for (index in 0..10){
launch(CoroutineName("子协程$index")) {
Log.d("${Thread.currentThread().name}","${coroutineContext[CoroutineName]}" )
}
}
try {
job.join()
} catch (e: Exception) {
e.printStackTrace()
}
Log.d("${Thread.currentThread().name}", "end")
}
}
复制代码
D/DefaultDispatcher-worker-3: 我要开始抛异常了
W/System.err: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine is cancelling; job=StandaloneCoroutine{Cancelling}@f6b7807
W/System.err: Caused by: java.lang.NullPointerException: 空指针异常
W/System.err: at com.carman.kotlin.coroutine.MainActivity$testException$1$job$1.invokeSuspend(MainActivity.kt:26//省略...
D/DefaultDispatcher-worker-6: end
D/exceptionHandler: CoroutineName(父协程) 处理异常 :java.lang.NullPointerException: 空指针异常
复制代码
我们看到子协程job
的异常被父协程处理了,无论我下面开启多少个子协程产生异常,最终都是被父协程处理。但是有个问题是:因为异常会导致父协程被取消执行,同时导致后续的所有子协程都没有执行完成(可能偶尔有个别会执行完)。那可能就会是有人问了,这种做法的意义和应用场景是什么呢?
如果有一个页面,它最终展示的数据,是通过请求多个服务器接口的数据拼接而成的,而其中某一个接口出问题都将不进行数据展示,而是提示加载失败。那么你就可以使用上面的方案去做,都不用管它们是谁报的错,反正都是统一处理,一劳永逸。类似这样的例子我们在开发中应该经常遇到。
但是另外一个问题就来了。例如我们APP的首页,首页上展示的数据五花八门。如:广告,弹窗,未读状态,列表数据等等都在首页存在,但是他们相互之间互不干扰又不关联,即使其中某一个失败了也不影响其他数据展示。那通过上面的方案,我们就没办法处理。
这个时候我们就可以通过主从(监督)作用域
的方式去实现,与协同作用域
一致,区别在于该作用域下的协程取消操作的单向传播性,子协程的异常不会导致其它子协程取消。我再盗个官方图:
我们在讲解主从(监督)作用域
的时候提到过,要实现主从(监督)作用域
需要使用supervisorScope
或者SupervisorJob
。这里我们需要补充一下,我们在使用supervisorScope
其实用的就是SupervisorJob
。 这也是为什么使用supervisorScope
与使用SupervisorJob
协程处理是一样的效果。
/**
* 省略...
* but overrides context's [Job] with [SupervisorJob].
* 省略...
*/
public suspend fun supervisorScope(block: suspend CoroutineScope.() -> R): R {
//省略...
}
复制代码
这段是摘自官方文档的,其他的我把它们省略了,只留了一句:"SupervisorJob
会覆盖上下文中的Job
"。这也就说明我们在使用supervisorScope
的就是使用的SupervisorJob
。我们先用supervisorScope
实现以下我们上面提到的案例:
private fun testException(){
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.d("exceptionHandler", "${coroutineContext[CoroutineName].toString()} 处理异常 :$throwable")
}
GlobalScope.launch(exceptionHandler) {
supervisorScope {
launch(CoroutineName("异常子协程")) {
Log.d("${Thread.currentThread().name}", "我要开始抛异常了")
throw NullPointerException("空指针异常")
}
for (index in 0..10) {
launch(CoroutineName("子协程$index")) {
Log.d("${Thread.currentThread().name}正常执行", "$index")
if (index %3 == 0){
throw NullPointerException("子协程${index}空指针异常")
}
}
}
}
}
}
复制代码
D/DefaultDispatcher-worker-1: 我要开始抛异常了
D/exceptionHandler: CoroutineName(异常子协程) 处理异常 :java.lang.NullPointerException: 空指针异常
D/DefaultDispatcher-worker-1正常执行: 1
D/DefaultDispatcher-worker-1正常执行: 2
D/DefaultDispatcher-worker-3正常执行: 0
D/DefaultDispatcher-worker-1正常执行: 3
D/exceptionHandler: CoroutineName(子协程0) 处理异常 :java.lang.NullPointerException: 子协程0空指针异常
D/exceptionHandler: CoroutineName(子协程3) 处理异常 :java.lang.NullPointerException: 子协程3空指针异常
D/DefaultDispatcher-worker-4正常执行: 4
D/DefaultDispatcher-worker-4正常执行: 5
D/DefaultDispatcher-worker-5正常执行: 7
D/DefaultDispatcher-worker-3正常执行: 6
D/DefaultDispatcher-worker-5正常执行: 8
D/DefaultDispatcher-worker-5正常执行: 9
D/exceptionHandler: CoroutineName(子协程9) 处理异常 :java.lang.NullPointerException: 子协程9空指针异常
D/exceptionHandler: CoroutineName(子协程6) 处理异常 :java.lang.NullPointerException: 子协程6空指针异常
D/DefaultDispatcher-worker-2正常执行: 10
复制代码
可以看到即使当中有多个协程都出现问题,我们还是能够让所有的子协程执行完成。这个时候我们用这样方案是不是就可以解决,我们首页多种数据互不干扰的刷新问题了,同也能够在出现异常的时候统一处理。
那我们在用SupervisorJob
实现一遍,看看是不是和supervisorScope
一样的,代码奉上:
private fun testException(){
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.d("exceptionHandler", "${coroutineContext[CoroutineName].toString()} 处理异常 :$throwable")
}
val supervisorScope = CoroutineScope(SupervisorJob() + exceptionHandler)
with(supervisorScope) {
launch(CoroutineName("异常子协程")) {
Log.d("${Thread.currentThread().name}", "我要开始抛异常了")
throw NullPointerException("空指针异常")
}
for (index in 0..10) {
launch(CoroutineName("子协程$index")) {
Log.d("${Thread.currentThread().name}正常执行", "$index")
if (index % 3 == 0) {
throw NullPointerException("子协程${index}空指针异常")
}
}
}
}
}
复制代码
可以看到我们通过CoroutineScope
创建一个SupervisorJob
的supervisorScope
,然后再通过with(supervisorScope)
是不是就变得跟直接使用supervisorScope
一样了。
D/DefaultDispatcher-worker-1: 我要开始抛异常了
D/DefaultDispatcher-worker-2正常执行: 0
D/exceptionHandler: CoroutineName(子协程0) 处理异常 :java.lang.NullPointerException: 子协程0空指针异常
D/exceptionHandler: CoroutineName(异常子协程) 处理异常 :java.lang.NullPointerException: 空指针异常
D/DefaultDispatcher-worker-2正常执行: 1
D/DefaultDispatcher-worker-2正常执行: 2
D/DefaultDispatcher-worker-4正常执行: 3
D/exceptionHandler: CoroutineName(子协程3) 处理异常 :java.lang.NullPointerException: 子协程3空指针异常
D/DefaultDispatcher-worker-1正常执行: 4
D/DefaultDispatcher-worker-4正常执行: 5
D/DefaultDispatcher-worker-4正常执行: 6
D/exceptionHandler: CoroutineName(子协程6) 处理异常 :java.lang.NullPointerException: 子协程6空指针异常
D/DefaultDispatcher-worker-4正常执行: 8
D/DefaultDispatcher-worker-3正常执行: 7
D/DefaultDispatcher-worker-2正常执行: 9
D/exceptionHandler: CoroutineName(子协程9) 处理异常 :java.lang.NullPointerException: 子协程9空指针异常
D/DefaultDispatcher-worker-3正常执行: 10
复制代码
当然,我们在使用协程的时候,可能某个协程需要自己处理自己的异常,这个时候只需要在这个协程的上下文中添加CoroutineExceptionHandler
即可。毕竟按需使用,谁也不知道产品又会有什么奇怪的想法。
好了,到现在我们也基本的知道协程中的异常产生流程,和按需处理协程中的异常问题。如果您还有什么不清楚的地方,可以自己动手实验一下或者在下方留言、私信笔者等方式,我会在看到消息的第一时间处理。
预告以及意见收集
在下一章节中,我们将会进入到实际的Android开发中,我们会先构建一个基础APP的框架,封装一些常用的协程方法和请求方式,至于具体的实战项目类型,我想征求一下大家的意见,然后根据反馈的实际情况再来决定,欢迎大家踊跃的提出意见。
最后:祝愿大家都能写出完美的BUG,让测试都无法找到BUG所在。
作者:一个被摄影耽误的程序猿
链接:https://juejin.cn/post/6954250061207306253
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。