CoroutineScope — 是 Kotlin 接口,它定义了协程的生存范围并为启动新协程提供上下文。根据 Kotlin 文档,2025,每个 CoroutineScope 实例都包含 CoroutineContext 并管理其中启动的所有协程。当 scope 结束时(cancel),所有子协程都会自动取消,这可以防止内存泄漏。
主要内容
CoroutineScope — 是来自 kotlinx.coroutines 库的基本接口,作为协程的容器。它定义了协程的生存边界:当 scope 结束时,其中的所有协程都会自动取消。
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
该接口仅包含一个字段 — coroutineContext。通过它,scope 为其中启动的所有协程提供调度器(Dispatcher)、任务(Job)、异常处理和其他上下文元素。
所有协程启动函数 — launch、async、runBlocking — 都是 CoroutineScope 的扩展函数。这意味着只有在存在 scope 对象时才能调用它们。这种设计保证了每个协程都有明确定义的父级和生命周期。
在 Android 中,每个架构组件都有自己的 scope:ViewModel 的 viewModelScope,Activity/Fragment 的 lifecycleScope。在服务器应用程序中,scope 可以绑定到 HTTP 请求或数据库连接池。
理解 CoroutineScope 的内部结构需要熟悉 Job 的概念和 结构化竞争 的原则。
每个协程在启动时都会返回一个 Job 对象(或 async 的 Deferred)。Job 代表一个具有明确生命周期的任务:New、Active、Completing、Completed、Cancelling、Cancelled。Job 对象形成树状结构:
结构化竞争 — Kotlin Coroutines 的关键架构原则,其中协程的寿命与其 scope 的寿命相绑定。这与“fire-and-forget”模型形成对比,在该模型中协程在 scope 结束后继续存活。结构化竞争的优点:
当调用 scope.cancel() 时,Job scope 进入 Cancelled 状态,这会递归地取消所有子 Job。取消后,scope 只能通过创建新的 CoroutineScope 实例来重新使用。
CoroutineScope 可以通过工厂函数或通过在类中实现接口来创建。我们来看看这两种方法。
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
scope.launch {
println("在 ${Thread.currentThread().name} 上运行")
}
工厂函数接受 CoroutineContext 并使用指定的上下文创建 scope。在示例中,CPU 密集型任务使用 Dispatchers.Default,并使用 SupervisorJob 来隔离子协程之间的异常。
class MyRepository {
private val scope = CoroutineScope(Dispatchers.IO + Job())
suspend fun fetchData(): Data = scope.async {
api.getData()
}.await()
fun cleanup() {
scope.cancel()
}
}
我们将 scope 作为类的字段存储,并手动调用 cleanup 来取消它。这适合于具有可管理生命周期的组件 — 例如,仓库或管理器。
Kotlin 允许通过关键字 by 委托 CoroutineScope 的实现:
class DataLoader : CoroutineScope by CoroutineScope(Dispatchers.IO) {
fun load() {
launch {
// 协程在 DataLoader scope 中运行
}
}
}
这种方法在类本身就是 scope 并希望提供协程启动方法时很方便。但是请谨慎:类会继承 CoroutineScope 的所有方法,包括 cancel,这可能会破坏封装性。
GlobalScope — 是整个应用程序的 CoroutineScope 单例。官方不建议在生产代码中使用它。
JetBrains 仅允许在稀有场景下使用 GlobalScope:应用程序级别的后台进程,这些进程在所有 Activity 关闭后必须保持活跃(例如,数据同步、分析)。但即使在这些情况下,更好的做法是使用 CoroutineScope(SupervisorJob()) 创建自己的 scope。
始终使用具有明确生命周期管理的 自定义 CoroutineScope。在 Android 中,这些是 viewModelScope 和 lifecycleScope。在服务器应用程序中,为每个请求或连接池创建 scope。
这两个函数都是暂停函数,用于为并行任务创建临时 scope,但它们在异常时的行为根本不同。
| 特征 | coroutineScope | supervisorScope |
|---|---|---|
| 错误时的行为 | 子协程中的异常会取消所有其他协程 | 子协程中的异常不会取消其他协程 |
| 错误传播 | 是,第一个异常会传播到外部 | 是,第一个异常会传播到外部 |
| 默认 Job | Job() — 子级绑定到父级 | SupervisorJob() — 子级之间相互独立 |
| 典型用例 | 由多个步骤组成的原子操作 | 独立的并行任务(UI 加载) |
当多个并行操作构成一个原子操作时,使用 coroutineScope。例如,从三个服务器加载数据:如果一个请求失败,其他请求就没有意义了。
suspend fun loadProductPage(): ProductPage = coroutineScope {
val product = async { api.getProduct() }
val reviews = async { api.getReviews() }
ProductPage(product.await(), reviews.await())
}
如果 getProduct 或 getReviews 抛出异常 — 两个协程都会被取消,并且异常会传播到调用代码。
当并行操作之间不相互依赖时,使用 supervisorScope。例如,在多个独立区域中加载个人资料数据:如果推荐区域失败,个人资料标题和好友列表应该正常显示。
我们来看看开发者在 Kotlin 中使用 CoroutineScope 时最常见的错误。
最常见的协程泄漏场景 — 在组件结束时创建 scope 而未调用 cancel。如果 scope 未被取消,协程会继续运行,保持对对象的引用。在 Android 中,使用会自动取消的 viewModelScope 或 lifecycleScope。
GlobalScope 忽略 Android 组件的生命周期。在 Activity 关闭后,在 GlobalScope 中启动的协程会继续运行并尝试更新 UI — 这将导致崩溃。UI 组件始终应使用 lifecycleScope。
调用 cancel() 后,scope 不能重新使用 — 其中的所有协程已经结束。通过工厂函数创建新的 CoroutineScope 实例。Job() 不支持重新激活。
通过 by 委托时,类会获得可以从任何地方调用的公共 cancel() 方法,这会破坏封装性。将 scope 作为私有字段存储,不要委托接口。
常见问题
CoroutineScope — 是拥有 CoroutineContext 并负责协程生命周期的接口。CoroutineContext — 是一组元素(调度器、job、错误处理),定义了协程“如何”执行。其中一个区别是:scope 创建协程,context 管理它们的行为。
可以,这是标准模式:CoroutineScope(Dispatchers.IO + SupervisorJob())。SupervisorJob 防止在一个子协程中出现异常时流式取消其他子协程。这对于独立的并行任务很有用,其中一个错误不应该停止其他任务。
scope 中的协程数量没有限制 — 只受到可用内存和调度器设置的限制。实际限制通常是一个 scope 中的 数千个活跃协程。但是,大量的协程可能表明架构问题。
正确的方法是通过构造函数将 scope 传递给类,或使用来自 kotlinx-coroutines-test 的 runBlockingTest / runTest。在测试中,可以将 scope 替换为 TestCoroutineDispatcher 并手动控制协程的执行。
不可以,scope 是协程的外部容器。协程本身不是 scope。但是,在协程内部,可以通过 coroutineScope 或 supervisorScope 创建新 scope 来并行启动子协程。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。