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<T>
类型的成功或者失败。实际上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<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 : 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(“ T h r e a d . c u r r e n t T h r e a d ( ) . n a m e " , " {Thread.currentThread().name}", " 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”, “ c o r o u t i n e C o n t e x t [ C o r o u t i n e N a m e ] : {coroutineContext[CoroutineName]} : 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”, “ c o r o u t i n e C o n t e x t [ C o r o u t i n e N a m e ] 处理异常: {coroutineContext[CoroutineName]} 处理异常 : 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(“ T h r e a d . c u r r e n t T h r e a d ( ) . n a m e " , " {Thread.currentThread().name}"," Thread.currentThread().name","{coroutineContext[CoroutineName]}” )
}
}
throw NullPointerException(“空指针异常”)
}
for (index in 0…10){
launch(CoroutineName(“子协程$index”)) {
Log.d(“ T h r e a d . c u r r e n t T h r e a d ( ) . n a m e " , " {Thread.currentThread().name}"," 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 1 1job$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”, “ c o r o u t i n e C o n t e x t [ C o r o u t i n e N a m e ] . t o S t r i n g ( ) 处理异常: {coroutineContext[CoroutineName].toString()} 处理异常 : 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(“ T h r e a d . c u r r e n t T h r e a d ( ) . n a m e 正常执行 " , " {Thread.currentThread().name}正常执行", " 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
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
![](https://i-blog.csdnimg.cn/blog_migrate/cf3631b369f1c10e57bed9daf49fc24e.jpeg)
尾声
评论里面有些同学有疑问关于如何学习material design控件,我的建议是去GitHub搜,有很多同行给的例子,这些栗子足够入门。
有朋友说要是动真格的话,需要NDK以及JVM等的知识,首现**NDK并不是神秘的东西,**你跟着官方的步骤走一遍就知道什么回事了,无非就是一些代码格式以及原生/JAVA内存交互,进阶一点的有原生/JAVA线程交互,线程交互确实有点蛋疼,但平常避免用就好了,再说对于初学者来说关心NDK干嘛,据鄙人以前的经历,只在音视频通信和一个嵌入式信号处理(离线)的两个项目中用过,嵌入式信号处理是JAVA->NDK->.SO->MATLAB这样调用的我原来MATLAB的代码,其他的大多就用在游戏上了吧,一般的互联网公司会有人给你公司的SO包的。
至于JVM,该掌握的那部分,相信我,你会掌握的,不该你掌握的,有那些专门研究JVM的人来做,不如省省心有空看看计算机系统,编译原理。
一句话,平常多写多练,这是最基本的程序员的素质,尽量挤时间,读理论基础书籍,JVM不是未来30年唯一的虚拟机,JAVA也不一定再风靡未来30年工业界,其他的系统和语言也会雨后春笋冒出来,但你理论扎实会让你很快理解学会一个语言或者框架,你平常写的多会让你很快熟练的将新学的东西应用到实际中。
初学者,一句话,多练。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
知识点,真正体系化!**
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
![](https://i-blog.csdnimg.cn/blog_migrate/cf3631b369f1c10e57bed9daf49fc24e.jpeg)
尾声
评论里面有些同学有疑问关于如何学习material design控件,我的建议是去GitHub搜,有很多同行给的例子,这些栗子足够入门。
有朋友说要是动真格的话,需要NDK以及JVM等的知识,首现**NDK并不是神秘的东西,**你跟着官方的步骤走一遍就知道什么回事了,无非就是一些代码格式以及原生/JAVA内存交互,进阶一点的有原生/JAVA线程交互,线程交互确实有点蛋疼,但平常避免用就好了,再说对于初学者来说关心NDK干嘛,据鄙人以前的经历,只在音视频通信和一个嵌入式信号处理(离线)的两个项目中用过,嵌入式信号处理是JAVA->NDK->.SO->MATLAB这样调用的我原来MATLAB的代码,其他的大多就用在游戏上了吧,一般的互联网公司会有人给你公司的SO包的。
至于JVM,该掌握的那部分,相信我,你会掌握的,不该你掌握的,有那些专门研究JVM的人来做,不如省省心有空看看计算机系统,编译原理。
一句话,平常多写多练,这是最基本的程序员的素质,尽量挤时间,读理论基础书籍,JVM不是未来30年唯一的虚拟机,JAVA也不一定再风靡未来30年工业界,其他的系统和语言也会雨后春笋冒出来,但你理论扎实会让你很快理解学会一个语言或者框架,你平常写的多会让你很快熟练的将新学的东西应用到实际中。
初学者,一句话,多练。