Android随笔-Kotlin CoroutineScope

Kotlin CoroutineScope(协程作用域 ) 是结构化并发 (Structured Concurrency)的核心载体。它定义了协程的生命周期边界:Scope 取消时,其内部所有协程自动取消;Scope 内的异常会按规则向上传播。

一、核心原理:Scope 到底是什么

1. 定义

kotlin 复制代码
public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

Scope 本身极其简单 ------只有一个 CoroutineContext 属性。它的真正威力来自约定:

  • 每次 launch/async 创建的协程都是某个 Scope 的子协程(Job)。
  • 子协程的 Job 会挂载到父 Job 下,形成树形结构
  • 父 Job 取消/失败时,递归取消所有子 Job 。
  • 所有子 Job 完成后,父 Job 才算完成。

这就是结构化并发 :协程不是野生 的,而是有组织的。

2. 与 CoroutineContext 的关系

概念 作用 类比
CoroutineScope 作用域容器,管理生命周期 Activity / ViewModel
CoroutineContext 协程的运行环境(调度器 + Job + 异常处理器 + 命名) 线程池 + 任务句柄 + 错误回调
kotlin 复制代码
val scope = CoroutineScope(Dispatchers.IO + Job() + CoroutineName("MyScope"))
// CoroutineScope 只是把这个 Context 包了一层

二、常见 Scope 及使用场景

1. GlobalScope(不推荐)

kotlin 复制代码
GlobalScope.launch { ... }  // 生命周期 = Application 生命周期

为什么不推荐:

  • 与 Application 同生命周期,不受控制
  • 无法取消,容易导致内存泄漏和后台耗电。
  • 官方文档明确标注:"GlobalScope is delicate"。

唯一适用场景: 真正需要在应用存活期间持续运行的后台任务(如全局日志上报),且你能接受它不可取消。

2. 自定义 CoroutineScope

kotlin 复制代码
val scope = CoroutineScope(Dispatchers.Main + Job())

scope.launch { ... }

// 手动取消(通常在 onDestroy 中调用)
scope.cancel()

注意 : 自定义 Scope 必须手动管理生命周期,否则就是另一个 GlobalScope。

3. Android 内置 Scope

Scope 生命周期 使用位置 取消时机
lifecycleScope 绑定 LifecycleOwner Activity / Fragment Lifecycle 销毁时
viewModelScope 绑定 ViewModel ViewModel ViewModel.onCleared()
kotlin 复制代码
// Fragment 中
viewLifecycleOwner.lifecycleScope.launch { ... }

// ViewModel 中
viewModelScope.launch { ... }

关键区别

  • lifecycleScope 在 Activity/Fragment 销毁时取消。
  • viewModelScope 在 ViewModel 清除时取消(配置变更如旋转屏幕时 ViewModel 存活,协程不取消)。

4. MainScope

kotlin 复制代码
val scope = MainScope()  // Dispatchers.Main + SupervisorJob()

// 需要在合适的时机调用
scope.cancel()

适用于需要在主线程运行且需手动管理的场景,不如 lifecycleScope 方便。

5. coroutineScope { }(挂起函数内的子作用域)

kotlin 复制代码
suspend fun fetchData() = coroutineScope {
    val deferred1 = async { api1() }
    val deferred2 = async { api2() }
    deferred1.await() + deferred2.await()  // 等待两个都完成
}

特点

  • 挂起函数,不是类。
  • 创建子作用域,等待内部所有子协程完成后才返回。
  • 内部任一协程失败,取消其他所有子协程(异常向上传播)。

6. supervisorScope { }(监督作用域)

kotlin 复制代码
suspend fun fetchData() = supervisorScope {
    val job1 = launch { api1() }  // 失败不影响 job2
    val job2 = launch { api2() }
}

特点

  • 子协程失败不会相互影响 ,也不会取消父协程。
  • 适合独立任务场景(如同时加载多张图片,一张失败不影响其他)。

三、结构化并发的 Job 树与取消机制

1. Job 树结构

复制代码
CoroutineScope (Job A)
├── launch { Job A-1 }
├── async { Job A-2 }
│   └── launch { Job A-2-1 }
└── launch { Job A-3 }
  • scope.cancel() → Job A 取消 → 递归取消 A-1、A-2、A-3、A-2-1。
  • 子协程抛异常 → 默认向上传播 → 父 Job 失败 → 取消所有兄弟 Job。

2. 取消的传播规则

父 Scope 类型 子协程失败行为 兄弟协程命运
普通 Job(默认) 异常向上传播 全部取消
SupervisorJob 异常不向上传播 各自独立,互不影响
kotlin 复制代码
// 普通 Job:一个子协程失败,整个 Scope 失败
val scope = CoroutineScope(Job())
scope.launch { throw Exception("fail") }  // 导致 scope 内其他协程也被取消

