Kotlin中的协程及在Android中的应用

前言

Kotlin协程底层是用线程实现的,是一个封装完善供开发者使用的线程框架。

Kotlin的一个协程可以理解为是运行在线程上的一个执行任务并且该任务可以在不同的线程间切换,一个线程可以同时运行多个协程。

从开发者角度来看:kotlin协程可以实现以同步的方式去编写异步执行的代码,解决线程切换回调的嵌套地狱。

协程挂起时不需要阻塞线程,几乎是无代价的。

创建协程的方式

runBlocking

这是一个顶层函数,会启动一个新的协程并阻塞调用它的线程,直到里面的代码执行完毕,返回值是泛型T。

1
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T

CoroutineScope.launch

通过一个协程作用域的扩展方法launch启动一个协程,不会阻塞调用它的线程,返回值是Job。

1
2
3
4
5
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job

CoroutineScope.async

通过一个协程作用域的扩展方法async启动一个协程,不会阻塞调用它的线程,返回值是 Deferred。

1
2
3
4
5
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T>

runBlocking方式因为会阻塞线程,所以runBlocking函数我们在开发中基本不会使用到,但可以用于代码调试。

我们一般使用后两种方式开启一个协程。

提前说一下async和launch的区别:

async函数体中最后一行代码表达式运行结果会作为结果返回,也就是Deferred中的泛型T,我们可以通过其他协程函数获取到这个执行结果,

而launch没有这样的返回值。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

runBlocking {
Log.e("协程","我们使用runBlocking启动了一个协程")
}

GlobalScope.async {
Log.e("协程","我们使用async启动了一个协程")
}

GlobalScope.launch {
Log.e("协程","我们使用launch启动了一个协程")
}

作用域

1
2
CoroutineScope(Dispatchers.IO).launch {
}

1
2
GlobalScope.launch(Dispatchers.IO){
}

这两种方式都是在指定的 IO 调度器中启动一个协程,但它们之间有一些区别:

  1. GlobalScope.launch(Dispatchers.IO){} 是在全局范围内启动一个协程,不受外部作用域的限制。

    这意味着该协程的生命周期与应用程序的整个生命周期相关联,一般情况下不建议在生产代码中使用GlobalScope,因为它会使得协程的生命周期难于管理。

  2. CoroutineScope(Dispatchers.IO).launch {} 是在指定的 CoroutineScope 中启动一个协程,通常情况下应该手动创建 CoroutineScope 对象,并确保在合适的时机取消该 CoroutineScope 以避免内存泄漏。这样做更加可控,可以更好地管理协程的生命周期。

因此,建议在大多数情况下使用 CoroutineScope 来启动协程,以便更好地管理协程的生命周期。

协程调度器

Kotlin给我们提供了四种调度器

  • Default:默认调度器,CPU密集型任务调度器,通常处理一些单纯的计算任务,或者执行时间较短任务。例如数据计算
  • IO:IO调度器,IO密集型任务调度器,适合执行IO相关操作。比如:网络请求,数据库操作,文件操作等
  • Main:UI调度器,只有在UI编程平台上有意义,用于更新UI,例如Android中的主线程
  • Unconfined:非受限调度器,无所谓调度器,当前协程可以运行在任意线程上

GlobalScope的协程调度器是Dispatchers.Default,那么我们如何改变呢?

我们前面查看launch和async方法时,看到他们的第一个参数都是context: CoroutineContext ,是的,我们可以从这里传入我们需要的上下文,并且会覆盖掉协程作用域里的上下文。
如下:我们希望开启一个协程运行在IO线程上

1
2
3
GlobalScope.launch(Dispatchers.IO){
Log.e("协程的coroutineContext",this.coroutineContext.toString())
}

切换上下文

在 Kotlin 中,withContext 是一个用于切换协程上下文的函数。它属于 Kotlin 协程库,用于在协程内部改变执行的上下文(例如线程或调度器)。

那如果我们想在协程运行中改变线程怎么办?

最常见的,网络请求在IO线程,而页面更新在主线程。

Kotlin给我们提供了一个顶层函数withContext用于改变协程的上下文并执行一段代码。

1
2
3
4
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T

示例:

1
2
3
4
5
6
7
GlobalScope.launch(Dispatchers.Main) {
val result = withContext(Dispatchers.IO) {
//网络请求
"返回结果"
}
mBtn.text = result
}

注意事项

  • withContext 不会创建新的协程,它只是切换当前协程的上下文。
  • withContext 是挂起函数,因此它只能在协程或其他挂起函数中调用。
  • 你可以在 withContext 块中执行任何需要的操作,但它的作用范围仅限于 withContext 块中的代码。

使用 withContext 可以有效地管理协程的执行上下文,确保代码在适当的线程上运行,提高应用的性能和响应速度。

协程名称

1
2
3
GlobalScope.launch(Dispatchers.Main + CoroutineName("主协程")) {
Log.e("协程的coroutineContext",this.coroutineContext.toString())
}

