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: 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创建一个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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

你可能感兴趣的:(Android版kotlin协程入门(三):kotlin协程的异常处理)