// SupervisorJob:子协程独立失败
val scope = CoroutineScope(SupervisorJob())
scope.launch { throw Exception("fail") }  // 不影响 scope 内其他协程

注意: viewModelScope 和 lifecycleScope 内部默认使用 SupervisorJob,所以一个子协程失败不会导致整个 Scope 崩溃。

四、常见问题

Q1:CoroutineScope 的作用是什么?为什么需要它?

  • CoroutineScope 是协程的生命周期管理容器,实现了结构化并发。
  • 它确保协程不是"野生的":每个协程都属于某个 Scope,形成 Job 树。
  • 作用:
    • 统一取消:Scope 取消时,自动递归取消所有子协程,防止内存泄漏。
    • 异常传播:定义异常向上或横向传播的边界。
    • 生命周期绑定:Android 中通过 lifecycleScope/viewModelScope 绑定组件生命周期。

Q2:GlobalScope 为什么不推荐?和自定义 Scope 有什么区别?

  • GlobalScope 是全局单例,生命周期与 Application 相同,永远不会自动取消。
  • 使用 GlobalScope.launch 等同于启动一个"守护线程":Activity 销毁了它还在跑,极易造成内存泄漏和后台耗电。
  • 自定义 CoroutineScope 可以绑定到特定生命周期(如 Activity.onDestroy 时调用 scope.cancel()),实现可控的资源释放。
  • 原则: 任何与 UI 或页面相关的协程,必须使用 lifecycleScope/viewModelScope 或自定义 Scope,绝不用 GlobalScope。

Q3:lifecycleScope 和 viewModelScope 有什么区别?使用场景?

lifecycleScope viewModelScope
绑定对象 LifecycleOwner(Activity/Fragment) ViewModel
取消时机 Lifecycle 进入 DESTROYED ViewModel.onCleared()
配置变更 屏幕旋转时 Fragment/Activity 重建,协程取消 屏幕旋转时 ViewModel 存活,协程不取消
适用场景 UI 相关一次性操作(动画、页面跳转) 业务逻辑、数据加载(需跨配置变更存活)

典型用法

  • 在 Fragment 中发起网络请求 → 用 viewModelScope(旋转屏幕不中断)。
  • 在 Fragment 中执行页面动画 → 用 lifecycleScope(页面销毁立即停止)。

Q4:coroutineScope { } 和 supervisorScope { } 有什么区别?

coroutineScope { } supervisorScope { }
Job 类型 普通 Job SupervisorJob
异常传播 子协程异常向上传播,取消兄弟协程 子协程异常独立,不取消兄弟协程
适用场景 多个子任务相互依赖,任一失败整体失败 多个子任务相互独立,如并行加载多张图片
父协程影响 父协程会失败 父协程不受影响
kotlin 复制代码
// coroutineScope:一个失败,全部取消
suspend fun loadAll() = coroutineScope {
    launch { loadImage1() }  // 如果失败
    launch { loadImage2() }  // 这个也会被取消
}

// supervisorScope:各自独立
suspend fun loadAll() = supervisorScope {
    launch { loadImage1() }  // 失败不影响
    launch { loadImage2() }  // 这个继续执行
}

Q5:withContext 和 coroutineScope 有什么区别?

withContext coroutineScope
作用 切换协程的调度器/上下文 创建子作用域,启动多个并行子协程
返回值 返回 lambda 最后一行 返回 lambda 最后一行
并行能力 顺序执行内部代码 可启动多个 launch/async 并行
使用场景 切换线程(如 IO 切 Main) 并发组合多个异步任务
kotlin 复制代码
// withContext:切换线程
val result = withContext(Dispatchers.IO) {
    networkRequest()  // 在 IO 线程执行
}

// coroutineScope:并行执行
val result = coroutineScope {
    val a = async { requestA() }
    val b = async { requestB() }
    a.await() + b.await()
}

注意: withContext 也会等待内部代码完成,但它不会创建多个并行子任务。

Q6:如何安全地取消一个 Scope?cancel() 后 launch 新协程会怎样?

  • 调用 scope.cancel() 会取消 Scope 的 Job,并递归取消所有子协程。
  • 取消后的 Scope 不能再启动新协程:scope.launch { } 会立即失败(Job 处于 Cancelling/Cancelled 状态)。
  • 如果需要重新使用,必须创建新的 Scope。
kotlin 复制代码
scope.cancel()
scope.launch { ... }  // 不会执行,Job 已取消

最佳实践:

  • 在 Activity.onDestroy() / Fragment.onDestroyView() 中取消自定义 Scope。
  • 使用 lifecycleScope/viewModelScope 则无需手动取消。

