Kotlin协程用法

433 阅读6分钟

这是我参与更文挑战的第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。