本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布[2023-02-21]
config:
-
system: macOS
-
android studio: 2022.1.1 Electric Eel
-
gradle: gradle-7.5-bin.zip
-
android build gradle: 7.1.0
-
Kotlin coroutine core: 1.6.4
tips:前面几篇全都是协程的基本使用,没有源码,等后面对协程有个基本理解之后,才会简单的分析一下源码!
上一篇(android kotlin coroutine 基本入门)
看完本篇你能学会什么:
-
CoroutineDispatcher // 协程调度器 用来切换线程
-
CoroutineName // 协程名字
-
CoroutineStart // 协程启动模式
-
CoroutineException // launch / async 捕获异常
-
GlobalCoroutineException // 全局捕获异常
CoroutineDispatcher 协程调度器
定义: 根据名字也可以看出来, 协程调度器, 主要用来切换线程,主要有4种
- Dispatchers.Main - 使用此调度程序可在 Android 主线程上运行协程。
- Dispatchers.IO - 此调度程序经过了专门优化,适合在主线程之外执行磁盘或网络 I/O。示例包括使用 Room 组件、从文件中读取数据或向文件中写入数据,以及运行任何网络操作。
- Dispatchers.Default - 此调度程序经过了专门优化,适合在主线程之外执行占用大量 CPU 资源的工作。用例示例包括对列表排序和解析 JSON。
- Dispatchers.Unconfined-始终和父协程使用同一线程
先来看一个简单的例子:
这行代码的意思是开启一个协程,他的作用域在子线程上
可以看出,只要设置DIspatchers.IO 就可以切换线程
tips: 这里我使用的是协程调试才可以打印出协程编号
1.
-Dkotlinx.coroutines.debug
使用协程DIspatcher切换线程的时候,需要注意的是,子协程如果调度了,就使用调度后的线程,如果没有调度,始终保持和父协程相同的线程
这里的调度就是指的是否有DIspatcher.XXX
例如这样:
对于coroutine#4,他会跟随 coroutine#3 的线程
coroutine#3 会 跟随 coroutine#2 的线程
coroutine#2 有自身的调度器IO,所以全部都是IO线程
再来看一段代码:
withContext() 是用来切换线程,这里切换到主线程,但是输出的结果并没有切换到主线程
withContext{} 与launch{} 调度的区别:
- withContext 在原有协程上切换线程
- launch 创建一个新的协程来切换线程
这里我感觉是kotlin对JVM支持还不够
因为本身JVM平台就没有Main线程,Main线程是对与Android平台的
所以我们将这段代码拿到android平台试一下
可以看出,可以切换,我们以android平台为主!
这里需要注意的是:
JVM平台上没有Dispatcher.Main, 因为Main只是针对android的,所以如果想要在JVM平台上切换Main线程,
需要添加:
implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
并且在dispatcher.Main之前调用 Dispatchers.setMain(Dispatchers.Unconfined)
现在我们知道了通过Dispatcher.XXX 就可以切换线程, 那么Dispatcher.XXX是什么呢? 这里以Dispatcher.IO为例
可以看出,继承关系为:
Dispatcher.IO = DefaultIoScheduler => ExecutorCoroutineDispatcher => CoroutineDispatcher => AbstractCoroutineContextElement => Element => CoroutineContext
最终都是 CoroutineContext 的子类!
CoroutineName 协程名字
**定义:**协程名字, 子协程会继承父协程的名字, 如果协程种有自己的名字,那么就优先使用自己的
这块代码比较简单,就不废话了
可以看出,CoroutineName也是CoroutineContext的子类, 如果说
现在我们现在想要切换到子线程上我们该怎么做?
通过刚才的代码,我们知道DIspatcher.XXX 其本质就是CoroutineContext, 那么我们就可以通过内置的操作符重载来实现两个功能的同时操作
CoroutineStart 协程启动模式
定义: coroutineStart 用来控制协程调度器,以及协程的执行时机等
- CoroutineStart.DEFAULT: 立即根据其上下文安排协程执行;
- CoroutineStart.LAZY: 懒加载,不会立即执行,只有调用的时候才会执行
- CoroutineStart.ATOMIC: 常配合Job#cancel()来使用, 如果协程体中有新的挂起点,调用Job#cancel()时 取消挂起点之后的代码,否则全部取消
- CoroutineStart.UnDISPATCHED: 不进行任何调度,包括线程切换等, 线程状态会跟随父协程保持一致
CoroutineStart.DEFAULT 我相信不用过多赘述, 默认就是这个,直接从 CoroutineStart.LAZY开始
CoroutineStart.LAZY
首先来看一段代码:
可以通过这段代码发现, 其余的协程都执行了,只有采用CoroutineStart.LAZY的协程没有执行,并且runBlocking 会一直等待他执行
那么只需要调用Job#start() 或者 job#join() 即可
CoroutineStart.ATOMIC
tips:该属性目前还在试验阶段
先来看正常效果:
在这段代码中,我们开启了一个协程,然后立即cancel了,协程中的代码没有执行
如果改成 CoroutineStart.ATOMIC 会发生什么情况呢?
可以惊奇的发现,居然取消协程没有作用!
那么这个CoroutineStart.ATOMIC到底有什么用呢?
再来看一段代码:
可以看出, CoroutineStart.ATOMIC 会将挂起点之后的代码给cancel掉,
即使这里delay很久,也会立即cancel
再换一种挂起点方式
也还是同样的结果.
Coroutine.UNDISPATCHED
定义: 不进行任何调度,包括线程切换等, 线程状态会跟随父协程保持一致
首先还是看默认状态
注意:这里代码会首先执行:1.main start 和 2. main end
这里有一个调度的概念,比较抽象:
协程始终都是异步执行的,kotlin协程的底层也是线程, kotlin协程说白了就是一个线程框架,
所以创建协程的时候,其实就是创建了一个线程, 使用线程的时候,我们会通过Thread#start() 告诉JVM我们有一个任务需要执行,
然后JVM去分配,最后JVM去执行
这里调度的大致逻辑和线程类似
只不过协程可以轻易的实现2个线程之前切换,切换回来的过程在协程中我们叫它恢复
这里扯的有点远,先来看本篇的内容 :)
我们来看看 Coroutine.UNDISPATCHED有什么作用
可以看出,一旦使用了这种启动模式, 就没有了调度的概念,即使是切换线程(withContext)也无济于事
跟随父协程线程状态而变化
说实话,这种启动模式我认为比较鸡肋,和不写这个协程好像也没有很大的区别
CoroutineException 协程异常捕获
重点: 协程异常捕获必须放在最顶层的协程作用域上
最简单的我们通过try catch 来捕获,这种办法就不说了,
首先我们来看看 coroutineException的继承关系
CoroutineExceptionHandler => AbstractCoroutineContextElement => Element => CoroutineContext
最终继承自 CoroutineContext
到目前为止,我们知道了 coroutineContext有4个有用的子类
- Job 用来控制协程生命周期
- CoroutineDispatcher 协程调度器,用来切换线程
- CoroutineName 写成名字
- CoroutineException 协程异常捕获
首先我们来分析 CoroutineScope#launch 异常捕获
捕获异常之前先说一个秘密: Job不仅可以用来控制协程生命周期,还可以用不同的Job 来控制协程的异常捕获
Job配合CoroutineHandler 异常捕获
先来看一段简单的代码:
tip: 如果不写Job 默认就是Job()
可以看出,目前的状态是协程1
出现错误之后,就会反馈给CoroutineExcetionHandler
然后协程2
就不会执行了
SupervisorJob()
假如有一个场景,我们需要某个子协程出现问题就出现问题,不应该影响到其他的子协程执行,那么我们就可以用 SupervisorJob()
SupervisorJob() 的特点就是:如果某个子协程出现问题不会影响兄弟协程
Job与 SupervisorJob 的区别也很明显
- Job 某个协程出现问题,会直接影响兄弟协程,兄弟协程不会执行
- SupervisorJob 某个协程出现问题,不会影响兄弟协程.
如果现在场景变一下,现在换成了子协程中出现问题,来看看效果
可以看出, 子协程2
并没有执行 这是默认效果,若在子协程中开启多个子协程,其实建议写法是这样的
coroutineScope{}
为什么要这么写呢? 明明我不写效果就一样,还得写这玩意,不是闲的没事么
我感觉,作用主要就是统一代码,传递CoroutineScope 例如这样
正常在实际开发中如果吧代码全写到一坨,应该会遭到同行鄙视 :]
现在场景又调整了, 刚才是子协程出现问题立即终止子协程的兄弟协程
现在调整成了: 某个子协程出现问题,不影响子协程的兄弟协程,就想 SupervisorJob() 类型
superiverScope{}
那就请出了我们的superiverScope{}
作用域
效果很简单
这里主要要分清楚
SuperiverScope() 和 superiverScope{} 是不一样的
- SuperiverScope() 是用来控制兄弟协程异常的,并且他是一个类
- superiverScope{} 是用来控制子协程的兄弟协程的,他是一个函数
async捕获异常
重点: async使用 CoroutineExceptionHandler 是捕获不到异常的
例如这样:
async 的异常在 Deferred#await()
中, 还记得上一篇中我们聊过 Deferred#await()
这个方法会获取到async{} 中的返回结果
如果我们想要捕获async{} 中的异常,我们只需要try{} catch{} await即可,例如这样写
async 也可以配合 SupervisorJob() 达到子协程出现问题,不影响兄弟协程执行,例如这样:
如何让 CoroutineExceptionHandler 监听到async的异常,本质是监听不到的,
但是,我们知道了deferred#await()
会抛出异常,那么我们可以套一层 launch{} 这样一来就可以达到我们想要的效果
suspend fun main() {
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
printlnThread("catch 到了 $throwable")
}
val customScope =
CoroutineScope(SupervisorJob() + CoroutineName("自定义协程") + Dispatchers.IO + exceptionHandler)
val deferred1 = customScope.async {
printlnThread("子协程 1 start")
throw KotlinNullPointerException(" ============= 出错拉 1")
"协程1执行完成"
}
val deferred2 = customScope.async {
printlnThread("子协程 2 start")
"协程2执行完成"
}
val deferred3 = customScope.async {
printlnThread("子协程 3 start")
throw KotlinNullPointerException(" ============= 出错拉 3")
"协程3执行完成"
}
customScope.launch {
supervisorScope {
launch {
val result = deferred1.await()
println("协程1 result:$result")
}
launch {
val result = deferred2.await()
println("协程2 result:$result")
}
launch {
val result = deferred3.await()
println("协程3 result:$result")
}
}
}.join()
}
结果为:
子协程 3 start: thread:DefaultDispatcher-worker-2 @自定义协程#3
子协程 2 start: thread:DefaultDispatcher-worker-3 @自定义协程#2
子协程 1 start: thread:DefaultDispatcher-worker-1 @自定义协程#1
协程2 result:协程2执行完成
catch 到了 kotlin.KotlinNullPointerException: ============= 出错拉 3: thread:DefaultDispatcher-worker-2 @自定义协程#7
catch 到了 kotlin.KotlinNullPointerException: ============= 出错拉 1: thread:DefaultDispatcher-worker-1 @自定义协程#5
协程捕获异常,最终要的一点就是,协程中的异常会一直向上传递,如果想要 使用 CoroutineExceptionHandler,监听到异常,那么就必须将 CoroutineExceptionHandler 配置到最顶级的coroutineScope中
GlobalCoroutineException 全局异常捕获
需要在本地配置一个捕获监听:
resources/META-INF/services/kotlinx.coroutines.CoroutineExceptionHandler
就和APT类似,如果你玩过APT的话,肯定知道这一步是在做什么
下一篇预告:
- 协程执行流程 [入门理解挂起与恢复]
- delay() 与 Thread#sleep() 区别
原创不易,您的点赞就是对我最大的支持!