CoroutineScope 是什么
CoroutineScope一个装协程的篮子,通俗地讲:你需要一个篮子来装鸡蛋(协程),这个篮子就是 CoroutineScope。
三个核心:
-
装协程 ------所有通过
launch、async启动的协程,都必须挂在一个 Scope 上 -
管生命周期 ------篮子扔了(
cancel()),里面的鸡蛋全碎(协程全取消) -
传环境------篮子自带配置(线程池、异常处理器等),里面的鸡蛋都按这个配置运行
Kotlin// 没有Scope,启动不了协程 launch { } // ❌ 编译错误 // 先造个篮子 val scope = CoroutineScope(Dispatchers.Main) // 往篮子里放鸡蛋 scope.launch { } // ✅ 可以跑了 // 不要篮子了,鸡蛋全扔 scope.cancel() // 所有协程取消
源码角度看Scope
先别急着写代码,我们看看官方是怎么定义的:
Kotlin
public interface CoroutineScope {
/**
* 此作用域的上下文。
* 上下文被作用域封装,并用于实现作为该作用域扩展的协程构建器。
* 除了访问 [Job] 实例以进行高级用法外,一般不建议在任何目的下在普通代码中访问此属性。
* 按照惯例,应该包含一个 [Job] 实例以强制结构化并发。
*/
public val coroutineContext: CoroutineContext
}
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
ContextScope(if (context[Job] != null) context else context + Job())
就这? 是的,接口只有一个属性,工厂方法也只是帮你补了一个Job。
这说明一个核心事实:Scope本身不执行任何协程,它只是Context的携带者 + 生命周期的边界标记。
我们再看launch函数的定义:
Kotlin
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
注意,
launch是CoroutineScope的扩展函数。这意味着:
没有Scope,你就调不了
launch
launch里的block接收者仍然是CoroutineScope(形成了嵌套Scope)
这个设计非常精妙:每个协程都有自己的Scope,父协程取消时,子协程通过这个继承的Scope感知到取消信号。
Scope最容易被误解的三个点
CoroutineScope ≠ CoroutineContext
很多初学者把这两个概念划等号。Scope是Context的容器,不是Context本身。
Kotlin
// 这是什么操作?
val context = job + Dispatchers.IO
context.launch { } // ❌ 编译不过
val scope = CoroutineScope(job + Dispatchers.IO)
scope.launch { } // ✅
| 对比维度 | CoroutineScope | CoroutineContext |
|---|---|---|
| 一句话定义 | 装协程的篮子 | 篮子里的配置清单 |
| 本质 | 容器/作用域 | 配置集合/上下文 |
| 接口定义 | interface CoroutineScope { val coroutineContext: CoroutineContext } |
interface CoroutineContext |
| 能否启动协程 | ✅ 能:scope.launch { } |
❌ 不能:context.launch { } 编译错误 |
| 生命周期 | 有生命周期,可取消:scope.cancel() |
无生命周期,不可取消 |
| 可变性 | 通常是单个实例,贯穿整个生命周期 | 可组合:Job() + Dispatchers.Main + CoroutineName("name") |
| 包含关系 | 持有 CoroutineContext | 被 CoroutineScope 持有 |
| 职责 | 管理协程的边界和生命周期 | 定义协程的行为配置(线程、Job、异常处理器等) |
| 类比 | 公司部门:有人、有预算、有负责人 | 员工手册:规定几点上班、去哪办公、找谁汇报 |
| 代码示例 | val scope = CoroutineScope(Job() + Dispatchers.IO) scope.launch { /* 干活 */ } |
val ctx = Job() + Dispatchers.Main + CoroutineName("Loader") // 只是个配置,不能跑协程 |
| 能否独立存在 | 必须依赖 Context(接口强制要求) | 可以独立存在,不依赖 Scope |
| 多个实例关系 | 各自独立,通常不组合 | 可以组合:ctx1 + ctx2 |
| 作用范围 | 横向边界:一组协程的集合 | 纵向传递:协程内部的继承链 |
| 取消操作 | scope.cancel() → 取消所有子协程 |
job.cancel() → 只取消单个 Job |
Job必须单独管理,不要复用
这是一个非常隐蔽的坑:
Kotlin
// 错误示范
val job = Job()
val scope1 = CoroutineScope(job + Dispatchers.Main)
val scope2 = CoroutineScope(job + Dispatchers.IO)
scope1.cancel() // job被cancel了
scope2.launch { } // ❌ 协程启动失败,因为job已取消
原则:每个Scope应该有自己的Job实例,不要跨Scope共享同一个Job。
cancel()会永久失效
Kotlin
val scope = CoroutineScope(Job())
scope.launch { /* 任务1 */ }
scope.cancel()
// 想重启?
scope.launch { /* 任务2 */ } // ❌ 不会执行,Scope已经死了
cancel()的本质是取消了Scope持有的Job。一个被cancel的Scope无法再启动新协程,你需要重建Scope。
不同业务场景下的Scope选型
UI层:永远使用lifecycleScope和viewModelScope
这是Google帮我们封装好的,生命周期绑定,开箱即用:
Kotlin
class UserProfileActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 界面可见时执行
lifecycleScope.launch {
loadUserData()
}
// 仅在Resumed状态执行,自动暂停恢复
lifecycleScope.launchWhenResumed {
startAnimations()
}
}
}
class UserViewModel : ViewModel() {
fun refresh() {
viewModelScope.launch {
repository.fetchData()
}
}
}
这里有个细节 :lifecycleScope默认使用的是Dispatchers.Main.immediate,所以在onCreate里直接launch,代码会立即执行,不会等下一个Looper循环。
数据层:Scope由调用方注入
数据层不应该自己创建Scope,这是分层架构的基本原则:
Kotlin
// ❌ 错误:仓库自己管理协程生命周期
class UserRepository {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
suspend fun getUser(): User {
return scope.async {
api.fetchUser()
}.await() // 这写法太别扭了
}
}
// ✅ 正确:挂起函数,Scope由上层决定
class UserRepository {
suspend fun getUser(): User = withContext(Dispatchers.IO) {
api.fetchUser()
}
}
// 如果需要并行请求,用coroutineScope或 supervisorScope
suspend fun getUserWithFriends(): UserDetail {
return coroutineScope {
val userDeferred = async { api.fetchUser() }
val friendsDeferred = async { api.fetchFriends() }
UserDetail(userDeferred.await(), friendsDeferred.await())
}
}
原则:挂起函数不感知Scope,并发组合用coroutineScope包装。
思考:为什么上面的写法是错误的呢?
并不是语法上的错误,而且会容易引起逻辑混乱, 不受控制
1. .await()是挂起点,会挂起当前协程直到结果返回但是 ------这个
async运行在scope里,不是当前协程的 child!
// 调用方 viewModelScope.launch { val user = repository.getUser() // ❌ 这里会挂起,但async里的协程不受viewModelScope控制 updateUI(user) } // 如果用户退出界面,viewModelScope取消 // 但repository.getUser()里的async还在后台继续跑!内存泄漏 + 资源浪费
- 违背结构化并发原则
Repository 自己创建 Scope,意味着:
这个 Scope 不依附于任何生命周期(Activity/ViewModel)
调用者无法取消它
即使整个应用退出,这个 Scope 可能还在跑
调试噩梦:
// 在任意地方调用 repository.getUser() // 启动了一个“孤儿”协程 // 没有任何人能取消它! // 没有取消句柄,没有生命周期感知3.suspend 函数不应该是"启动器"
suspend fun的语义是:我是挂起函数,我会在 当前协程 执行,不私自开线程。但这个函数:
私自创建 Scope
私自开新协程(async)
再等待它完成
这违反了 suspend 函数的基本契约。
后台服务类:自定义Scope + 手动释放
有些常驻对象需要长时间运行协程,比如传感器监听、WebSocket连接:
Kotlin
class SensorMonitor(
private val context: Context
) {
// 使用SupervisorJob,一个子协程失败不影响其他
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.Default + job)
private var isActive = false
fun start() {
if (isActive) return
isActive = true
scope.launch {
// 采集加速度传感器数据
collectAccelerometer()
}
scope.launch {
// 采集陀螺仪数据
collectGyroscope()
}
}
fun stop() {
isActive = false
job.cancelChildren() // 只取消子协程,保留Job活跃
// 或者 job.cancel() 完全销毁Scope
}
fun release() {
stop()
job.cancel()
}
}
这里用cancelChildren()而不是cancel(),是为了让Scope可以继续复用。
构建一个可重试的文件上传器
Kotlin
package com.example.mytest0
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
import java.util.Collections
class FileUploader(private val api :UploadService, private val externalScope : CoroutineScope) {
// 外部传人scope,通常传viewModelScope或自定义Scope
// 上传任务的Job,用于批量控制
private val uploadJob = SupervisorJob()
// 基于外部Scope创建子Scope,自动继承外部协程上下文
private val uploadScope = CoroutineScope(externalScope.coroutineContext + uploadJob)
// 当前正在上传的任务
private val uploadingTasks = Collections.synchronizedSet(mutableSetOf<String>())
private val maxRetries = 3
/**
* 上传文件,支持进度监听和自动重试
*/
fun uploadFile(fileId : String, file : File, onProgress: (Float) -> Unit = {}, onResult : (Result<String>) -> Unit) {
if (uploadingTasks.contains(fileId)) {
onResult(Result.failure(IllegalStateException("文件正在上传")))
return
}
uploadingTasks.add(fileId)
uploadScope.launch {
var retryCount = 0
while (retryCount < maxRetries) {
try {
val result = withContext(Dispatchers.IO) {
api.uploadFile(file) { percent ->
onProgress(percent)
}
}
withContext(Dispatchers.Main) {
onResult(Result.success(result))
}
} catch (e : IOException) {
retryCount++
if (retryCount >= maxRetries) {
withContext(Dispatchers.Main) {
onResult(Result.failure(e))
}
break
}
delay(1000L * retryCount)
}
}
uploadingTasks.remove(fileId)
}
}
/**
* 取消所有上传任务
*/
fun cancelAll() {
uploadJob.cancelChildren()
uploadingTasks.clear()
}
/**
* 释放资源,外部Scope销毁时自动调用
*/
fun release() {
uploadJob.cancel()
}
}
class UploadService {
suspend fun uploadFile(file : File, onProgress: (Float) -> Unit = {}) : String {
delay(1000)
return "success"
}
}
使用方式:
Kotlin
class UploadViewModel : ViewModel() {
private val uploader = FileUploader(apiService, viewModelScope)
fun startUpload(file: File) {
uploader.uploadFile(
fileId = file.name,
file = file,
onProgress = { progress ->
_progress.value = progress
},
onResult = { result ->
result.onSuccess { url ->
_uploadSuccess.value = url
}
}
)
}
override fun onCleared() {
uploader.release()
super.onCleared()
}
}
小结
1. Scope本质是凭证
没有Scope你启动不了协程,但它本身不干活。把它当成进入协程世界的门票。
2. 谁需要取消,谁管理Scope
UI组件管理自己的Scope,数据层不应该持有Scope,挂起函数就够了。
3. SupervisorJob是常备工具
大部分业务场景不需要子协程失败就取消父协程,SupervisorJob能避免很多非预期取消。
4. 自定义Scope三思而后行
先问自己:能不能用viewModelScope?能不能用coroutineScope?如果答案是否定的,再自定义。
5. 结构化并发不是束缚,是安全带
很多开发者觉得协程的限制太多,不如Thread灵活。但正是这些"限制"让你写出无泄漏、可取消、可测试的代码。
CoroutineScope, CoroutineContext, Job, Dispatcher之间的关系
CoroutineScope, CoroutineContext, Job, Dispatcher之间的关系我之间用相关的图进行说明一下,让大家更加容易理解。
以公司进行进行说明

层级关系树

生命周期时序图

一句话总结图

关系口诀:
Scope 抱着 Context,Context 装着 Job 和 Dispatcher
Scope 生协程,协程挂 Job 上,跑在 Dispatcher 里
爸爸取消,儿子全停;爸爸死了,绝后(不能再开新协程)