打印结果:

1
[CoroutineName(主协程), StandaloneCoroutine{Active}@288ff9, Dispatchers.Main]

子协程的协程作用域会继承父协程协程作用域里的 协程上下文

1
2
3
4
5
6
GlobalScope.launch(Dispatchers.Main + CoroutineName("主协程")) {
Log.e("协程的coroutineContext" , this.coroutineContext.toString())
launch {
Log.e("协程的coroutineContext2" , this.coroutineContext.toString())
}
}

可以看到

1
2
协程的coroutineContext: [CoroutineName(主协程), StandaloneCoroutine{Active}@288ff9, Dispatchers.Main]
协程的coroutineContext2: [CoroutineName(主协程), StandaloneCoroutine{Active}@b95b3e, Dispatchers.Main]

Job与协程的生命周期

前面说launch和async两个扩展函数时,可以看到launch返回结果是一个Job,而async的返回结果是一个Deferred,Deferred其实是Job的子类。

那么Job是什么呢?

协程启动以后,我们可以得到一个Job对象,通过Job对象我们可以检测协程的生命周期状态,并且可以操作协程(比如取消协程)。

我们可以大致把Job理解为协程本身。

协程的生命周期:

  1. 协程创建以后,处于New(新建)状态,
  2. 协程启动(调用start()方法)以后,处于Active(活跃) 状态,
  3. 协程及所有子协程完成任务以后,处于Completed(完成) 状态,
  4. 协程被取消(调用cancel()方法)以后,处于Cancelled(取消) 状态

我们可以使用Job下面的字段检查协程的状态:

  • isActive 用于判断协程是否处于活跃状态
  • isCancelled 用于判断协程是否被取消
  • isCompleted用于判断协程是否结束

除了获取协程状态,还有很多可以用于操纵协程的函数:

  • cancel()取消协程。
  • start()启动协程。
  • await() 等待协程执行完成

我们验证一下协程的生命周期:

1
2
3
4
5
6
7
8
GlobalScope.launch {
val job = launch(CoroutineName("子协程")) {

}
Log.e("子协程的状态","${job.isActive} ${job.isCancelled} ${job.isCompleted}")
delay(1000)
Log.e("子协程的状态2","${job.isActive} ${job.isCancelled} ${job.isCompleted}")
}

打印结果:

1
2
子协程的状态: true false false
子协程的状态2: false false true

协程取消

1
2
3
4
5
6
7
8
GlobalScope.launch {
val job = launch(CoroutineName("子协程")) {
delay(5000)
}
Log.e("子协程的状态","${job.isActive} ${job.isCancelled} ${job.isCompleted}")
job.cancel()
Log.e("取消后子协程的状态","${job.isActive} ${job.isCancelled} ${job.isCompleted}")
}

打印结果:

1
2
子协程的状态: true false false
取消后子协程的状态: false true false

我们使用协程的生命周期验证一下子协程的第二个注意点:

如果父协程取消了,所有的子协程也会被取消

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var childJob : Job? = null
val parentJob = GlobalScope.launch {
childJob = launch(CoroutineName("子协程")) {
delay(5000)
}
}
Log.e("父协程的状态" , "${parentJob.isActive} ${parentJob.isCancelled} ${parentJob.isCompleted}")
Handler().postDelayed(Runnable {
Log.e("子协程的状态" ,
"${childJob?.isActive} ${childJob?.isCancelled} ${childJob?.isCompleted}")
parentJob.cancel()
Log.e("父协程的状态" ,
"${parentJob.isActive} ${parentJob.isCancelled} ${parentJob.isCompleted}")
Log.e("子协程的状态" ,
"${childJob?.isActive} ${childJob?.isCancelled} ${childJob?.isCompleted}")
} , 1000)

打印结果如下:可以看到父协程取消以后,子协程也取消了。

1
2
3
4
父协程的状态: true false false
子协程的状态: true false false
父协程的状态: false true false
子协程的状态: false true false

挂起函数

Kotlin协程最大的优势就是以同步的方式写异步代码,这就是通过挂起函数用来实现。

被关键字suspend修饰的函数称为挂起函数,挂起函数只能在协程或者另一个挂起函数中调用。

挂起函数的特点是“挂起与恢复”,当协程遇到挂起函数时,协程会被挂起,等挂起函数执行完毕以后,协程会恢复到挂起的地方重新运行。
挂起是非阻塞性的挂起,不会阻塞线程;恢复不用我们手动恢复,而是协程帮我们完成。

顺序执行异步代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
suspend fun returnNumber1() : Int {
delay(1000L)
Log.e("returnNumber1" , "调用了returnNumber1()方法")
return 1
}

