协程速通 · Android

5. 协程作用域 Scope

作用域(CoroutineScope)管理协程的生命周期与取消。它内部挂了一棵 Job 树:你 launch / async 出的每个协程都是树上的一个节点,取消作用域会把整棵树一起取消。在 Android 中优先用框架提供的作用域,不要自己 new。

对照你熟悉的 XML 开发

以前 AsyncTask 要在 onDestroy 里手动 cancel,忘了就内存泄漏。现在框架作用域(viewModelScope/lifecycleScope)在页面销毁时自动取消,你不用管收尾。

一、Scope 全家福(按使用频率)

频率作用域生命周期绑定典型场景
🔥 极高viewModelScopeViewModel 清空时取消ViewModel 里发起网络 / 刷新 UI 状态
🔥 极高lifecycleScopeActivity/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)
}
普通 Scope vs SupervisorJob

普通 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。

Kotlin 协程速通 · Android 开发 · 内容基于 kotlinx.coroutines 1.7+