前言
Kotlin协程底层是用线程实现的,是一个封装完善供开发者使用的线程框架。
Kotlin的一个协程可以理解为是运行在线程上的一个执行任务并且该任务可以在不同的线程间切换,一个线程可以同时运行多个协程。
从开发者角度来看:kotlin协程可以实现以同步的方式去编写异步执行的代码,解决线程切换回调的嵌套地狱。
协程挂起时不需要阻塞线程,几乎是无代价的。
创建协程的方式
runBlocking
这是一个顶层函数,会启动一个新的协程并阻塞调用它的线程,直到里面的代码执行完毕,返回值是泛型T。
1 | public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T |
CoroutineScope.launch
通过一个协程作用域的扩展方法launch启动一个协程,不会阻塞调用它的线程,返回值是Job。
1 | public fun CoroutineScope.launch( |
CoroutineScope.async
通过一个协程作用域的扩展方法async启动一个协程,不会阻塞调用它的线程,返回值是 Deferred。
1 | public fun <T> CoroutineScope.async( |
runBlocking方式因为会阻塞线程,所以runBlocking函数我们在开发中基本不会使用到,但可以用于代码调试。
我们一般使用后两种方式开启一个协程。
提前说一下async和launch的区别:
async函数体中最后一行代码表达式运行结果会作为结果返回,也就是Deferred中的泛型T,我们可以通过其他协程函数获取到这个执行结果,
而launch没有这样的返回值。
示例:
1 | import kotlinx.coroutines.GlobalScope |
作用域
1 | CoroutineScope(Dispatchers.IO).launch { |
和
1 | GlobalScope.launch(Dispatchers.IO){ |
这两种方式都是在指定的 IO 调度器中启动一个协程,但它们之间有一些区别:
GlobalScope.launch(Dispatchers.IO){}
是在全局范围内启动一个协程,不受外部作用域的限制。这意味着该协程的生命周期与应用程序的整个生命周期相关联,一般情况下不建议在生产代码中使用GlobalScope,因为它会使得协程的生命周期难于管理。
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 | GlobalScope.launch(Dispatchers.IO){ |
切换上下文
在 Kotlin 中,withContext
是一个用于切换协程上下文的函数。它属于 Kotlin 协程库,用于在协程内部改变执行的上下文(例如线程或调度器)。
那如果我们想在协程运行中改变线程怎么办?
最常见的,网络请求在IO线程,而页面更新在主线程。
Kotlin给我们提供了一个顶层函数withContext用于改变协程的上下文并执行一段代码。
1 | public suspend fun <T> withContext( |
示例:
1 | GlobalScope.launch(Dispatchers.Main) { |
注意事项
withContext
不会创建新的协程,它只是切换当前协程的上下文。withContext
是挂起函数,因此它只能在协程或其他挂起函数中调用。- 你可以在
withContext
块中执行任何需要的操作,但它的作用范围仅限于withContext
块中的代码。
使用 withContext
可以有效地管理协程的执行上下文,确保代码在适当的线程上运行,提高应用的性能和响应速度。
协程名称
1 | GlobalScope.launch(Dispatchers.Main + CoroutineName("主协程")) { |
打印结果:1
[CoroutineName(主协程), StandaloneCoroutine{Active}@288ff9, Dispatchers.Main]
子协程的协程作用域会继承父协程协程作用域里的 协程上下文
1 | GlobalScope.launch(Dispatchers.Main + CoroutineName("主协程")) { |
可以看到
1 | 协程的coroutineContext: [CoroutineName(主协程), StandaloneCoroutine{Active}@288ff9, Dispatchers.Main] |
Job与协程的生命周期
前面说launch和async两个扩展函数时,可以看到launch返回结果是一个Job,而async的返回结果是一个Deferred,Deferred其实是Job的子类。
那么Job是什么呢?
协程启动以后,我们可以得到一个Job对象,通过Job对象我们可以检测协程的生命周期状态,并且可以操作协程(比如取消协程)。
我们可以大致把Job理解为协程本身。
协程的生命周期:
- 协程创建以后,处于New(新建)状态,
- 协程启动(调用start()方法)以后,处于Active(活跃) 状态,
- 协程及所有子协程完成任务以后,处于Completed(完成) 状态,
- 协程被取消(调用cancel()方法)以后,处于Cancelled(取消) 状态
我们可以使用Job下面的字段检查协程的状态:
isActive
用于判断协程是否处于活跃状态isCancelled
用于判断协程是否被取消isCompleted
用于判断协程是否结束
除了获取协程状态,还有很多可以用于操纵协程的函数:
cancel()
取消协程。start()
启动协程。await()
等待协程执行完成
我们验证一下协程的生命周期:
1 | GlobalScope.launch { |
打印结果:
1 | 子协程的状态: true false false |
协程取消
1 | GlobalScope.launch { |
打印结果:
1 | 子协程的状态: true false false |
我们使用协程的生命周期验证一下子协程的第二个注意点:
如果父协程取消了,所有的子协程也会被取消
1 | var childJob : Job? = null |
打印结果如下:可以看到父协程取消以后,子协程也取消了。
1 | 父协程的状态: true false false |
挂起函数
Kotlin协程最大的优势就是以同步的方式写异步代码,这就是通过挂起函数用来实现。
被关键字suspend修饰的函数称为挂起函数,挂起函数只能在协程或者另一个挂起函数中调用。
挂起函数的特点是“挂起与恢复”,当协程遇到挂起函数时,协程会被挂起,等挂起函数执行完毕以后,协程会恢复到挂起的地方重新运行。
挂起是非阻塞性的挂起,不会阻塞线程;恢复不用我们手动恢复,而是协程帮我们完成。
顺序执行异步代码
1 | suspend fun returnNumber1() : Int { |
打印结果:1
2
3
4
5
6returnNumber1: 调用了returnNumber1()方法
number1: 需要获取number1
returnNumber1: 调用了returnNumber2()方法
number2: 需要获取number2
执行完毕: 3
运行时间: 3010
并行执行
1 | GlobalScope.launch(Dispatchers.Main) { |
打印结果如下:
1 | 开始运行第二个协程 |
协程的启动模式
我们在查看launch和async扩展函数时,还有第二个参数,start: CoroutineStart,这个参数的含义就是协程的启动模式,
1 | public enum class CoroutineStart { |
可以看到CoroutineStart是一个枚举类,有四种类型。
- DEFAULT默认启动模式,协程创建后立即开始调度,注意是立即调度而不是立即执行,可能在执行前被取消掉。
- LAZY懒汉启动模式,创建后不会有任何调度行为,直到我们需要它执行的时候才会产生调度。需要我们手动的调用Job的start、join或者await等函数时才会开始调度。
- ATOMIC 在协程创建后立即开始调度,但它和DEFAULT模式是有区别的,该模式下协程启动以后需要执行到第一个挂起点才会响应cancel操作。
- UNDISPATCHED协程在这种模式下会直接开始在当前线程下执行,直到运行到第一个挂起点。和ATOMIC很像,但UNDISPATCHED很受调度器的影响。
示例代码:
DEFAULT:代码立即打印,说明协程创建后立即调度
1 | GlobalScope.launch { |
LAZY:未调用start()方法前,无打印,调用start()方法后,代码打印。协程说明创建后不会调度,需要我们手动启动。
1 | val lazyJob = GlobalScope.launch(start = CoroutineStart.LAZY){ |
ATOMIC:协程运行后运行到第一个挂起函数后才会响应cancel()方法。
1 | val atomicJob = GlobalScope.launch(start = CoroutineStart.ATOMIC) { |
打印结果:1
atomic启动模式: 运行到挂起函数前
Jetpack Compose中使用协程
1 | val coroutineScope = rememberCoroutineScope() |
rememberCoroutineScope():
- 这是一个 Composable 函数,用于在 Composable 中创建一个记住的(remembered)协程作用域。
rememberCoroutineScope()
会创建一个协程作用域对象,并将其与当前 Composable 的生命周期相关联。
ViewModel中使用协程
1 | import androidx.lifecycle.ViewModel |