Activity 生命周期概览:从创建到销毁

讲 Flow 订阅泄漏,绕不开 Activity 生命周期。为什么呢?因为大多数泄漏都发生在 Activity 重建或销毁的时候。我就吃过这个亏——好多次。

这一节我们快速过一下 Android 官方给出的六大生命周期回调。别嫌基础,我敢说你项目中至少有 30% 的泄漏都跟没理解透这几个回调的执行顺序有关。

六大回调,一个流程图

先看一张我手绘的生命周期流转图,后面所有的分析都围绕它展开。

Activity 生命周期六大回调状态机 onCreate() onStart() onResume() onPause() onStop() onDestroy() 配置旋转:onPause → onStop → onDestroy → onCreate → onStart → onResume 后台杀进程:onPause → onStop → onDestroy(可能不触发)

每个回调在做什么

我把它们分成三组来说,这样你更容易记住。

回调 什么时候调? 你作为开发者该干啥
onCreate() Activity 第一次被创建 初始化视图、绑定 ViewModel、启动非耗时协程
onStart() Activity 可见但不可交互 开始动画、打开摄像头预览
onResume() Activity 处于前台可交互 恢复播放、开始位置更新
onPause() 另一个 Activity 在前台,当前失去焦点 保存草稿、暂停动画、释放独占资源
onStop() Activity 完全不可见 释放大对象、取消网络请求
onDestroy() Activity 被销毁(用户返回或系统回收) 取消所有协程、清理观察者
关键认知:onDestroy 是最后一道防线。如果 Flow 订阅没有在 onDestroy 中取消,那就是泄漏——Activity 对象无法被 GC,内存永远涨。

为什么这跟 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。

生命周期的两种流转路径

很多人只记得「创建→开始→恢复→暂停→停止→销毁」这条直线。但实际项目里还有两条重要路径:

  1. 配置变更(旋转屏幕):onPause → onStop → onDestroy → onCreate → onStart → onResume。整个 Activity 重建,所有旧引用必须完全释放。
  2. 按 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。你可能会发现几个隐藏的泄漏点。