协程作用域与生命周期管理:viewModelScope、lifecycleScope、自定义作用域、取消与异常

协程作用域决定了协程的存活范围与取消传播方式。在Android组件中,将协程绑定到组件的生命周期上,可以避免内存泄漏和无效的后台操作。本章深入剖析viewModelScope、lifecycleScope以及自定义作用域的原理,并详细讲解取消与异常处理的工程实践。

核心原则:协程作用域的取消应当自动且及时——当组件销毁时,所有在该作用域内启动的协程都应被取消,以释放资源和避免泄露。

2.1 内置作用域架构

Kotlin协程提供了两个与Android生命周期深度整合的内置作用域:

作用域 绑定的生命周期 取消时机 典型使用场景
viewModelScope ViewModel ViewModel的onCleared()被调用时 负责数据加载、网络请求、数据库操作等与UI无关的后台任务
lifecycleScope LifecycleOwner(Activity/Fragment) Lifecycle到达DESTROYED状态时 直接需要在UI层面执行的协程,如收集Flow、更新界面
区分用法:如果协程需要在配置变更(如屏幕旋转)后继续,请使用viewModelScope;如果协程需要随当前Activity/Fragment的生命周期同步取消,请使用lifecycleScope。

2.2 生命周期与协程取消的绑定机制

下面是Android组件生命周期与协程作用域取消的对应关系图:

生命周期与协程作用域取消绑定关系 LifecycleOwner CREATED STARTED RESUMED DESTROYED lifecycleScope viewModelScope 取消所有子协程 (Job.cancel()) 取消所有子协程 (Job.cancel()) 当Lifecycle到达DESTROYED时,lifecycleScope和viewModelScope均触发Job.cancel(),所有活跃协程被立即取消。

2.3 代码实现流程

2.3.1 viewModelScope 使用示例

class MyViewModel : ViewModel() {
    init {
        // 在viewModelScope中启动协程,协程随ViewModel生命周期绑定
        viewModelScope.launch {
            val data = repository.fetchData()
            _uiState.value = UiState.Success(data)
        }
    }

    override fun onCleared() {
        super.onCleared()
        // 自动取消所有viewModelScope内的协程,不需要手动调用
    }
}

2.3.2 lifecycleScope 使用示例

class MyFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // 在lifecycleScope中启动,当Fragment进入DESTROYED时自动取消
        lifecycleScope.launch {
            viewModel.uiState.collect { state ->
                // 更新UI
                textView.text = state.toString()
            }
        }

        // 也支持在指定的Lifecycle状态时启动
        lifecycleScope.launchWhenResumed {
            // 仅在RESUMED状态下活跃,离开RESUMED时挂起,重新RESUMED时恢复
        }
    }
}

2.3.3 自定义作用域实现

当内置作用域无法满足需求时(例如:后台服务、跨组件共享),可以使用CoroutineScope配合SupervisorJob手动管理生命周期。

class MyBackgroundService {
    private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

    fun startWork() {
        serviceScope.launch {
            // 长期运行的后台任务
            while (isActive) {
                doPolling()
                delay(5000)
            }
        }
    }

    fun stop() {
        serviceScope.cancel() // 取消所有子协程
    }
}
注意:自定义作用域必须显式调用cancel(),否则协程会一直存活,可能导致内存泄漏或资源耗尽。推荐使用SupervisorJob而非默认的Job,以便一个子协程失败不会连带取消其他子协程。

2.4 取消与异常处理

2.4.1 取消机制原理

  • 取消是通过Job.cancel()触发的,协程会在下一个挂起点(如delay()、withContext等)抛出CancellationException。
  • 协程体必须配合isActive检查,或使用ensureActive()来响应取消,否则长时间运行的CPU密集型任务可能忽略取消信号。
  • 挂起函数如withContext(Dispatchers.Default) { ... }会自动检查取消状态。

2.4.2 异常传播与处理

异常类型 传播行为 处理方式
CancellationException 不会触发CoroutineExceptionHandler,只用于取消流程 通常不必捕获;若需清理资源可使用try/catch/finally
普通异常(如IOException) 向父Job传播,会取消兄弟协程(除非使用SupervisorJob) 使用try/catch局部处理,或通过CoroutineExceptionHandler全局处理

2.4.3 异常处理实践

// 使用SupervisorJob防止子协程异常影响兄弟协程
viewModelScope.launch(SupervisorJob() + CoroutineExceptionHandler { _, throwable ->
    Log.e("TAG", "协程异常:${throwable.message}")
}) {
    // 子协程1
    launch {
        throw RuntimeException("出现异常")
    }
    // 子协程2 — 不受子协程1异常影响
    launch {
        delay(100)
        Log.d("TAG", "继续执行")
    }
}
最佳实践:在viewModelScope和lifecycleScope中启动协程时,尽量在每个协程内部使用try/catch捕获业务异常,避免未捕获异常传播到顶层导致应用崩溃。也可以使用CoroutineExceptionHandler作为兜底。

2.5 调试技巧

  • Android Studio Coroutines Debugger:在Debug模式下,点击“Coroutines”标签可查看所有活跃协程的作用域、状态和堆栈。
  • 日志输出:在协程启动和取消处添加日志,观察生命周期事件。
  • 使用onCleared()断点:验证ViewModel销毁时viewModelScope是否被取消。
viewModelScope.launch {
    Log.d("TAG", "协程启动,viewModelScope的isActive = ${coroutineContext[Job]?.isActive}")
    try {
        delay(3000)
    } catch (e: CancellationException) {
        Log.d("TAG", "协程被取消:${e.message}")
        throw e // 重新抛出是惯例
    }
}

2.6 常见错误与避坑

  1. 未取消自定义作用域:忘记在onDestroy()或onStop()中调用scope.cancel(),导致协程持续运行,引发内存泄漏。
  2. 在错误的作用域启动IO操作:在viewModelScope中使用Dispatchers.Main执行网络请求,会阻塞主线程,应显式指定Dispatchers.IO。
  3. 捕获CancellationException后不重新抛出:导致协程无法正常取消,可能产生僵尸任务。
  4. 使用GlobalScope启动协程:生命周期不受控制,极易造成泄漏,应坚决避免。
性能优化建议:
  • 使用SupervisorJob避免单个子协程异常导致整个作用域被取消,减少不必要的重启开销。
  • 在lifecycleScope中长时间收集Flow时,优先使用flowWithLifecycle或repeatOnLifecycle来利用生命周期感知收集,只在可见状态下消耗资源。
  • 取消不必要的协程:在ViewModel启动多个网络请求时,使用Job().cancel()或viewModelScope.launch { job.cancelAndJoin() }主动取消已过时的请求。

2.7 小结

理解协程作用域与生命周期管理的本质,能够帮助你写出稳定、无泄漏的Android协程代码。掌握viewModelScope与lifecycleScope的绑定规则,合理使用SupervisorJob和自定义作用域,并正确处理取消与异常,是生产级应用的基础保障。