Activity 生命周期概览:从创建到销毁
讲 Flow 订阅泄漏,绕不开 Activity 生命周期。为什么呢?因为大多数泄漏都发生在 Activity 重建或销毁的时候。我就吃过这个亏——好多次。
这一节我们快速过一下 Android 官方给出的六大生命周期回调。别嫌基础,我敢说你项目中至少有 30% 的泄漏都跟没理解透这几个回调的执行顺序有关。
六大回调,一个流程图
先看一张我手绘的生命周期流转图,后面所有的分析都围绕它展开。
每个回调在做什么
我把它们分成三组来说,这样你更容易记住。
| 回调 | 什么时候调? | 你作为开发者该干啥 |
|---|---|---|
onCreate() |
Activity 第一次被创建 | 初始化视图、绑定 ViewModel、启动非耗时协程 |
onStart() |
Activity 可见但不可交互 | 开始动画、打开摄像头预览 |
onResume() |
Activity 处于前台可交互 | 恢复播放、开始位置更新 |
onPause() |
另一个 Activity 在前台,当前失去焦点 | 保存草稿、暂停动画、释放独占资源 |
onStop() |
Activity 完全不可见 | 释放大对象、取消网络请求 |
onDestroy() |
Activity 被销毁(用户返回或系统回收) | 取消所有协程、清理观察者 |
为什么这跟 Flow 泄漏有关?
你想想看,一个 Flow 在 Activity 里被 collect,默认情况下会持有一个 Job。如果你在 lifecycleScope.launch 里 collect,这个 Job 的生命周期是绑定在 LifecycleOwner 上的。但如果你用 GlobalScope.launch 或者自己创建了一个 CoroutineScope 忘了取消——那么即使 Activity 调用了 onDestroy,那个协程依然在后台跑,它持有的 Activity 引用永远不会被回收。
我遇到过最典型的例子:有一个同事(嗯,就是我自己)在 onCreate 里直接启动了一个 flow.collect,用的 lifecycleScope,但他忘了用 WhenStarted 或者 repeatOnLifecycle。结果旋转屏幕时,onDestroy 被调用,但协程还在跑,因为它绑定的 Lifecycle 从 DESTROYED 变成了 INITIALIZED?不,问题出在生命周期没处理好。我们后面会细讲。
onDestroy 里显式调用 lifecycleScope.cancel() 吗?千万不要!lifecycleScope 是框架管理的,你手动 cancel 会导致其他正常绑定也失效。正确做法是使用 repeatOnLifecycle 或 flowWithLifecycle。
生命周期的两种流转路径
很多人只记得「创建→开始→恢复→暂停→停止→销毁」这条直线。但实际项目里还有两条重要路径:
- 配置变更(旋转屏幕):
onPause → onStop → onDestroy → onCreate → onStart → onResume。整个 Activity 重建,所有旧引用必须完全释放。 - 按 Home 键再返回:
onPause → onStop → onRestart → onStart → onResume。注意没有 onDestroy,所以不能指望在 onDestroy 里清理所有东西。
Flow 订阅泄漏的重灾区就是路径 1。Activity 被销毁了,但协程还活着,导致旧 Activity 实例泄漏。
每个回调中 Flow 订阅的「雷区」
我把常见的错误姿势列出来,你对照一下自己的代码:
- onCreate 中启动 collect:如果你没有使用
lifecycleScope或viewModelScope,Activity 旋转重建时会产生多个并发的 collect 实例。我第一次做登录页面时就踩了这个坑——每次旋转都多一个网络请求。 - onResume 中启动 collect 但 onPause 没停止:这种情况会导致 collect 在 Activity 不可见时仍在执行,既浪费电量,又可能更新已经不可见的 UI(因为 View 已经 detached 了)。
- onDestroy 中才释放:对于按 Home 键再返回的场景,onDestroy 不触发,泄漏就永久存在了。
onDestroy 里用 lifecycleScope.cancel() 去取消自己启动的协程——这会连框架正常的生命周期协程一起干掉。你应该只取消自己创建的 Scope,或者用 repeatOnLifecycle 自动管理。
最佳实践:一张表格搞定
| 回调 | Flow 订阅操作 | 推荐方式 |
|---|---|---|
| onCreate | 初始化 Flow 但不启动 collect | 创建 Flow 实例或绑定 ViewModel |
| onStart | 启动 collect(适用于 UI 可见时需要的订阅) | lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { flow.collect() } } |
| onResume | 启动 collect(仅前台需要) | lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) { flow.collect() } } |
| onPause / onStop / onDestroy | 不需要手动取消,由框架自动处理 | 无需额外代码,但前提是你用了 repeatOnLifecycle |
说白了,这一章就是要让你脑子里刻下一个印象:Activity 的每一次重建都是一次「新出生」,旧实例必须被彻底遗忘。如果旧实例还被某个 Flow 的 collect 协程引用着,那它就会变成内存里的「僵尸」,你的 App 就会越来越卡。
下一节我们正式开始分析第一个案例——在 onCreate 中直接 collect 并旋转屏幕会发生什么。这里我先留个悬念:你猜猜看,如果不用 repeatOnLifecycle,Activity 重建时会同时存在多少个并发在跑的 collect?答案会让你吓一跳。
lifecycleScope.launch,看看有没有哪个 collect 没有包裹 repeatOnLifecycle 或 flowWithLifecycle。你可能会发现几个隐藏的泄漏点。