史上最详 Android 版 kotlin 协程入门进阶实战 (三)(1),面试 Android 岗
我们在开发 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
运行了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<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())
}
}
}
接着我们继续看DispatchedTask
的run
方法,前面怎么获取exception
的我们先不管,直接看当exception
不为空时,通过continuation
的resumeWithException
返回了异常。我们在上面提到过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
库的异常,我们这里大致的了解下就行了。主要分为两种:
kotlinx.coroutines
库或编译器有错误,导致的内部错误问题。ThreadContextElement
也就是协程上下文错误,这是因为我们提供了不正确的ThreadContextElement
实现,导致协程处于不一致状态。
public interface ThreadContextElement<S> : 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.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]}" )
}
}
评论