Kotlin Coroutines 与 Flow 的高级应用与原理(2):结构化并发:告别协程泄漏与混乱

Kotlin Coroutines 与 Flow 的高级应用与原理(2):结构化并发:告别协程泄漏与混乱

本文是「Kotlin Coroutines 与 Flow 的高级应用与原理」系列的第 2 篇,共 5 篇。在上一篇中,我们探讨了「引言:告别回调地狱,拥抱结构化并发」的相关内容。

二、结构化并发:告别协程泄漏与混乱

这是协程区别于其他并发模型(如裸线程、GlobalScope)的核心优势。

1. 核心理念

协程的生命周期应该与执行它的某个作用域(Scope)绑定。当作用域结束时,其内部启动的所有协程都应该被自动取消。这极大地简化了资源管理,避免了「协程泄漏」。

2. 核心概念

  • CoroutineScope: 定义协程的作用域。每个作用域都有一个关联的 CoroutineContext,其中通常包含一个 Job
  • Job: 代表一个可取消的工作单元,具有生命周期状态(Active、Completing、Completed、Cancelling、Cancelled)。Job 可以组织成父子层级结构

3. 结构化并发的关键原则

  • 作用域约束: 协程必须在某个 CoroutineScope 内启动(使用 launchasync 等构建器)
  • 生命周期绑定: 协程的生命周期受其所在 CoroutineScope 的 Job 控制。取消 Scope 的 Job 会递归地取消其所有子 Job(及其协程)
  • 父子关系:
    • 父等子: 父 Job 只有在其所有子 Job 都完成后才能进入完成状态
    • 子败父崩(默认): 如果一个子协程(非 SupervisorJob 下的直接子级)因为未捕获的异常而失败,它会取消它的父 Job,进而导致父 Job 取消所有其他子 Job

4. 常用作用域与构建器

  • GlobalScope: 谨慎使用! 这是一个全局单例 Scope,生命周期与整个应用进程绑定。在 GlobalScope 中启动的协程很容易泄漏,因为它们不会随特定的 UI 组件或业务逻辑结束而自动取消。主要用于某些顶层后台常驻任务(且需要极其小心的手动管理)
  • runBlocking { … }: 启动一个协程并阻塞当前线程直到其内部所有任务完成。主要用于连接阻塞代码与挂起世界(如在 main 函数或测试代码中调用 suspend 函数)。切勿在 Android 主线程或协程内部使用(除非明确知道后果)
  • coroutineScope { … }(suspend 函数): 创建一个结构化的嵌套作用域。它会继承外部作用域的上下文,但拥有自己的 Job。它会挂起调用者,直到其内部启动的所有子协程都完成。如果其内部任何一个子协程失败,coroutineScope 自身会失败并重新抛出异常,同时取消其他子协程。常用于将一项工作分解为多个并行子任务,并等待它们全部完成
  • supervisorScope { … }(suspend 函数): 与 coroutineScope 类似,也创建嵌套作用域并等待子任务完成。关键区别: 它使用 SupervisorJob。其直接子协程的失败不会导致 supervisorScope 本身失败,也不会取消其他兄弟子协程。异常需要由子协程自己处理(或通过 CoroutineExceptionHandler)。常用于需要隔离子任务失败影响的场景(如一个 UI 界面上有多个独立加载数据的区域)

5. Android Jetpack Scopes

  • viewModelScope(ViewModel 扩展属性): 预置在 ViewModel 中的 Scope,生命周期与 ViewModel 绑定(ViewModel onCleared() 时自动取消)。内部使用 SupervisorJob + Dispatchers.Main.immediate。是在 ViewModel 中启动协程处理业务逻辑和数据加载的首选方式
  • lifecycleScope(LifecycleOwner 扩展属性): 预置在 Activity/Fragment 中的 Scope,生命周期与组件的 Lifecycle 绑定(Lifecycle DESTROYED 时自动取消)。内部也使用 SupervisorJob + Dispatchers.Main.immediate。提供了 launchWhenCreatedlaunchWhenStartedlaunchWhenResumed 等方法,可以在特定生命周期状态下启动协程,并在状态退出时自动暂停或取消

6. Job() vs. SupervisorJob()

  • Job(): 子任务失败会导致父任务和所有兄弟任务被取消(默认的失败传递)
  • SupervisorJob(): 子任务失败不会影响父任务或兄弟任务,实现失败隔离。viewModelScope 和 lifecycleScope 默认使用它。直接创建 CoroutineScope(SupervisorJob() + ...) 可以自定义使用

(图示:结构化并发 —— Job 层级与取消)

+---------------------------------------------+
| CoroutineScope (Parent Job)                 |
|---------------------------------------------|
|    launch { // Child Job 1                 |
|      ...                                    |
|      launch { // Grandchild Job 1.1        | --------+
|        ...                                  |         | Cancellation
|      }                                      |         | propagates down
|    } // Child Job 1 completes when 1.1 done |         V
|                                             |
|    async { // Child Job 2 (using default Job) |
|      ...                                    |
|      if (error) throw Exception() --------->|--- X (Failure)
|      ...                                    |
|    } // Failure here cancels Parent & Child 1|
|                                             |
|    launch(SupervisorJob()) { // Child Job 3 |
|      launch { // Grandchild Job 3.1        |
|        if (error) throw Exception() ------->|--- X (Failure) - Only 3.1 fails, 3 survives
|      }                                      |
|    } // Child Job 3 unaffected by 3.1 failure|
+---------------------------------------------+
   |
   | Parent Job completes only when ALL (non-failing or supervised) children complete.
   | If Parent Job is cancelled, ALL children are cancelled.


下一篇我们将探讨「调度器(Dispatchers):协程在何处运行」,敬请关注本系列。

「Kotlin Coroutines 与 Flow 的高级应用与原理」系列目录

  1. 引言:告别回调地狱,拥抱结构化并发
  2. 结构化并发:告别协程泄漏与混乱(本文)
  3. 调度器(Dispatchers):协程在何处运行
  4. StateFlow & SharedFlow:热流状态与事件总线
  5. 取消机制:优雅地停止