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 子协程独立 |