Q7:ViewModel 中使用协程,为什么推荐 viewModelScope 而不是自己创建 Scope?

  • 生命周期自动管理:viewModelScope 在 ViewModel.onCleared() 时自动取消,避免内存泄漏。
  • 配置变更存活:屏幕旋转时 ViewModel 存活,协程不中断,请求结果可恢复。
  • SupervisorJob 内置:一个协程失败不会导致整个 Scope 崩溃,其他协程继续运行。
  • Dispatchers.Main.immediate:默认主线程调度,适合直接更新 LiveData/StateFlow。
  • 代码简洁:无需手动 cancel(),减少样板代码。

Q8:协程取消后,内部代码一定会立即停止吗?

  • 不会自动立即停止。协程取消是协作式(cooperative)的
  • 取消只会将 Job 状态设为 Cancelling,代码必须主动检查取消状态才能响应。
  • 检查方式:
    • yield():挂起点检查取消,如果已取消则抛 CancellationException。
    • ensureActive():如果已取消立即抛异常。
    • isActive:手动判断。
    • 在 suspend 函数内部:标准库挂起函数(如 delay、withContext)会自动检查取消。
kotlin 复制代码
scope.launch {
    while (isActive) {  // 手动检查,否则死循环不会退出
        doWork()
    }
}

注意: 如果协程在执行纯计算(非挂起)的代码,且没有检查 isActive,取消后它仍会运行到下一个挂起点或结束。

Q9:CoroutineScope(Dispatchers.Main + Job()) 和 CoroutineScope(SupervisorJob()) 有什么区别?

  • Job():普通 Job,子协程异常会向上传播并取消兄弟协程。
  • SupervisorJob():监督 Job,子协程独立失败,互不影响。
kotlin 复制代码
// 普通 Job
val scope = CoroutineScope(Job() + Dispatchers.Main)
scope.launch { throw Exception("A") }  // 导致整个 scope 失败,其他协程取消

// SupervisorJob
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
scope.launch { throw Exception("A") }  // 仅该协程失败,其他协程继续

Android 内置 Scope 默认使用 SupervisorJob,所以 viewModelScope.launch 的异常不会导致整个 ViewModel 崩溃。

Q10:如何自定义一个绑定到生命周期的 CoroutineScope?

kotlin 复制代码
class MyPresenter : DefaultLifecycleObserver {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

    fun doWork() {
        scope.launch { ... }
    }

    override fun onDestroy(owner: LifecycleOwner) {
        scope.cancel()  // 生命周期结束时取消
    }
}

或LifecycleCoroutineScope:

kotlin 复制代码
lifecycle.addObserver(object : DefaultLifecycleObserver {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
    
    override fun onResume(owner: LifecycleOwner) {
        scope.launch { loadData() }
    }
    
    override fun onPause(owner: LifecycleOwner) {
        scope.cancel()
    }
})

更推荐直接使用 lifecycleScope( androidx 已提供)。

五、总结

问题 一句话答案
Scope 是什么 协程生命周期容器,实现结构化并发和统一取消
GlobalScope 全局生命周期,永不自动取消,不推荐
lifecycleScope 绑定 Activity/Fragment 生命周期,DESTROYED 时取消
viewModelScope 绑定 ViewModel 生命周期,跨配置变更存活,onCleared 时取消
coroutineScope 挂起函数内子作用域,普通 Job,一失败全取消
supervisorScope 监督作用域,子协程独立失败,互不影响
取消机制 协作式取消,需检查 isActive/yield/ensureActive
异常传播 普通 Job 向上传播;SupervisorJob 子协程独立
相关推荐
m0_547486666 小时前
《Java程序设计与实践 》全套PPT课件2026
java·开发语言
AFinalStone6 小时前
Android 7系统无障碍服务(一)全景图与架构概览
android·无障碍服务
Android-Flutter6 小时前
android 自定义view 详解
android·kotlin
WAsbry7 小时前
协程任务的失败控制:取消、异常传播与Supervisor
android
Android打工仔7 小时前
从 finally 理解程序的控制流:它为什么不是 catch 后面的代码?
android·kotlin
灵析表格7 小时前
灵析表格财务函数深度实用性分析与实操教程
开发语言·ai·json·excel·wps
随遇丿而安7 小时前
第15周:Service 全功能 + 后台优化
android
YXL1111YXL7 小时前
续体和状态机 —— suspend 函数的 CPS 变换
android·kotlin
WAsbry7 小时前
Flow数据模型:冷流、状态、事件与共享策略
android
WAsbry7 小时前
深入理解 Kotlin suspend:挂起语义、状态机与恢复调度
android