7、Lifecycle KTX:lifecycleScope与repeatOnLifecycle,感知生命周期协程

协程在Android里已经成了标配,但有个问题一直让人头疼——生命周期管理。

你想想看,一个网络请求发出去,页面都销毁了,回调还在跑。轻则报个空指针,重则内存泄漏。我早期做项目时,就吃过这个亏。那时候还在用AsyncTask,页面退出了任务还在后台跑,结果一回调,UI已经没了,直接崩掉。

后来有了协程,配合Lifecycle KTX,这个问题才算真正解决。说白了,就是让协程能感知Activity或Fragment的生死,该停就停,该取消就取消。

7.1 lifecycleScope:自带生命周期的协程作用域

先看个最简单的用法。以前我们启动协程,要么用GlobalScope(不推荐),要么自己搞个CoroutineScope手动管理。现在好了,Lifecycle直接给你内置了一个作用域。

class MyFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // 直接用 lifecycleScope
        lifecycleScope.launch {
            // 这个协程会在 Fragment 进入 DESTROYED 状态时自动取消
            val result = fetchData()
            textView.text = result
        }
    }
}

这个lifecycleScope是什么?它是Lifecycle的一个扩展属性。每个LifecycleOwner(比如Fragment、Activity)都自带一个。它的生命周期跟着Lifecycle走,当Lifecycle到达DESTROYED状态时,作用域内的所有协程都会被取消。

我在项目中遇到过一种情况:有人在Fragment里同时启动了多个协程,分别做网络请求、数据库读写、文件下载。如果每个都手动管理取消逻辑,代码会变得很臃肿。用lifecycleScope就清爽多了,一个作用域统一管理。

核心要点:lifecycleScope 是 Lifecycle 的扩展属性,协程的生命周期与 Lifecycle 绑定。当 Lifecycle 进入 DESTROYED 状态时,自动取消所有协程。

7.2 launchWhenCreated / launchWhenStarted / launchWhenResumed

lifecycleScope解决了取消问题,但还有个场景:我希望协程在特定生命周期阶段才开始执行。

比如,一个动画效果,我希望Fragment可见(onResume之后)才启动。或者一个数据加载,我希望至少onCreate之后才执行。

KTX提供了三个便捷方法:

lifecycleScope.launchWhenCreated {
    // 只在 Lifecycle 至少是 CREATED 状态时执行
    // 如果低于 CREATED,协程会挂起
}

lifecycleScope.launchWhenStarted {
    // 只在 Lifecycle 至少是 STARTED 状态时执行
}

lifecycleScope.launchWhenResumed {
    // 只在 Lifecycle 是 RESUMED 状态时执行
}

这三个方法有个共同特点:当生命周期低于指定状态时,协程会挂起而不是取消。等生命周期回到指定状态后,协程会继续执行。

嗯,这里要注意。挂起和取消是两回事。挂起只是暂停,协程还在;取消是彻底结束。如果你希望协程在后台也能继续(比如保存数据到数据库),那就用lifecycleScope.launch。如果你希望协程只在特定阶段运行,就用launchWhenXxx。

我的建议:数据加载类任务用 lifecycleScope.launch 就够了,因为页面销毁后没必要继续。UI更新类任务用 launchWhenStarted 或 launchWhenResumed,确保只在可见时更新界面。

7.3 repeatOnLifecycle:重复执行的优雅方案

launchWhenXxx有个小坑。我曾经踩过:我在launchWhenStarted里启动了一个数据流收集,页面切到后台再回来,发现数据流没有重新开始收集。

为什么会这样?因为launchWhenStarted只是挂起,不是重新启动。当生命周期回到STARTED时,协程从挂起点继续执行,但数据流可能已经错过了中间的数据。

这时候就需要repeatOnLifecycle了。它的语义是:每次进入指定生命周期状态时,重新启动协程块

lifecycleScope.launch {
    // 每次 Fragment 进入 STARTED 状态时,重新执行这个块
    // 每次离开 STARTED 状态时,取消这个块
    lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
        // 这里可以安全地收集 Flow
        viewModel.uiState.collect { state ->
            textView.text = state.toString()
        }
    }
}

这个模式特别适合收集Flow。每次页面可见时重新开始收集,不可见时自动取消。不会漏数据,也不会重复收集。

我现在的项目里,所有UI层的Flow收集都改成了repeatOnLifecycle。以前用LiveData的时候,还要手动处理生命周期。现在用Flow + repeatOnLifecycle,代码更简洁,逻辑也更清晰。

注意:repeatOnLifecycle 必须在 lifecycleScope 的协程内部调用。它本身是一个挂起函数,会阻塞当前协程直到 Lifecycle 被销毁。所以不要直接在 lifecycleScope 外面调用。

7.4 三种模式的对比

我把这三种模式整理了一下,方便你对比选择:

模式 行为 适用场景
lifecycleScope.launch 立即启动,销毁时取消 一次性任务,如网络请求、数据库写入
launchWhenStarted / launchWhenResumed 低于指定状态时挂起,回到状态后继续 需要持续运行但只在特定阶段活跃的任务
repeatOnLifecycle 每次进入指定状态时重新启动,离开时取消 Flow收集、需要重新初始化的场景

7.5 知识体系总览

下面这张图帮你理清Lifecycle KTX的核心脉络:

Lifecycle KTX 协程感知体系 LifecycleOwner lifecycleScope lifecycleScope.launch 销毁时取消,一次性任务 launchWhenStarted/Resumed 低于状态时挂起,恢复后继续 repeatOnLifecycle 每次进入状态重新启动 核心原则:协程生命周期与 Lifecycle 绑定 DESTROYED → 自动取消 | 低于指定状态 → 挂起或取消

7.6 避坑指南

最后分享几个我踩过的坑:

  • 不要在非UI层使用lifecycleScope——它属于LifecycleOwner,ViewModel里应该用viewModelScope。我曾经把lifecycleScope用在Repository里,结果编译报错,后来才意识到作用域绑定错了。
  • repeatOnLifecycle不要嵌套——如果你在repeatOnLifecycle里面再套一个repeatOnLifecycle,逻辑会变得非常混乱。一个协程块里只做一件事,保持简单。
  • 注意协程的取消异常——当协程被取消时,会抛出CancellationException。如果你用try-catch捕获了所有异常,可能会把取消逻辑也吞掉。建议只捕获特定异常,或者用runCatching处理。

我曾经在launchWhenStarted里写了一个死循环,想着页面不可见时自动挂起就没事了。结果页面销毁时,协程被取消,但循环里的资源没有释放,导致内存泄漏。后来改成用repeatOnLifecycle,每次重新启动时重新初始化资源,问题就解决了。

最佳实践:UI层收集Flow用 repeatOnLifecycle,一次性任务用 lifecycleScope.launch,需要持续运行的后台任务用 lifecycleScope.launch 配合挂起函数。记住这个原则,大部分场景都能覆盖。

好了,Lifecycle KTX的核心内容就这些。说白了就是让协程学会看人脸色——页面活着就干活,页面死了就停手。这个思路贯穿了整个Jetpack KTX的设计哲学。


公众号:蓝海资料掘金营,微信deep3321