Skip to content

参考资料:https://juejin.cn/post/6908271959381901325#heading-0

什么是协程

一种处理并发(并发和并行的区别)的方式,用来简化Android平台下异步执行的代码

协程的优点

  • 轻量:可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作
  • 内存泄露更少:使用结构化并发机制在一个作用域内执行多个操作(如何理解)
  • 内置取消支持:取消功能会自动通过正在运行的协程层次结构传播
  • Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供你用于结构化并发

Android 平台下引入协程

kotlin
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'

第一个协程

kotlin
fun main(){
    GlobalScope.launch(Dispatchers.IO){
        delay(1000)
        log("launch")
    }
    Thread.sleep(2000)
    log("end")
}

fun log(msg:Any){
    println("[${Thread.currentThread().name}] $msg")
}

输出:

kotlin
[DefaultDispatcher-worker-1] launch
[main] end

suspend挂起函数

挂起函数不会阻塞其所在线程,而是会将协程挂起,在特定的时候才再恢复执行 **如何理解:**delay() 函数类似于 Java 中的 Thread.sleep(),而之所以说 delay() 函数是非阻塞的,是因为它和单纯的线程休眠有着本质的区别。例如,当在 ThreadA 上运行的 CoroutineA 调用了delay(1000L)函数指定延迟一秒后再运行,ThreadA 会转而去执行 CoroutineB,等到一秒后再来继续执行 CoroutineA。所以,ThreadA 并不会因为 CoroutineA 的延时而阻塞,而是能继续去执行其它任务,所以挂起函数并不会阻塞其所在线程,这样就极大地提高了线程的并发灵活度,最大化了线程的利用效率。而如果是使用Thread.sleep()的话,线程就只能干等着而不能去执行其它任务,降低了线程的利用效率 协程和线程的区别:

  1. 协程是运行于线程上的,一个线程可以运行多个(几千上万个)协程。
  2. 线程的调度行为是由操作系统来管理的,而协程的调度行为是可以由开发者来指定并由编译器来实现的
  3. 协程能够细粒度地控制多个任务的执行时机和执行线程,当线程所执行的当前协程被 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 输出日志

kotlin
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)
}

输出:

kotlin
[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 会阻塞其所在线程

kotlin
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")
}

输出:

kotlin
[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 本身带有阻塞线程的意味,但其内部运行的协程又是非阻塞的