官方出处:https://developer.android.google.cn/kotlin/coroutines/coroutines-adv?hl=zh-cn
目录
一、Job介绍
🎮 Job:协程的“遥控器”和“身份证”
文档里说“Job 是协程的句柄”,意思是它就像您拿到的一个遥控器。通过这个遥控器,您可以:
- 控制:随时取消正在运行的协程。
- 检查:查看协程是活跃还是已完成。
- 等待:等待协程执行完毕。
同时,每个通过 launch 或 async 启动的协程都会返回一个专属的 Job 实例。这个实例就是该协程的唯一身份证,用于标识和管理它自己。
🤔 两种使用 Job 的方式
您可以将 Job 用在两个层面,理解它们的区别很关键:
方式一:用 Job 控制单个协程(最常见)
当您用 launch 启动协程时,会立即得到一个返回的 Job 对象。这个 job 就是专门用来控制那一个协程的。
kotlin
// 假设 scope 是之前创建的 CoroutineScope
val job: Job = scope.launch {
// ... 这里是协程要执行的代码
}
// 您可以用这个 job 来控制这个特定的协程
if (someCondition) {
job.cancel() // 只取消这一个协程
}
job.join() // 等待这一个协程完成
方式二:将 Job 传给 CoroutineScope(高级用法)
这是文档中“您还可以将 Job 传递给 CoroutineScope 以进一步管理其生命周期”这句话的意思。这里的 Job 不再是控制单个协程,而是作为 Scope 的“根Job”,用来控制该 Scope 下的所有协程的生命周期。
kotlin
// 1. 先创建一个 Job 实例,比如叫 parentJob
val parentJob = Job()
// 2. 创建 CoroutineScope 时,把 parentJob 作为上下文的一部分传进去
// 这样,parentJob 就成了这个 Scope 的“根Job”
val scope = CoroutineScope(parentJob + Dispatchers.Main)
// 3. 用这个 scope 启动多个协程
scope.launch {
// 协程 A
delay(1000)
}
scope.launch {
// 协程 B
delay(2000)
}
// 4. 此时,您可以通过最初的那个 parentJob 来管理所有协程
if (needToCleanUpAll) {
parentJob.cancel() // ⭐️ 取消整个 scope!所有通过 scope 启动的协程都会被取消
}
📊 两种方式的对比
为了帮助您更直观地理解,我整理了这个对比表格:
| 方式 | 代码示例 | 控制的 Job 对象 | 控制范围 | 典型场景 |
|---|---|---|---|---|
| 控制单个协程 | val job = scope.launch { ... } |
launch 返回的 job |
仅这一个协程 | 需要单独取消或等待某一个任务完成。 |
| 控制整个作用域 | val scope = CoroutineScope(Job()) |
创建 Scope 时传入的 Job |
Scope 内所有协程 | 统一管理一组相关的协程,例如在页面销毁时,一次性取消所有任务。 |
💡 总结一下
Job本身就是一个对象,您可以持有它的引用。- 直接使用
launch返回的Job:控制的是单个协程。 - 把
Job传给CoroutineScope:这个Job就成了 Scope 的“根”,调用它的cancel(),等于一次性取消整个 Scope 里所有的协程。
二、Scope传入Job作为根Job的意义
疑问:一次性取消整个 Scope 里所有的协程,为啥不直接调用Scope的cancel方法呢?
您问到了一个非常关键且精妙的设计点!直接调用 Scope.cancel() 和通过根 Job 取消,在效果上完全等价。之所以有两种方式,核心原因在于设计的灵活性和分层。
💡 核心答案:它们是“同一个操作的两个入口”
Scope.cancel() 方法在内部实际上就是调用了它持有的根 Job 的 cancel() 方法。所以:
- 直接调用
scope.cancel():是官方推荐的、最直观的方式。 - 通过根
Job取消:是一种更底层、更灵活的方式,尤其在需要精细控制 Job 生命周期或解耦时非常有用。
为了更清晰地说明,让我们深入一层:
🔍 深入一层:Scope 和 Job 的关系
当您创建一个 CoroutineScope 时,您必须给它一个 CoroutineContext,而这个上下文中必须包含一个 Job。
kotlin
// 您创建的 Scope,它内部持有了一个 Job
val scope = CoroutineScope(Job() + Dispatchers.Main)
这个被传入的 Job,就是该 Scope 的根 Job。Scope 对象本身并不实现取消逻辑,它只是一个拥有 coroutineContext 属性的接口。取消逻辑是由Job 实现的。
🎯 两种方式的对比与选择
| 维度 | 方式一:调用 scope.cancel() |
方式二:调用 rootJob.cancel() |
|---|---|---|
| 语义清晰度 | 最高。scope.cancel() 直接表达了“取消这个作用域里的所有协程”的意图,代码自解释性最强。 |
稍弱。job.cancel() 本身只表达“取消这个 Job”,需要结合上下文才能理解它在取消整个作用域。 |
| 使用便捷性 | 最方便。只要持有 scope 引用,随时可调用。 |
较繁琐。需要额外持有 rootJob 的引用。 |
| 设计灵活性 | 较低。scope 被销毁时,其内部的 Job 通常也应随之取消,这是一种强耦合。 |
极高。Job 的生命周期可以与 Scope 解耦。一个 Job 可以被多个 Scope 共享,或传递给其他组件。 |
🚀 为什么需要“通过根 Job 取消”这种灵活性?
文档中特意提到“您还可以将 Job 传递给 CoroutineScope”,就是为了应对更复杂的设计场景。
场景一:Job 共享
您可以在多个作用域之间共享同一个根 Job。这样,取消这一个 Job,就能同时取消所有相关作用域里的全部协程。这是 scope.cancel() 做不到的。
kotlin
// 创建一个根 Job
val rootJob = Job()
// 创建两个作用域,但共享同一个根 Job
val scopeA = CoroutineScope(rootJob + Dispatchers.Main)
val scopeB = CoroutineScope(rootJob + Dispatchers.IO)
scopeA.launch { /* 任务 A1 */ }
scopeA.launch { /* 任务 A2 */ }
scopeB.launch { /* 任务 B1 */ }
// 一次取消,同时取消 scopeA 和 scopeB 里的所有协程
rootJob.cancel()
场景二:生命周期解耦
有时,Job 的生命周期需要比创建它的 Scope 更长。例如,您可能在一个函数内部创建了一个 Scope,但希望将这个 Scope 内启动的一个长时间运行的后台任务的 Job 返回给调用方,让调用方能独立于 Scope 来控制它。
kotlin
fun startLongTask(): Job {
// 在函数内部创建一个 Scope
val scope = CoroutineScope(Dispatchers.Default)
// 启动一个长时间任务,并返回它的 Job
val taskJob = scope.launch {
// 执行很长的任务...
}
// 函数执行完毕,scope 本身可能不再需要,但任务 Job 被返回了
// 调用方可以通过返回的 taskJob 单独控制这个任务
return taskJob
// 注意:这里 scope 没有取消,因为它没有被清理,这不是好实践,
// 只是为了说明 Job 可以独立于创建它的 Scope 被传递和控制。
}
📝 总结与最佳实践
| 场景 | 推荐做法 | 原因 |
|---|---|---|
| 日常开发,管理UI组件生命周期 | 直接调用 scope.cancel() (如 viewModelScope、lifecycleScope) |
最简单、最直观,完全符合结构化并发的原则。 |
| 需要精细控制多个相关作用域 | 创建并持有根 Job,通过它统一取消 |
提供了超越单个作用域的、更强大的控制力。 |
| 需要将任务生命周期与创建者解耦 | 将任务自身的 Job 返回给调用方 |
实现了更灵活的设计,调用方可以独立控制该任务。 |
一句话总结:scope.cancel() 是官方推荐的“快捷方式”,而通过根 Job 取消是更底层、更灵活的“万能钥匙”,用于应对更复杂的设计需求。
0 条评论