协程基础与关键知识

191 阅读23分钟

Executor 和 CoroutinContenxt

一、Executor vs. CoroutineContext

  • Executor

    • Java 的线程池接口,只负责将 Runnable 提交到某个线程执行;
    • 不包含“主线程”概念,纯粹的线程调度器。
  • CoroutineContext

    • Kotlin 协程的上下文,功能更强:不仅可以指定线程池(CoroutineDispatcher),还可携带作业(Job)、名称(CoroutineName)等元素;
    • launch { … } 并非真正“新建线程”,而是将协程调度到 CoroutineContext 指定的线程或线程池执行。

补充:在协程框架内部,负责线程切换的接口是 ContinuationInterceptor,其常见实现就是各种 DispatchersDefault,处理计算类型的调度器,没有什么IO,线程池大小一般就是与CPU的核心数量。

二、常见调度器(Dispatchers

调度器用途线程池大小
DefaultCPU 密集型任务(计算、排序、解析等),线程数≈CPU 核心数与 CPU 核心数相当
IOIO 密集型任务(磁盘读写、网络请求等),线程数较多以提升并发性能默认 ≈64(可动态伸缩)
Unconfined不强制切换线程:协程在当前调用点继续执行,直到首次挂起;挂起后恢复也不保证切回原线程
MainAndroid 主线程,用于更新 UI主线程

Unconfined:不进行线程管理的一个分发器。写的代码在哪里就在那里执行,甚至是挂起行数里边的线程继续执行,不会有任何的切线程操作。

三、线程状态与 IO 操作(示意)

   发起 IO 调用       IO 完成中断
       |                  |
       v                  v
  Running  →  Blocked  →  Ready  →  Running
(发出系统调用) (线程休眠) (等待 CPU) (继续执行)
  • IO 操作 发起后,线程进入 Blocked,不占用 CPU;IO 完成后由操作系统中断唤醒,再进入 Ready 等待调度。

四、挂起函数与协程“挂起/恢复”

  • 挂起函数(如 withContextdelay):

    • 不会阻塞底层线程,而是挂起当前协程,释放线程给其它任务;
    • 挂起发生时,协程保存现场,挂起函数内部可切换线程继续执行其它协程逻辑。
  • 示例:线程切换与顺序执行

    CoroutineScope(Dispatchers.Main).launch {
      // 切到 IO 线程执行网络请求
      val data: String = withContext(Dispatchers.IO) {
        // 网络代码
        "data"
      }
    
      // 切到 Default(CPU 线程池)执行数据处理
      val processed: String = withContext(Dispatchers.Default) {
        // 处理代码
        "processed $data"
      }
    
      println("Result: $processed")
    }
    
    • withContext 保证两段代码 串行 执行;
    • withContext 的调度器与当前相同,则不会真正切线程。

在Activity 或者Fragment中应该使用lifecyclerScope,因为这个是和页面的生命周期绑定的。如果是用ViewModel组件,最好使用ViewModelScope去开启协程。

五、协程启动与作用域

  • GlobalScope

    GlobalScope.launch(Dispatchers.Main) { … }
    
    • 全局协程,不绑定任何生命周期,小心内存泄漏。
  • lifecycleScope(Activity/Fragment)

    lifecycleScope.launch { … }
    
    • 与页面生命周期绑定,页面销毁时自动取消协程;
    • 默认派发到主线程,无需手动 Dispatchers.Main
  • viewModelScope(ViewModel)

    viewModelScope.launch { … }
    
    • 与 ViewModel 生命周期绑定,ViewModel 清除时取消。

lifecycleScope vs. viewModelScope

特性lifecycleScopeviewModelScope
所属组件ComponentActivity / FragmentViewModel
绑定的生命周期视图 生命周期绑定,onDestroy() 时取消ViewModel 生命周期绑定,onCleared() 时取消
默认调度器Dispatchers.Main(主线程)Dispatchers.Main.immediate(主线程、立即执行)
典型用例界面相关的协程:更新 UI、短时用户交互与业务逻辑相关的协程:数据请求、缓存、长时计算
防止泄漏页面销毁自动取消,避免在后台继续运行ViewModel 销毁自动取消,避免持久任务泄漏

六、 launch(并行) vs. withContext(串行)

  • launch { … }

    • 启动一个新的协程,不返回结果,只返回一个 Job
    • 多次调用会并行执行,彼此之间不等待
  • withContext(dispatcher) { … }

    • 是一个挂起函数,会切换到指定调度器执行,并挂起当前协程直到代码块完成;
    • 保证调用顺序——后续代码要等该块执行完再继续;
    • dispatcher 与当前相同,则不会实际切线程。

示例:并行(非预期串行)

// 在 Main 调度器启动
CoroutineScope(Dispatchers.Main).launch {
  // ① 这个 launch 启动一个子协程,但当前协程并不等它完成就结束
  launch {
    // 这里的“网络代码”会异步执行,其返回值不会被外层协程使用
    val data = fetchNetworkData()
    println("Inner data: $data")
  }
  // 外层协程到这里就已经结束,inner 仍在后台跑
}

示例:串行(正确顺序)

CoroutineScope(Dispatchers.Main).launch {
  // 切到 IO 线程做网络请求,挂起当前协程直到完成
  val data: String = withContext(Dispatchers.IO) {
    fetchNetworkData()
  }
  // 切回 Default 线程做密集计算
  val processed: String = withContext(Dispatchers.Default) {
    processData(data)
  }
  // 回到 Main,安全更新 UI
  println("Processed data: $processed")
}

七、线程切换规则

  1. 切换条件

    • 只有在 withContext 或其它挂起点指定了不同的 CoroutineDispatcher 时,才会发生真正的线程切换;
    • 相同调度器下的 withContext 不触发额外切换。
  2. IO vs. CPU 密集

    • IO 密集(网络、磁盘、数据库)用 Dispatchers.IO
    • CPU 密集(复杂计算、排序、加解密)用 Dispatchers.Default
    • UI 更新短任务Dispatchers.Main
  3. 避免卡顿

    • 不要在 Main 线程执行耗时逻辑,务必用 withContext(Dispatchers.IO/Default) 或在后台协程里执行。
  4. Scope 选择

    • 界面相关:用 lifecycleScope.launch { … },Activity/Fragment 销毁自动取消;
    • 业务/数据:用 viewModelScope.launch { … },ViewModel 清除自动取消;
    • 全局任务:慎用 GlobalScope,易造成泄漏。

总结

  • launch 快速并行起协程;
  • withContext 串行挂起、切换线程并获取结果;
  • 选择合适的 Scope,结合调度器,才能写出既高效又可靠的协程代码。

挂起函数为什么不卡线程?

因为实际任务的执行是在另外的线程。并非当前的线程。切换的过程类似于回调的方式。

结构化并发

GCroot有哪些?对于这个问题,你只需要知道,一个对象在什么样的情况下不被回收,其实就能比较清楚了:

  1. 被Static引用的对象,static的生命周期是整个app的生命周期,因此他引用的对象都是不会被回收的
  2. 活跃的线程,就是runable或者running状态的线程。
  3. 来自JNI(Native)对象的引用,因为JNI已经脱离了Java的范畴,JVM无法判断这个对象是否可以被回收,因此统一认为他无法被回收。

一、GC Roots

在 JVM 中,对象只有在“不可达”时才会被回收。常见的 GC Roots 包括:

  1. 静态引用

    • static 字段引用的对象,其生命周期与应用相同,JVM 不会回收。
  2. 活跃线程栈帧

    • 处于 RunnableRunning 状态的线程的栈帧中引用的对象。
  3. JNI(Native)引用

    • 来自本地代码的引用不在 JVM 可控范围内,也被视为GC Root。

二、协程取消

// 启动一个协程并获得 Job
val job = lifecycleScope.launch {
    launch { /* 子协程 A */ }
}

// 只取消 job 及其子协程
job.cancel()

// 取消整个 Scope(及其所有子协程)
// 一般不手动调用,因为 lifecycleScope 会在 onDestroy 自动取消
lifecycleScope.cancel()
  • job.cancel():取消该 Job 以及它启动的子协程。
  • scope.cancel():取消所有由该 Scope 启动的协程。

三、协程线程切换

协程切换线程有两种方式:

  1. launch 启动 —— 并行执行
    每次调用都会并行启动一个新的协程,互不等待

    CoroutineScope(Dispatchers.Main).launch {
      // 第一个协程体
      launch {
        // 这是第二个协程,与外层并行
        val data = fetchNetworkData()
        println("Inner data: $data")
      }
      // 外层协程不会等待内层完成就结束
    }
    
  2. withContext 挂起 —— 串行执行
    是挂起函数:切换到指定调度器执行、挂起当前协程,直到代码块完成,然后恢复。

    CoroutineScope(Dispatchers.Main).launch {
      // 切到 IO 线程,挂起当前协程直到完成
      val data: String = withContext(Dispatchers.IO) {
        fetchNetworkData()
      }
      // 切到 Default 线程处理
      val processed: String = withContext(Dispatchers.Default) {
        processData(data)
      }
      println("Processed data: $processed")
    }
    

注意:如果 withContext 的调度器与当前相同,则 不会真正切线程。

四、结构化并发(Structured Concurrency)

Kotlin 协程通过父子关系和作用域,保证并发任务的可控、可取消、可组合。

4.1 launch vs. async

  • launch { … }

    • 启动一个不返回结果的协程,返回 Job,用于并行执行。
  • async { … }

    • 启动一个返回结果的协程,返回 Deferred<T>,通过 await() 获取结果。
4.2. launch + async 示例
4.2.1 先 asynclaunch + coroutineScope
// 提前启动一个 Deferred,用于并行获取 contributors
val deferred = lifecycleScope.async {
  gitHub.contributors("square", "retrofit")
}

lifecycleScope.launch {
// 添加这个代码的原因是为异常的结构化管理。
// 下边这三行中的 contributors1 与 contributors2 是并行的。不能添加lifecycleScope.,
// 否则会导致外层的lifecycleScope.无法管理内部的写成,因为加了lifecycleScope.就不是外部的子协程了。
  coroutineScope {
    // 这两行是并行执行的:一行直接调用,一行从 deferred 拿结果
    val contributors1 = gitHub.contributors("square", "retrofit")
    val contributors2 = deferred.await()
    // 合并并展示
    showContributors(contributors1 + contributors2)
  }
}
  • 要点

    • deferredcontributors1 会并行发起网络请求。
    • coroutineScope { … } 确保内部子协程的异常能被统一捕获和处理。

4.2.2 推荐写法:在同一 coroutineScope 内启动两个 async
lifecycleScope.launch {
// 添加这个代码的原因是为异常的结构化管理。
// 下边这三行中的 deferred1 与 deferred2 是并行的。不能添加lifecycleScope.,
// 否则会导致外层的lifecycleScope.无法管理内部的写成,因为加了lifecycleScope.就不是外部的子协程了。
  coroutineScope {
    // 并行启动两个异步任务
    val deferred1 = async { gitHub.contributors("square", "retrofit") }
    val deferred2 = async { gitHub.contributors("square", "okhttp") }

    // await() 会挂起直到各自完成,然后合并结果
    showContributors(deferred1.await() + deferred2.await())
  }
}

注意

  • 不要在内部再用 lifecycleScope.async,那样会脱离外层 coroutineScope 的管理,无法保证结构化并发。
  • async { … } 默认继承外层 CoroutineScope

4.2.3 等价的 CompletableFuture 写法(Java 风格)
private fun completableFutureStyleMerge() {
  val future1 = gitHub.contributorsFuture("square", "retrofit")
  val future2 = gitHub.contributorsFuture("square", "okhttp")

  future1
    .thenCombine(future2) { c1, c2 -> c1 + c2 }
    .thenAccept { merged ->
      handler.post { showContributors(merged) }
    }
}

// Retrofit + CompletableFuture 的封装方法签名
fun contributorsFuture(
  @Path("owner") owner: String,
  @Path("repo") repo: String
): CompletableFuture<List<Contributor>>

4.3. 并行流程仅需顺序依赖(不需要结果)

lifecycleScope.launch {
  // 两个初始化任务:一个 launch(无返回值),一个 async(可 await)
  val initJob  = launch { init() }
  val initJob2 = async  { init2() }

  // 并行发起网络请求
  val contributors1 = gitHub.contributors("square", "retrofit")

  // 等待 initJob 完成(不取结果)
  initJob.join()

  // 等待 initJob2 完成,并取其结果(如果需要)
  val init2Result = initJob2.await()

  // 继续后续处理
  processData(contributors1, init2Result)
}
  • 区别

    • join():挂起当前协程直到目标 Job 完成,但不返回结果。
    • await():挂起当前协程直到 Deferred 完成,并返回结果;若只是用来等待也可以,但必须用 async 启动。

五、join vs. await

  • Job.join()

    • 挂起当前协程直到目标协程完成,但返回结果;
  • Deferred<T>.await()

    • 挂起当前协程直到目标协程完成,并返回结果。

示例:join 的执行流程

import kotlinx.coroutines.*

fun main() = runBlocking {
  val job1 = launch {
    delay(1_000L)
    println("Job1 completed")
  }

  val job2 = launch {
    println("Job2 waiting for Job1")
    job1.join()             // 等待 job1 完成
    println("Job2 starts after Job1")
  }

  println("Main starts")
  job2.join()               // 等待 job2 完成
  println("Main ends")
}

执行顺序:

  1. runBlocking 阻塞主线程,直到内部所有协程完成。
  2. job1 延迟 1s 打印 Job1 completed
  3. job2 先打印 Job2 waiting for Job1,然后在 job1.join() 处挂起。
  4. job1 完成后,job2 恢复并打印 Job2 starts after Job1
  5. 最后主协程在 job2.join() 处挂起,待 job2 完成后打印 Main ends

在这个流程中:

  • job1是被等待的协程。
  • job2调用了job1.join(),因此job2等待job1完成。
  • 主协程调用了job2.join(),因此主协程等待job2完成。

调用join的协程会挂起自身的执行,直到目标协程完成。这是协程同步的一种方式,用于确保某些协程任务完成后再执行其他操作。其实也可以简单的理解为调用join的协程加入了另一个写成的执行流程。

协程与回调型API的协作:suspendCoroutine与suspendCancellableCoroutine

  • 目的:将回调式(Callback)API 包装成挂起函数,便于在协程中串行调用并支持结构化并发

  • 区别

    • suspendCoroutine 感知协程的取消;
    • suspendCancellableCoroutine 支持协程取消,并可在 invokeOnCancellation 中做清理。

suspendCoroutinesuspendCancellableCoroutine 是用来与线程世界做连接的,用他们就能把回调格式的api 转换成挂起函数。

1. 使用 suspendCoroutine 转换回调

// 将 Retrofit 的 Callback API 包装成挂起函数
suspend fun callbackToSuspend(): List<Contributor> =
    suspendCoroutine { continuation ->  // 指定类型 List<Contributor>
        gitHub.contributorsCall("square", "retrofit")
            .enqueue(object : Callback<List<Contributor>> {
                override fun onResponse(
                    call: Call<List<Contributor>>,
                    response: Response<List<Contributor>>,
                ) {
                    // 结果的处理:恢复协程并返回数据
                    continuation.resume(response.body()!!)
                }

                override fun onFailure(call: Call<List<Contributor>>, t: Throwable) {
                    // 发生异常:恢复协程并抛出异常
                    continuation.resumeWithException(t)
                }
            })
        // ⚠️ suspendCoroutine 不支持协程取消后的清理
    }

调用示例

val job = lifecycleScope.launch {
    try {
        // 调用上面定义的挂起函数
        val contributors = callbackToSuspend()
        showContributors(contributors)
    } catch (e: Exception) {
        // 捕获并处理挂起过程中抛出的异常
        infoTextView.text = e.message
    }
}

2. 使用 suspendCancellableCoroutine 支持取消

suspend fun callbackToCancellableSuspend(): List<Contributor> =
    suspendCancellableCoroutine { continuation ->
        // 1) 发起 Retrofit 请求
        val call = gitHub.contributorsCall("square", "retrofit")
        
        call.enqueue(object : Callback<List<Contributor>> {
            override fun onResponse(
                call: Call<List<Contributor>>,
                response: Response<List<Contributor>>,
            ) {
                continuation.resume(response.body()!!)
            }

            override fun onFailure(call: Call<List<Contributor>>, t: Throwable) {
                continuation.resumeWithException(t)
            }
        })

        // 2) 注册协程取消后的收尾工作
        continuation.invokeOnCancellation {
            // 协程被取消时,取消底层网络请求
            call.cancel()
        }
    }

调用示例

val job2 = lifecycleScope.launch {
    try {
        val contributors = callbackToCancellableSuspend()
        showContributors(contributors)
    } catch (e: Exception) {
        infoTextView.text = e.message
    }
}
  • suspendCancellableCoroutine

    • 在挂起点内部,协程可以被取消;
    • invokeOnCancellation { … } 中执行取消回调,实现对 “传统回调 API” 的收尾和清理。

协程内部调用传统式的回调函数

  • 不要在协程内部再用 launch 嵌套启动子协程来调用 suspendCoroutine,否则外层的 try…catch 捕获不到 suspendCoroutine 中抛出的异常。

  • 要在同一个协程体内直接调用 suspendCoroutine,才能让异常落到外层 catch

  • 如果需要响应协程取消并做清理,则应使用 suspendCancellableCoroutine 并在 invokeOnCancellation 中执行收尾工作。

协程内部调用传统式的回调函数:

1. 错误示例:嵌套 launch 无法捕获异常

lifecycleScope.launch {
    try { 
        launch {  
            // ❌ 这里又用 launch,会启动一个新的子协程,
            //    它的异常不会被外层的 try–catch 捕获
            val contributors = suspendCoroutine<List<Contributor>> { cont ->
                gitHub.contributorsCall("square", "retrofit")
                    .enqueue(object : Callback<List<Contributor>> {
                        override fun onResponse(
                            call: Call<List<Contributor>>, 
                            response: Response<List<Contributor>>
                        ) {
                            cont.resume(response.body()!!) 
                        }

                        override fun onFailure(call: Call<List<Contributor>>, t: Throwable) {
                            cont.resumeWithException(t) 
                        }
                    })
            }
            showContributors(contributors)
        }
    } catch (e: Exception) { 
        // 这里捕获不到上面 enqueue 回调里的异常
    }
}
  • 为什么捕获不到?
    launch { … } 启动子协程后立即返回,外层的 try 块已经结束,后续异常不会再被这个 try 捕获。

2. 正确示例:直接在协程体内使用 suspendCoroutine

lifecycleScope.launch {
    try { 
        // ▶️ 直接把回调包装成挂起函数,异常会被下面的 catch 捕获
        val contributors = suspendCoroutine<List<Contributor>> { cont ->
            gitHub.contributorsCall("square", "retrofit")
                .enqueue(object : Callback<List<Contributor>> {
                    override fun onResponse(
                        call: Call<List<Contributor>>, 
                        response: Response<List<Contributor>>
                    ) {
                        cont.resume(response.body()!!)  // 恢复并返回数据
                    }

                    override fun onFailure(call: Call<List<Contributor>>, t: Throwable) {
                        cont.resumeWithException(t)  // 抛出异常
                    }
                })
        }
        showContributors(contributors)
    } catch (e: Exception) { 
        // ✅ 能捕获 suspendCoroutine 内部抛出的异常
        infoTextView.text = e.message
    }
}
  • 要点:在同一个协程体内调用 suspendCoroutine,外层 try–catch 才能拦截到回调中通过 resumeWithException 抛出的错误。

3. 响应取消并做清理:suspendCancellableCoroutine

suspend fun cancellableContributors(): List<Contributor> =
    suspendCancellableCoroutine { cont ->
        // 1) 发起网络请求
        val call = gitHub.contributorsCall("square", "retrofit")
        call.enqueue(object : Callback<List<Contributor>> {
            override fun onResponse(
                call: Call<List<Contributor>>, 
                response: Response<List<Contributor>>
            ) {
                cont.resume(response.body()!!)
            }
            override fun onFailure(call: Call<List<Contributor>>, t: Throwable) {
                cont.resumeWithException(t)
            }
        })

        // 2) 注册协程取消时的收尾工作
        cont.invokeOnCancellation {
            // 协程被取消后,主动取消底层请求
            call.cancel()
        }
    }

// 调用示例
val job2 = lifecycleScope.launch {
    try {
        val contributors = cancellableContributors()
        showContributors(contributors)
    } catch (e: Exception) {
        infoTextView.text = e.message
    }
}
  • 为什么要用 suspendCancellableCoroutine
    它能让挂起的协程在 被取消 时执行 invokeOnCancellation 中的清理逻辑(如取消 Retrofit Call)。用 suspendCoroutine 则不会触发此回调。
// 协程内部调用传统式的回调函数
lifecycleScope.launch {
    try { // 加入try catch 代码块是为了捕获suspendCoroutine内部的异常,但是这里我们亦可以不处理
        launch { // 如果直接多launch进行try-catch,会无法捕获suspendCoroutine出现的异常,只能捕获启动协程的异常。
            // 因为launch只是一个启动协程的过程,启动完成后,这个流程就结束了,意味着trycatch也结束了。
            /*val contributors =
              *//*下边这个代码可以抽出来,当做一个挂起函数来使用,然后上边的contributors就可以按照写成的串行方式传递下去了。*//*
    suspendCoroutine<List<Contributor>>*//*指定类型,否则resume处会出现类型不匹配错误*//* {
      gitHub.contributorsCall("square", "retrofit")
        .enqueue(object : Callback<List<Contributor>> {
          override fun onResponse(
            call: Call<List<Contributor>>, response: Response<List<Contributor>>,
          ) {
            it.resume(response.body()!!) // 结果的处理需要换成新的函数,这个是挂起函数的返回
            // showContributors(response.body()!!)
          }

          override fun onFailure(call: Call<List<Contributor>>, t: Throwable) {
            it.resumeWithException(t) //一旦发生异常,suspendCoroutine会立即结束。
          }
        })
    }
  showContributors(contributors)*/
        }
        val contributors =
            /*下边这个代码可以抽出来,当做一个挂起函数来使用,然后上边的contributors就可以按照写成的串行方式传递下去了。*/
            suspendCoroutine<List<Contributor>>/*指定类型,否则resume处会出现类型不匹配错误*/ {
                gitHub.contributorsCall("square", "retrofit")
                    .enqueue(object : Callback<List<Contributor>> {
                        override fun onResponse(
                            call: Call<List<Contributor>>,
                            response: Response<List<Contributor>>,
                        ) {
                            it.resume(response.body()!!) // 结果的处理需要换成新的函数,这个是挂起函数的返回
                            // showContributors(response.body()!!)
                        }

                        override fun onFailure(
                            call: Call<List<Contributor>>,
                            t: Throwable
                        ) {
                            it.resumeWithException(t) //一旦发生异常,suspendCoroutine会立即结束。
                        }
                    })
            }
        showContributors(contributors)
    } catch (e: Exception) { //捕获异常并进行处理。

    }
}

注册取消之后的收尾工作:

val job2 = lifecycleScope.launch {
    it.invokeOnCancellation {// 注册取消之后的收尾工作。
    
    } 
    suspendCancellableCoroutine { // 在这个代码块里边执行的回调函数的协程(job2)可以被取消(但不会会终止这里边回调式api的执行,你需要再invokeOnCancellation内处理回调式api的后续工作)。使用suspendCoroutine时不行的。

    }
}

一、回调式 API 转为可取消的挂起函数

  • 如何将回调 API 变为可取消的挂起函数并做清理?
  • 协程取消只是“状态标识”,只有挂起点才能识别并中断执行,阻塞调用则不会响应取消。

示例:

private suspend fun callbackToCancellableSuspend(): List<Contributor> =
    suspendCancellableCoroutine { continuation ->
        // 标记回调是否已处理过,防止重复清理
        var callbackHandled = false

        // 1) 注册协程取消时的收尾工作
        continuation.invokeOnCancellation {
            println("Coroutine cancelled")
            // 只有回调尚未处理时,才执行清理
            if (!callbackHandled) {
                // 例如:取消 Retrofit 请求
                gitHub.contributorsCall("square", "retrofit").cancel()
            }
        }

        // 2) 发起回调式请求
        gitHub.contributorsCall("square", "retrofit")
            .enqueue(object : Callback<List<Contributor>> {
                override fun onResponse(
                    call: Call<List<Contributor>>,
                    response: Response<List<Contributor>>
                ) {
                    // 协程仍然活跃时才恢复,并标记已处理
                    if (continuation.isActive) {
                        callbackHandled = true
                        continuation.resume(response.body()!!)
                    }
                }

                override fun onFailure(
                    call: Call<List<Contributor>>,
                    t: Throwable
                ) {
                    // 协程仍然活跃时才恢复,并标记已处理
                    if (continuation.isActive) {
                        callbackHandled = true
                        continuation.resumeWithException(t)
                    }
                }
            })
    }
  • 逻辑说明

    1. 先调用 invokeOnCancellation {…} 注册取消回调;
    2. 发起网络请求并在回调里调用 resumeresumeWithException
    3. callbackHandled 防止已响应的请求再次进入取消分支;
    4. 若协程被外部 cancel(),且请求尚未回调完成,则在 invokeOnCancellation 中取消请求。

二、协程取消与阻塞调用对比

val job = lifecycleScope.launch {
    println("Coroutine start")
    delay(500)      // 可被取消的挂起函数,会检测协程取消状态
    Thread.sleep(500) // 阻塞调用,不会响应协程取消
    println("Coroutine end")
}

lifecycleScope.launch {
    delay(200)
    job.cancel()    // 仅设置取消状态,挂起函数(delay)会中断协程,Thread.sleep 不会
}
  • 要点

    • 挂起函数(如 delaywithContextsuspendCancellableCoroutine)会在恢复前检查协程的取消状态,一旦检测到取消则抛出 CancellationException 并终止后续逻辑。
    • 阻塞方法(如 Thread.sleep)仅停住当前线程,不会抛出或响应协程的取消信号,协程仍会在阻塞结束后继续执行。

回到线程世界:runBlocking()

一、三种协程启动方式对比

启动器返回类型是否阻塞调用线程典型用途
launch {…}Job不阻塞启动一个不返回结果的并发协程
async {…}Deferred<T>不阻塞启动一个可返回结果的并发协程
runBlocking {…}阻塞当前线程阻塞在普通函数或测试中从“挂起”桥接到阻塞

说明:“阻塞”指的是调用它的线程会被停住,直到内部所有协程体执行完毕才继续后续代码。


二、launch 创建新协程时的上下文继承

  1. 继承与复制

    • 新协程基于父上下文复制大部分元素(调度器、名称等)。
  2. Job 层次

    • 每次 launch 会创建一个新的 Job,并作为父 Job 的子任务,从而在父取消时一并取消。
  3. 调度器覆盖

    • 如果显式传入如 Dispatchers.IO,新的上下文会用它覆盖父调度器;否则继承父调度器。

三、CoroutineScope 的作用

  1. 提供 CoroutineContext

    • 包含 ContinuationInterceptor(即 Dispatcher),决定协程运行线程;
  2. 管理生命周期

    • 持有所有子协程的 Job,可一键取消整个 Scope 内的所有协程。

四、runBlocking:从协程世界“回到”阻塞线程世界

kotlin
// 普通函数中启动协程并等待其完成
fun main() = runBlocking {
    val data = withContext(Dispatchers.IO) {
        // 调用阻塞式 API,但在 IO 线程执行
        gitHub.contributors("square", "retrofit")
    }
    println("Received data: $data")
}
  • 特点

    • 无需显式 CoroutineScope:直接在阻塞函数中使用;
    • 阻塞调用线程:调用它的线程会被停住,直到所有挂起函数执行完。
    • 桥接作用:把挂起函数包装为普通阻塞式调用,便于在不支持挂起的环境中使用。

五、runBlocking 的典型场景

  1. 测试代码

    • 单元测试框架通常是同步的,使用 runBlocking 能顺序地执行挂起函数并等待结果。
  2. 阻塞式 API 包装

    • 在普通(非挂起)函数中,临时桥接协程调用。
  3. 启动入口

    • 在构建 DSL 或初始化逻辑中,需要在继续执行前等待协程任务完成。

六、与其他启动器的关系示例

lifecycleScope.launch(Dispatchers.Main.immediate) {
    // ▶️ 会立即在主线程开始执行
    showLoading()
}
println("这行不会被阻塞")  // launch 不阻塞

runBlocking {
    // ▶️ 这里会阻塞当前线程,直到挂起体完成
    val contributors = gitHub.contributors("square", "retrofit")
    showContributors(contributors)
}
// runBlocking 阻塞后,才会继续下面的代码

七、注意事项

  • 不要在 UI 线程长时间使用 runBlocking,否则会导致界面卡顿。
  • 生产环境优先用 launch/async + 合适的 Dispatcher,保持非阻塞。
  • 测试或桥接场景下再考虑 runBlocking,以免误用造成性能问题。

学后测验

一、单项选择题(每题 2 分,共 6 题)

  1. 下列关于 ExecutorCoroutineContext 的描述,哪一项是正确的?
    A. 两者都能携带 CoroutineName
    B. Executor 只负责提交 Runnable,不包含协程元素
    C. CoroutineContext 不能决定线程调度
    D. Executor 可以直接存放 Job
    【答案】B
    【解析】 Executor 是 Java 线程池接口,只管把 Runnable 交给线程;CoroutineContext 既能携带线程调度器(Dispatcher)又能保存 Job、名称等。
  2. Dispatchers.Default 最适合执行哪类任务?
    A. 大量数据库读写 B. 图片下载 C. CPU 密集型计算 D. 主线程 UI 更新
    【答案】C
    【解析】 Default 线程池大小≈CPU 核心数,专为计算密集场景设计。
  3. 下面哪个 Scope 与 Activity/Fragment 的生命周期绑定?
    A. GlobalScope B. lifecycleScope C. viewModelScope D. CoroutineScope(Dispatchers.IO)
    【答案】B
    【解析】 lifecycleScope 在组件 onDestroy() 时自动 cancel()
  4. withContext(Dispatchers.IO) 相比 launch(Dispatchers.IO) 的主要区别是:
    A. 前者并行,后者串行 B. 前者挂起等待完成 C. 后者会阻塞线程 D. 前者返回 Job
    【答案】B
    【解析】 withContext 是挂起函数,会挂起直到代码块执行完;launch 立即返回 Job 并并行执行。
  5. 使用 suspendCoroutine 包装回调 API 时,若协程被 cancel(),默认行为是:
    A. 自动抛出 CancellationException 并取消回调
    B. 协程继续等待回调结果
    C. 立即停止回调、不再返回
    D. JVM 抛出 InterruptedException
    【答案】B
    【解析】 suspendCoroutine 不感知取消;只有 suspendCancellableCoroutine 才能在取消时执行清理。
  6. CoroutineContext 合并 (+) 逻辑里,为什么要把 ContinuationInterceptor 放最外层?
    A. 避免深拷贝 B. 方便最快速读取 Dispatcher C. 节省内存 D. 防止 GC
    【答案】B
    【解析】 协程恢复频繁读取 Dispatcher,放最外层可 O(1) 获取。

二、多项选择题(每题 3 分,共 6 题)

  1. 下列哪些属于常见 GC Roots?
    A. 活跃线程栈帧 B. 被 static 引用的对象 C. JNI 引用 D. WeakReference
    【答案】A B C
    【解析】 WeakReference 对象本身可被回收,不是 GC Root。
  2. GlobalScope.launch 的特点有:
    A. 协程 Job 的 parentnull
    B. 默认使用 Dispatchers.Default
    C. 会在 Activity 销毁时自动取消
    D. 适合全局日志、心跳等任务
    【答案】A B D
    【解析】 与界面生命周期无关,需手动管理取消。
  3. 下面哪些调用能确保外层 try-catch 捕获回调中的异常?
    A. 直接在协程体内调用 suspendCoroutine
    B. 在协程体内 launch { suspendCoroutine{} }
    C. 使用 suspendCancellableCoroutine 并在回调里 resumeWithException
    D. 在 runBlocking 里起线程执行回调
    【答案】A C
    【解析】 嵌套 launch 会把异常抛到子协程,外层 try 捕不到。
  4. 关于 supervisorScope,下列说法正确的有:
    A. 内部子协程异常不会取消兄弟协程
    B. 其根 Job 为 SupervisorJob
    C. 内部任一协程抛异常后 supervisorScope 不会向外抛异常
    D. 适合并行任务需要失败隔离的场景
    【答案】A B D
    【解析】 supervisorScope 最终仍会把最后一个异常向上传递。
  5. 调用 withContext 真的会发生线程切换的条件有:
    A. 传入的 Dispatcher 与当前 Dispatcher 不同
    B. 传入一个新的 SupervisorJob
    C. 当前协程已经被取消
    D. 参数为 Dispatchers.Unconfined
    【答案】A D
    【解析】 仅当调度器不同或使用 Unconfined 时会切换;换 Job 不影响线程。
  6. suspendCancellableCoroutine 提供的能力包括:
    A. 能在协程取消时触发清理逻辑
    B. 自动重试失败的回调请求
    C. 允许继续使用 resume/resumeWithException
    D. 内置超时机制
    【答案】A C
    【解析】 重试与超时需开发者自行实现。

三、判断题(每题 1 分,共 4 题)

  1. Dispatchers.IO 线程池大小固定为 64。
    【答案】×
    【解析】 默认上限≈64,可按阻塞情况动态伸缩。
  2. runBlocking 会阻塞调用它的线程,因此不应该在 Android 主线程长时间使用。
    【答案】√
    【解析】 主线程被阻塞会导致 ANR。
  3. CoroutineContext 中,同一种 Key 的元素只能存在一份,后加的会替换先加的。
    【答案】√
  4. withContext(EmptyCoroutineContext)coroutineScope {} 的运行语义完全等价。
    【答案】√
    【解析】 都不会切换调度器,只是挂起直至代码块结束。

四、简答题(每题 5 分,共 4 题)

  1. 解释 launchwithContext 在“并行 / 串行”方面的根本差异,并各给一个典型使用场景。
    【答案】

    • launch 启动新协程立即返回 Job,外层继续执行 ⇒ 并行;适合并发发送多条日志、无返回值的后台任务。
    • withContext 会挂起当前协程直至代码块完成 ⇒ 串行;适合“切线程 + 等结果”场景,如先 IO 下载再回主线程更新 UI。
  2. 为何推荐在 Activity/Fragment 中使用 lifecycleScope 而不是手写 GlobalScope
    【答案】 lifecycleScope 内建根 Job 与组件生命周期绑定,onDestroy 自动取消子协程,避免内存泄漏;GlobalScope 协程无父 Job,除非手动取消,否则页面销毁后仍会在后台运行。

  3. 简述将回调式网络请求封装成可取消挂起函数的关键步骤。
    【答案】

    1. 使用 suspendCancellableCoroutine 创建挂起点;
    2. 在回调成功时 resume(value),失败时 resumeWithException(e)
    3. 调用 invokeOnCancellation 注册协程取消后的清理逻辑(如 call.cancel());
    4. 可用布尔标记防止回调与取消重复执行。
  4. ContinuationInterceptor 总被放在 CoroutineContext 最外层有什么性能意义?
    【答案】 取调度器是协程恢复的高频操作;置顶后查询只需一次哈希 / 一层解包即可 O(1) 获取,避免在深度嵌套的 CombinedContext 中线性查找。


五、编程题(共 2 题,每题 8 分)

  1. 封装回调 API
    将下列 Retrofit 回调接口封装为 可取消 的挂起函数 suspend fun fetchContributors(owner:String,repo:String):List<Contributor>。要求:

    • 取消协程时能同时取消 Retrofit Call
    • 网络失败时把异常抛给调用者。
    interface GitHub {
        @GET("/repos/{owner}/{repo}/contributors")
        fun contributorsCall(
            @Path("owner") owner: String,
            @Path("repo") repo : String
        ): Call<List<Contributor>>
    }
    

    【答案代码(核心片段)】

    suspend fun GitHub.fetchContributors(
        owner: String,
        repo : String
    ): List<Contributor> = suspendCancellableCoroutine { cont ->
        val call = contributorsCall(owner, repo)
        call.enqueue(object : Callback<List<Contributor>> {
            override fun onResponse(
                call: Call<List<Contributor>>,
                response: Response<List<Contributor>>
            ) {
                if (cont.isActive) cont.resume(response.body()!!)
            }
    
            override fun onFailure(call: Call<List<Contributor>>, t: Throwable) {
                if (cont.isActive) cont.resumeWithException(t)
            }
        })
        cont.invokeOnCancellation { call.cancel() }
    }
    

    【解析】 suspendCancellableCoroutine 使协程取消时触发 invokeOnCancellation,从而安全取消底层网络请求。

  2. 结构化并发处理
    viewModelScope 内并行请求 “retrofit” 与 “okhttp” 的 contributors,然后排序后更新 UI。要求:

    • 使用结构化并发;
    • 能捕获任一请求失败并在同一处处理;
    • 不得在内部写 viewModelScope.async(要保持同一作用域)。

    【答案代码(核心片段)】

    viewModelScope.launch {
        try {
            coroutineScope {
                val d1 = async { api.fetchContributors("square", "retrofit") }
                val d2 = async { api.fetchContributors("square", "okhttp") }
    
                val merged = (d1.await() + d2.await())
                    .sortedByDescending { it.contributions }
    
                _uiState.value = merged               // LiveData / StateFlow 更新
            }
        } catch (e: Exception) {
            _uiState.value = emptyList()              // 统一错误处理
        }
    }
    

    【解析】

    • coroutineScope {} 保证两个 async 同属一棵协程树,异常可一次 catch
    • 若内部改用 viewModelScope.async 将脱离当前 coroutineScope,无法集中管理异常。