suspend fun returnNumber2() : Int {
delay(2000L)
Log.e("returnNumber1" , "调用了returnNumber2()方法")
return 2
}
GlobalScope.launch {
val time = measureTimeMillis {
val number1 = returnNumber1()
Log.e("number1" , "需要获取number1")
val number2 = returnNumber2()
Log.e("number2" , "需要获取number2")
val result = number1 + number2
Log.e("执行完毕" , result.toString())
}
Log.e("运行时间",time.toString())
}

打印结果:

1
2
3
4
5
6
returnNumber1: 调用了returnNumber1()方法
number1: 需要获取number1
returnNumber1: 调用了returnNumber2()方法
number2: 需要获取number2
执行完毕: 3
运行时间: 3010

并行执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GlobalScope.launch(Dispatchers.Main) {
val time = measureTimeMillis {
val deferred1 = async {
Log.e("--" , "子协程1运行开始")
returnNumber1()
}
Log.e("--" , "开始运行第二个协程")
val deferred2 = async {
Log.e("--" , "子协程2运行开始")
returnNumber2()
}
Log.e("--" , "开始计算结果")

val result = deferred1.await() + deferred2.await()
Log.e("执行完毕" , result.toString())

}
Log.e("运行时间" , time.toString())
}

打印结果如下:

1
2
3
4
5
6
7
8
开始运行第二个协程
开始计算结果
子协程1运行开始
子协程2运行开始
returnNumber1: 调用了returnNumber1()方法
returnNumber1: 调用了returnNumber2()方法
执行完毕: 3
运行时间: 2009

协程的启动模式

我们在查看launch和async扩展函数时,还有第二个参数,start: CoroutineStart,这个参数的含义就是协程的启动模式,

1
2
3
4
5
6
public enum class CoroutineStart {
DEFAULT,
LAZY,
ATOMIC,
UNDISPATCHED;
}

可以看到CoroutineStart是一个枚举类,有四种类型。

  • DEFAULT默认启动模式,协程创建后立即开始调度,注意是立即调度而不是立即执行,可能在执行前被取消掉。
  • LAZY懒汉启动模式,创建后不会有任何调度行为,直到我们需要它执行的时候才会产生调度。需要我们手动的调用Job的start、join或者await等函数时才会开始调度。
  • ATOMIC 在协程创建后立即开始调度,但它和DEFAULT模式是有区别的,该模式下协程启动以后需要执行到第一个挂起点才会响应cancel操作。
  • UNDISPATCHED协程在这种模式下会直接开始在当前线程下执行,直到运行到第一个挂起点。和ATOMIC很像,但UNDISPATCHED很受调度器的影响。

示例代码:
DEFAULT:代码立即打印,说明协程创建后立即调度

1
2
3
GlobalScope.launch {
Log.e("default启动模式", "协程运行")
}

LAZY:未调用start()方法前,无打印,调用start()方法后,代码打印。协程说明创建后不会调度,需要我们手动启动。

1
2
3
4
val lazyJob = GlobalScope.launch(start = CoroutineStart.LAZY){
Log.e("lazy启动模式", "协程运行")
}
//lazyJob.start()

ATOMIC:协程运行后运行到第一个挂起函数后才会响应cancel()方法。

1
2
3
4
5
6
val atomicJob = GlobalScope.launch(start = CoroutineStart.ATOMIC) {
Log.e("atomic启动模式" , "运行到挂起函数前")
delay(100)
Log.e("atomic启动模式" , "运行到挂起函数后")
}
atomicJob.cancel()

打印结果:

1
atomic启动模式: 运行到挂起函数前

Jetpack Compose中使用协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val coroutineScope = rememberCoroutineScope()

coroutineScope.launch {
// 在后台线程执行耗时操作
val result = withContext(Dispatchers.IO) {
// 执行耗时操作,例如网络请求或数据库查询
// 等待3秒钟
Log.i("Thread", "请求数据 Current thread: ${Thread.currentThread().name}")
delay(3000L)
"我是返回的数据"
}

// 在主线程更新 UI
Log.i("Thread", "Data: ${result} Current thread: ${Thread.currentThread().name}")
}

rememberCoroutineScope()

  • 这是一个 Composable 函数,用于在 Composable 中创建一个记住的(remembered)协程作用域。
  • rememberCoroutineScope() 会创建一个协程作用域对象,并将其与当前 Composable 的生命周期相关联。

ViewModel中使用协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch

class MyViewModel : ViewModel() {
private val viewModelJob = Job()
private val viewModelScope = CoroutineScope(Dispatchers.Main + viewModelJob)

fun performAsyncTask() {
viewModelScope.launch {
// 在这里执行异步操作,例如发起网络请求或读取数据库
// 例如
val result = fetchDataFromNetwork()
// 更新界面数据
updateUiWithData(result)
}
}

private suspend fun fetchDataFromNetwork(): String {
// 模拟一个网络请求
return "Fake network data"
}

private fun updateUiWithData(data: String) {
// 更新界面数据
}

override fun onCleared() {
super.onCleared()
viewModelJob.cancel() // 在 ViewModel 被清除时取消所有相关的协程
}
}