5. 协程作用域 Scope
作用域(CoroutineScope)管理协程的生命周期与取消。它内部挂了一棵 Job 树:你 launch / async 出的每个协程都是树上的一个节点,取消作用域会把整棵树一起取消。在 Android 中优先用框架提供的作用域,不要自己 new。
以前 AsyncTask 要在 onDestroy 里手动 cancel,忘了就内存泄漏。现在框架作用域(viewModelScope/lifecycleScope)在页面销毁时自动取消,你不用管收尾。
一、Scope 全家福(按使用频率)
| 频率 | 作用域 | 生命周期绑定 | 典型场景 |
|---|---|---|---|
| 🔥 极高 | viewModelScope | ViewModel 清空时取消 | ViewModel 里发起网络 / 刷新 UI 状态 |
| 🔥 极高 | lifecycleScope | Activity/Fragment 生命周期 | 在界面层收集 Flow、绑定生命周期的任务 |
| 🔥 极高 | repeatOnLifecycle(...) | 随前台/后台 STARTED 暂停恢复 | Fragment/Activity 里收集 UI 数据流(推荐) |
| 高 | rememberCoroutineScope() | Compose 组件存活期间 | Compose 里点按钮触发一次性协程 |
| 高 | CoroutineScope(...) | 自定义(你负责取消) | 单例后台任务:日志上报、长连接 |
| 高 | coroutineScope { } | 临时作用域,块结束即取消 | 在 suspend 里组织「并行 + 一起取消」 |
| 中 | supervisorScope { } | 子失败不连坐 | 多个互相独立的子任务 |
| 中 | MainScope() | 主线程 + SupervisorJob | 非 Android 组件(如自定义 View)的临时协程 |
| 低 | GlobalScope | 进程级,不推荐 | 几乎不用;易泄漏 |
二、结构化并发:取消会冒泡
作用域下的协程是父子关系。父取消 → 所有子取消;某个子抛异常(非 supervisor)→ 父也取消 → 其它兄弟一起取消。这就是「结构化并发」,保证不会留下孤儿协程。
viewModelScope.launch { // 父
launch { fetchA() } // 子 1
launch { fetchB() } // 子 2
}
// viewModelScope 取消时,父子三个一起取消
三、实战场景
3.1 viewModelScope:加载 UI 状态
class UserViewModel(private val repo: UserRepository) : ViewModel() {
private val _ui = MutableStateFlow<UiState>(UiState.Loading)
val ui: StateFlow<UiState> = _ui.asStateFlow()
fun load(id: String) = viewModelScope.launch { // ViewModel 销毁时自动 cancel
_ui.value = UiState.Success(repo.getUser(id))
}
}
3.2 lifecycleScope + repeatOnLifecycle:前台才收集
在 Fragment/Activity 里收集 StateFlow/LiveData 时,用 repeatOnLifecycle(STARTED):进入前台才收集、退到后台自动暂停,避免后台空转和泄漏。
class HomeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { render(it) } // 仅前台收集,退后台暂停
}
}
}
}
别再用 launchWhenStarted:launchWhenStarted 已被 repeatOnLifecycle 取代:前者退后台不取消收集、只挂起上游,仍有泄漏与状态不一致风险。新代码一律用 repeatOnLifecycle。
3.3 Compose:rememberCoroutineScope 点按钮
@Composable
fun RefreshButton(viewModel: HomeViewModel) {
val scope = rememberCoroutineScope() // 组件消失时自动取消
Button(onClick = {
scope.launch { viewModel.refresh() } // 一次性的用户操作
}) { Text("刷新") }
}
3.4 自定义作用域:全局后台任务
// 单例上报器:脱离页面生命周期,必须自己负责取消
class AnalyticsReporter {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
fun report(event: String) = scope.launch {
api.send(event) // 单个上报失败不影响其它
}
fun dispose() = scope.cancel() // 进程退出 / 不再需要时调用
}
自定义 scope 必须能取消:自己 CoroutineScope(...) 时,一定要在某个确定时机调用 cancel()(如 Application 销毁),否则协程永远活着 → 内存泄漏。也建议带上 SupervisorJob(),见 3.6。
3.5 并行:coroutineScope 里 async 两个请求
suspend fun loadDashboard(): Dashboard = coroutineScope { // 临时作用域
val user = async { repo.getUser() } // 同时发起
val orders = async { repo.getOrders() }
Dashboard(user.await(), orders.await()) // 两者都完成才返回;任一失败则整体取消
}
3.6 supervisorScope:子任务互不连坐
supervisorScope {
launch { taskA() } // A 抛异常
launch { taskB() } // B 不会被 A 连累取消(独立子任务用 supervisor)
}
普通 CoroutineScope(Dispatchers.IO) 下,任意子协程抛异常会取消整个作用域、其它在跑的子任务一起死。加上 SupervisorJob()(或 supervisorScope)后,子失败只影响自己,兄弟继续跑。需要「各自独立」时用 supervisor。
四、真实开发中会踩的坑
坑 1:用 GlobalScope 起网络 → 泄漏:GlobalScope.launch 不受任何生命周期管理,页面关了它还在跑,持有 Activity 引用就内存泄漏。官方已标记不推荐。Android 里改用 viewModelScope/lifecycleScope。
坑 2:自定义 scope 忘了 cancel:写 val scope = CoroutineScope(Dispatchers.IO) 后从不在合适时机 cancel(),任务越积越多。除非是进程级单例(在 Application 管理),否则优先用框架作用域。
坑 3:普通 scope 里一个子任务崩,全盘取消:默认结构中,某个 launch 抛异常会让整个 scope 取消,连带着其它正常任务也被砍。若这些任务彼此独立(如同时刷新多个看板),用 SupervisorJob() 或 supervisorScope 隔离。
坑 4:在 Compose 用错 scope 持有界面:在 @Composable 里直接用 viewModelScope 或全局 scope 去收集数据,组件重组 / 消失后收集不停止。UI 层收集请走 lifecycleScope + repeatOnLifecycle(View 体系)或 collectAsStateWithLifecycle()(Compose)。
五、小结
- 界面层用
viewModelScope/lifecycleScope;收集数据流用repeatOnLifecycle。 - Compose 里一次性操作(如点击)用
rememberCoroutineScope()。 - 自定义
CoroutineScope一定要能cancel(),并视情况加SupervisorJob()。 - 并行用
coroutineScope + async;想要子任务互不连坐用supervisorScope。 - 别用
GlobalScope;别再用launchWhenStarted。