参考资料:https://juejin.cn/post/6908271959381901325#heading-0
什么是协程
一种处理并发(并发和并行的区别)的方式,用来简化Android平台下异步执行的代码
协程的优点
- 轻量:可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作
- 内存泄露更少:使用结构化并发机制在一个作用域内执行多个操作(如何理解)
- 内置取消支持:取消功能会自动通过正在运行的协程层次结构传播
- Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供你用于结构化并发
Android 平台下引入协程
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
第一个协程
fun main(){
GlobalScope.launch(Dispatchers.IO){
delay(1000)
log("launch")
}
Thread.sleep(2000)
log("end")
}
fun log(msg:Any){
println("[${Thread.currentThread().name}] $msg")
}
输出:
[DefaultDispatcher-worker-1] launch
[main] end
suspend挂起函数
挂起函数不会阻塞其所在线程,而是会将协程挂起,在特定的时候才再恢复执行 **如何理解:**delay() 函数类似于 Java 中的 Thread.sleep(),而之所以说 delay() 函数是非阻塞的,是因为它和单纯的线程休眠有着本质的区别。例如,当在 ThreadA 上运行的 CoroutineA 调用了delay(1000L)函数指定延迟一秒后再运行,ThreadA 会转而去执行 CoroutineB,等到一秒后再来继续执行 CoroutineA。所以,ThreadA 并不会因为 CoroutineA 的延时而阻塞,而是能继续去执行其它任务,所以挂起函数并不会阻塞其所在线程,这样就极大地提高了线程的并发灵活度,最大化了线程的利用效率。而如果是使用Thread.sleep()的话,线程就只能干等着而不能去执行其它任务,降低了线程的利用效率 协程和线程的区别:
- 协程是运行于线程上的,一个线程可以运行多个(几千上万个)协程。
- 线程的调度行为是由操作系统来管理的,而协程的调度行为是可以由开发者来指定并由编译器来实现的
- 协程能够细粒度地控制多个任务的执行时机和执行线程,当线程所执行的当前协程被 suspend 后,该线程也可以腾出资源去处理其他任务
suspend挂起与恢复
CoroutineScope协程作用域
所有的协程都需要通过 CoroutineScope 来启动,它会跟踪通过 launch 或 async 创建的所有协程,你可以随时调用** scope.cancel()**
取消正在运行的协程。CoroutineScope 本身并不运行协程,它只是确保你不会失去对协程的追踪,即使协程被挂起也是如此。在 Android 中,某些 ktx 库为某些生命周期类提供了自己的 CoroutineScope,例如,ViewModel 有 viewModelScope,Lifecycle 有 lifecycleScope CoroutineScope分为三种:
- GlobalScope。即全局协程作用域,在这个范围内启动的协程可以一直运行直到应用停止运行。GlobalScope 本身不会阻塞当前线程,且启动的协程相当于守护线程,不会阻止 JVM 结束运行
- runBlocking。一个顶层函数,和 GlobalScope 不一样,它会阻塞当前线程直到其内部所有相同作用域的协程执行结束
- 自定义 CoroutineScope。可用于实现主动控制协程的生命周期范围,对于 Android 开发来说最大意义之一就是可以在 Activity、Fragment、ViewModel 等具有生命周期的对象中按需取消所有协程任务,从而确保生命周期安全,避免内存泄露
GlobalScope
GlobalScope 属于 全局作用域,这意味着通过 GlobalScope 启动的协程的生命周期只受整个应用程序的生命周期的限制,只要整个应用程序还在运行且协程的任务还未结束,协程就可以一直运行 GlobalScope 不会阻塞其所在线程,所以以下代码中主线程的日志会早于 GlobalScope 内部输出日志。此外,GlobalScope 启动的协程相当于守护线程,不会阻止 JVM 结束运行,所以如果将主线程的休眠时间改为三百毫秒的话,就不会看到 launch A 输出日志
fun main() {
log("start")
GlobalScope.launch {
launch {
delay(400)
log("launch A")
}
launch {
delay(300)
log("launch B")
}
log("GlobalScope")
}
log("end")
Thread.sleep(500)
}
输出:
[main] start
[main] end
[DefaultDispatcher-worker-1] GlobalScope
[DefaultDispatcher-worker-3] launch B
[DefaultDispatcher-worker-3] launch A
GlobalScope.launch 会创建一个顶级协程,尽管它很轻量级,但在运行时还是会消耗一些内存资源,且可以一直运行直到整个应用程序停止(只要任务还未结束),这可能会导致内存泄露,所以在日常开发中应该谨慎使用 GlobalScope https://blog.jetbrains.com/zh-hans/kotlin/2021/05/kotlin-coroutines-1-5-0-released/#%E5%90%88%E7%90%86%E7%9A%84%E7%94%A8%E6%B3%95
runBlocking
runBlocking 的一个方便之处就是:只有当内部相同作用域的所有协程都运行结束后,声明在 runBlocking 之后的代码才能执行,即 runBlocking 会阻塞其所在线程
fun main(){
log("start")
runBlocking {
launch {
repeat(3){
delay(100)
log("launchA - $it")
}
}
launch {
repeat(3){
delay(100)
log("launchB - $it")
}
}
GlobalScope.launch {
repeat(3){
delay(120)
log("GlobalScope - $it")
}
}
}
log("end")
}
private fun log(msg:Any){
println("[${Thread.currentThread().name}] $msg")
}
输出:
[main] start
[main] launchA - 0
[main] launchB - 0
[DefaultDispatcher-worker-1] GlobalScope - 0
[main] launchA - 1
[main] launchB - 1
[DefaultDispatcher-worker-1] GlobalScope - 1
[main] launchA - 2
[main] launchB - 2
[main] end
所以说,runBlocking 本身带有阻塞线程的意味,但其内部运行的协程又是非阻塞的