写点什么

史上最详 Android 版 kotlin 协程入门进阶实战 (三)(1),面试 Android 岗

用户头像
Android架构
关注
发布于: 15 小时前


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码




我们在开发 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.MainActivity11.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<Task>(singleConsumer = false)


internal actual typealias SchedulerTask = Task


我可以看到这个队列里面存放的就是Task,又通过 kotlin 语言中的[typealias](


)给Task取了一个SchedulerTask的别名。而DispatchedTask继承自SchedulerTask,那么DispatchedTask的来源就解释清楚了。


internal abstract class DispatchedTask<in T>(


@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<T>


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<T>类型的成功或者失败。实际上resumeWithException调用的是resumeWith,只是它是个扩展函数,只是它只能返回Result.failure。同时异常就这么被Continuation无情抛出。


public inline fun <T> Continuation<T>.resumeWithException(exception: Throwable): Unit =


resumeWith(Result.failure(exception))


诶,不对啊,我们在这里还没有执行invokeSuspend啊,你是不是说错了。



是滴,这里只是一种可能,我们现在回到调用continuation的地方,这里的continuation在前面通过DispatchedContinuation得到的,而实际上DispatchedContinuation是个BaseContinuationImpl对象(这里不扩展它是怎么来的,不然又得从头去找它的来源)。


val delegate = delegate as DispatchedContinuation<T>


val continuation = delegate.continuation


internal abstract class BaseContinuationImpl(


public val completion: Continuation<Any?>?


) : Continuation<Any?>, CoroutineStackFrame, Serializable {


public final override fun resumeWith(result: Result<Any?>) {


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<Any?> =


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<T>类型的结果。和我们上面说的是一样的,只是他们是在不同阶段。


那、那、那、那下面那个finally它又是有啥用,我们都通过resumeWithException把异常抛出去了,为啥下面又还有个handleFatalException,这货又是干啥用的???


handleFatalException主要是用来处理kotlinx.coroutines库的异常,我们这里大致的了解下就行了。主要分为两种:


  1. kotlinx.coroutines库或编译器有错误,导致的内部错误问题。

  2. ThreadContextElement也就是协程上下文错误,这是因为我们提供了不正确的ThreadContextElement实现,导致协程处于不一致状态。


public interface ThreadContextElement<S> : 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.MainActivity1$1.invokeSuspend(MainActivity.kt:252)


W/System.err: at com.carman.kotlin.coroutine.MainActivity1$1.invoke(Unknown


//省略...


D/DefaultDispatcher-worker-1: end


诶嘿,这个时候我们程序没有崩溃,只是输出了警告日志而已。那如果遇到try..catch搞不定的怎么办,或者遗漏了需要try..catch的位置怎么办。比如:


private fun testException(){


var a:MutableList<Int> = mutableListOf(1,2,3)


GlobalScope.launch{


launch {


Log.d("${Thread.currentThread().name}","我要开始抛异常了" )


try {


launch{


Log.d("{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.MainActivity11.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<CoroutineExceptionHandler>


public fun handleException(context: CoroutineContext, exception: Throwable)


}


我们稍作修改:


private fun testException(){


val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->


Log.d("exceptionHandler", "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", "throwable")


}


GlobalScope.launch(CoroutineName("父协程") + exceptionHandler){


val job = launch(CoroutineName("子协程")) {


Log.d("${Thread.currentThread().name}","我要开始抛异常了" )


for (index in 0..10){


launch(CoroutineName("孙子协程 $index")) {


Log.d("{coroutineContext[CoroutineName]}" )


}


}

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
史上最详Android版kotlin协程入门进阶实战(三)(1),面试Android岗