Android版kotlin协程入门(三):kotlin协程的异常处理

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运行了 DispatchedTaskrun方法,最后 DispatchedTask.run调用了 continuationresumeWith方法, 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继承 ExecutorWorker继承 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())
        }
    }
}
复制代码

接着我们继续看 DispatchedTaskrun方法,前面怎么获取 exception 的我们先不管,直接看当 exception 不为空时,通过 continuationresumeWithException返回了异常。我们在上面提到过 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库的异常,我们这里大致的了解下就行了。主要分为两种:

  1. kotlinx.coroutines库或编译器有错误,导致的内部错误问题。
  2. ThreadContextElement也就是协程上下文错误,这是因为我们提供了不正确的 ThreadContextElement实现,导致协程处于不一致状态。
public interface ThreadContextElement : CoroutineContext.Element {
    public fun updateThreadContext(context: CoroutineContext): S
    public fun restoreThreadContext(context: CoroutineContext, oldState: S)
}
复制代码

我们看到 handleFatalException实际是调用了 handleCoroutineException方法。 handleCoroutineExceptionkotlinx.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: endD/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创建一个 SupervisorJobsupervisorScope,然后再通过 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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。