这是我参与更文挑战的第8天,活动详情查看: 更文挑战
一、概念
协程是一种并发设计模式,在android上可以让异步的代码并行执行,这个能有效提升代码开发效率及质量。
它运行在主线程上,延迟执行delay是一种特殊的挂起函数,不会导致主线程阻塞,也可以方便的在协程内进行线程切换。
这里涉及到几个概念:
挂起函数(suspend function): 一种可以暂停和恢复执行的特殊函数,它只能在协程或者其他挂起函数中调用。挂起函数可以让协程在等待某个结果的时候,释放当前线程,让其他协程继续执行,从而提高并发效率。
协程作用域(coroutine scope): 一种定义协程生命周期范围的对象,它可以管理协程的启动、取消和结构化并发。每个协程都必须在某个作用域内运行,当作用域被销毁时,它内部的所有协程都会被自动取消。
协程构建器(coroutine builder): 一种用于创建和启动协程的函数,它接收一个挂起函数作为参数,并返回一个协程对象。常见的协程构建器有launch和async两种,它们分别用于执行不返回结果和返回结果的异步任务
调度器(dispatcher): 一种决定协程在哪个线程上执行的对象,它可以指定协程运行的线程池或者线程模式。Kotlin提供了三种调度器,分别是Dispatchers.Main、Dispatchers.IO和Dispatchers.Default,它们分别适用于在主线程、后台线程执行I/O操作和后台线程执行CPU密集型操作的场景
上下文(context): 一种包含了协程相关信息的对象,例如调度器、作业、名称等。每个协程都有自己的上下文,可以通过coroutineContext属性访问。上下文可以在创建协程时指定,也可以在运行时切换
依赖:
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}
二、协程创建方式
1:GlobalScope.launch
一般不推荐使用,它可以让我们在应用程序的整个生命周期内创建和运行协程,因此使用不当,容易造成内存泄漏
btn.setOnClickListener{
GlobalScope.launch {
delay(500)
Log.e("TAG","aaaaaa")
}
Log.e("TAG","bbbbbb")
}
输出结果:
bbbbbb
aaaaaa
2:RunBlocking
会阻塞主线程
btn.setOnClickListener{
runBlocking {
delay(500)
Log.e("TAG","aaaaaa")
}
Log.e("TAG","bbbbbb")
}
aaaaaa
bbbbbb
3:by MainScope
class CoroutineActivity : AppCompatActivity(),CoroutineScope by MainScope() {
fun test(){
launch{
}
}
}
4:lifecycleScope.launch
lifecycleScope只能在Activity、Fragment中使用,会绑定Activity和Fragment的生命周期
需要依赖 implementation "androidx.lifecycle:lifecycle-runtime:2.5.1"
lifecycleScope.launch {
}
5:viewModelScope.launch
viewModelScope只能在ViewModel中使用,绑定ViewModel的生命周期
需依赖 implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
viewModelScope.launch {
}
6:val scope = CoroutineScope()
val scope = CoroutineScope(Job()+ CoroutineExceptionHandler { _, _ -> })
scope.launch {
}
三、挂起函数withContext 与 async
withContext 与 async 都可以返回耗时任务的执行结果。 一般来说,多个 withContext 任务是串行的, 且withContext 可直接返回耗时任务的结果。 多个 async 任务是并行的,async 返回的是一个Deferred,需要调用其await()方法才会执行
1:withContext
需要等待其执行完
btn.setOnClickListener {
CoroutineScope(Dispatchers.Main).launch {
val time1 = System.currentTimeMillis()
val task1 = withContext(Dispatchers.IO) { //运行到子线程
delay(2000)
Log.e("TAG", "aaaaaa")
"one" //返回结果赋值给task1
}
val task2 = withContext(Dispatchers.IO) {
delay(1000)
Log.e("TAG", "bbbbbb")
"two" //返回结果赋值给task2
}
Log.e("TAG", "cccccc")
}
}
结果:
aaaaaa
bbbbbb
cccccc
2:async
可以并行执行
val task1 = async(Dispatchers.IO) {
delay(2000)
Log.e("TAG", "aaaaaa")
"one" //返回结果赋值给task1
}
val task2 = async(Dispatchers.IO) {
delay(1000)
Log.e("TAG", "bbbbbb")
"two" //返回结果赋值给task2
}
Log.e("TAG", "task1 = ${task1.await()} , task2 = ${task2.await()},耗时两秒")
结果:
bbbbbb
aaaaaa
task1=one,taks2=two
//如果await直接跟在async后面,则耗时效果跟withContext一样
val task1 = async(Dispatchers.IO) {
delay(2000)
Log.e("TAG", "aaaaaa")
"one" //返回结果赋值给task1
}.await()
val task2 = async(Dispatchers.IO) {
delay(1000)
Log.e("TAG", "bbbbbb")
"two" //返回结果赋值给task2
}.await()
Log.e("TAG", "task1 = 耗时三秒")
结果:
bbbbbb
aaaaaa
task1=one,taks2=two
四、协程异常处理
如果在协程中抛出异常,则协程将被取消。协程的所有子程序也将被取消,并且这些协程中的任何未完成的工作都将丢失。
1:异常传播流程
1:先 cancel 子协程
2:取消自己
3:将异常传递给父协程 4:(重复上述过程,直到根协程关闭)
GlobalScope.launch {
val parentJob = launch {
val childJob1 = launch {
delay(1000)
Log.d("TAG","aaaaaaaaaaaaaaaaaaaaa")
throw NullPointerException() //会导致父协程任务和兄弟协程任务都会被取消
}
val childJob2 = launch {
delay(2000)
Log.d("TAG","bbbbbbbbbbbbbbbbbbbbbbbb")
}
delay(5000)
Log.d("TAG","cccccccccccccccccccccc")
}
Log.d("TAG","dddddddddddddddddddddd")
}
结果:
dddddddddddddddddddddd
aaaaaaaaaaaaaaaaaaaaaa
2:CoroutineExceptionHandler
其是用于在协程中全局捕获异常行为的最后一种机制,你可以理解为,类似 Thread.uncaughtExceptionHandler 一样。
需要在最顶层协程作用域才能捕获,否则还是会引起闪退
这种方式依然捕获不住异常,程序闪退
val scope = CoroutineScope(Job())
scope.launch() {
launch(CoroutineExceptionHandler { _, _ -> }) {
delay(10)
throw RuntimeException()
}
}
这种方式可以正常捕获住异常,程序不会闪退
val scope = CoroutineScope(Job()+ CoroutineExceptionHandler { _, _ -> })
scope.launch {
launch{
delay(10)
throw RuntimeException()
}
}
3:SupervisorJob
supervisorJob 是一个特殊的Job,其会改变异常的传递方式,当使用它时,我们子协程的失败不会影响到其他子协程与父协程,也就是:子协程会自己处理异常,并不会影响其兄弟协程或者父协程。
结果正常输出:
val scope = CoroutineScope(SupervisorJob() + CoroutineExceptionHandler { _, _ -> })
scope.launch(CoroutineName("A")) {
delay(10)
throw RuntimeException()
}
scope.launch(CoroutineName("B")) {
delay(100)
Log.e("TAG", "aaaaaaaa")
}
结果:
aaaaaaaa
结果不能输出被异常打断
子协程在 launch 时会创建新的协程作用域,其会使用默认新的 Job 替代我们传递 SupervisorJob ,所以导致我们传递的 SupervisorJob 被覆盖。所以如果我们想让子协程不影响父协程或者其他子协程,此时就必须再显示添加 SupervisorJob。
val scope = CoroutineScope(CoroutineExceptionHandler { _, _ -> })
scope.launch(SupervisorJob()) {
launch(CoroutineName("A")) {
delay(10)
throw RuntimeException()
}
launch(CoroutineName("B")) {
delay(100)
Log.e("TAG", "aaaaaaaa")
}
}
修正方式
val scope = CoroutineScope(CoroutineExceptionHandler { _, _ -> })
scope.launch {
launch(CoroutineName("A") + SupervisorJob()) {
delay(10)
throw RuntimeException()
}
launch(CoroutineName("B")) {
delay(200)
Log.e("TAG", "aaaaaaaa")
}
}
结果:
aaaaaaaa
综合示例:
-----------------------------------------------------------------------
未添加异常捕获,出现异常父子协程最终会被取消
GlobalScope.launch() {
launch {
val childJob1 = launch(SupervisorJob()) {
delay(1000)//子任务做一些事情
Log.d("TAG", "aaaaaaaaaaaaaaaaaaaaaaa")
throw NullPointerException()
}
val childJob2 = launch() {
delay(2000)//子任务做一些事情
Log.d("TAG", "bbbbbbbbbbbbbbbbbbbbbbbb")
}
delay(5000)
Log.d("TAG", "cccccccccccccccccccccc")
}
Log.d("TAG","dddddddddddddddddddddddd")
}
结果:
dddddddddddddddddddddddd
aaaaaaaaaaaaaaaaaaaaaaaa
-----------------------------------------------------------------------
在最顶层添加异常捕获,结合SupervisorJob(),子协程的异常,不会影响父兄弟协程
GlobalScope.launch(CoroutineExceptionHandler { _, _ -> }) {
launch {
val childJob1 = launch(SupervisorJob()) {
delay(1000)//子任务做一些事情
Log.d("TAG", "aaaaaaaaaaaaaaaaaaaaaaaa")
throw NullPointerException()
}
val childJob2 = launch() {
delay(2000)//子任务做一些事情
Log.d("TAG", "bbbbbbbbbbbbbbbbbbbbbbbb")
}
delay(5000)
Log.d("TAG", "cccccccccccccccccccccc")
}
Log.d("TAG","dddddddddddddddddddddddd")
}
结果:
dddddddddddddddddddddddd
aaaaaaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccc
-----------------------------------------------------------------------
SupervisorJob()放到childJob1的父协程,子协程默认会用新的Job替代父协程传递SupervisorJob,导致失效
GlobalScope.launch(CoroutineExceptionHandler { _, _ -> }) {
launch(SupervisorJob()) {
val childJob1 = launch(CoroutineName("A")) {
delay(1000)//子任务做一些事情
Log.d("TAG", "aaaaaaaaaaaaaaaaaaaaaaa")
throw NullPointerException()
}
val childJob2 = launch(CoroutineName("B")) {
delay(2000)//子任务做一些事情
Log.d("TAG", "bbbbbbbbbbbbbbbbbbbbbbbb")
}
delay(5000)
Log.d("TAG", "cccccccccccccccccccccc")
}
Log.d("TAG","dddddddddddddddddddddddd")
}
结果:
dddddddddddddddddddddddd
aaaaaaaaaaaaaaaaaaaaaaa
SupervisorJob 可以用来改变我们的协程异常传递方式,从而让子协程自行处理异常。但需要注意的是,因为协程具有结构化的特点,SupervisorJob 仅只能用于同一级别的子协程。如果我们在初始化 scope 时添加了 SupervisorJob ,那么整个scope对应的所有 根协程 都将默认携带 SupervisorJob ,否则就必须在 CoroutineContext 显示携带 SupervisorJob。