Android Activity 与 Fragment 状态丢失和内存泄漏排查指南
Android 页面不稳定,通常绕不开两类问题:
IllegalStateException这类由状态丢失引发的崩溃。Activity/Fragment没有按预期释放,最终引发内存泄漏甚至 OOM。
这两类问题表面上一个是崩溃、一个是泄漏,但根因都和生命周期理解不到位有关。本文把它们拆开说清楚,并给出更适合生产环境的修复思路。
一、为什么会出现状态丢失
最典型的报错是:
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
当 Activity 进入后台、发生旋转,或者系统准备回收进程时,会先执行 onSaveInstanceState()。这一步的作用,是把当前页面状态序列化保存下来,供系统后续恢复。
如果你在这之后仍然尝试执行 FragmentTransaction.commit(),框架就会抛异常。原因并不复杂:新的事务已经无法被安全保存到系统快照里了。
常见触发场景
- 网络请求返回后立即切页面
- 延迟任务执行时页面已经退到后台
- Flow / LiveData 回调时直接提交 Fragment 事务
下面这个例子就很危险:
fun fetchProfile() {
api.getProfile().enqueue(object : Callback<Profile> {
override fun onResponse(call: Call<Profile>, response: Response<Profile>) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, ProfileFragment())
.commit()
}
override fun onFailure(call: Call<Profile>, t: Throwable) = Unit
})
}
如果用户在请求期间按了 Home 键,等回调回来时页面很可能已经不处于安全提交状态了。
更稳妥的做法
关键不是“想办法把 commit 成功执行”,而是只在生命周期安全的窗口里更新 UI。
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
if (state is UiState.Ready) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, ProfileFragment())
.commit()
}
}
}
}
这样做的好处是:
- 页面不可见时不会盲目提交事务
- 回到前台后会继续收集状态
- 事务执行时机更稳定
commitAllowingStateLoss() 什么时候能用
它不是“万能修复”,而是明确告诉系统:这次 UI 改动即使丢了也没关系。
只有在下面这种场景才适合考虑:
- 非核心视觉刷新
- 关闭一个提示层
- 某个装饰性 Fragment 丢失也不会影响业务
如果涉及支付、登录、表单、多步骤流程,不要依赖它掩盖问题。
二、为什么会出现内存泄漏
内存泄漏的本质,是一个本该被释放的对象,仍然被更长生命周期的对象持有引用。
在 Android 里最常见的重灾区就是:
Activity被异步任务持有Fragment的 View 没有及时释放- 全局协程或单例错误持有页面引用
1. Activity 被后台任务“吊住”
class DetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Thread {
Thread.sleep(10_000)
runOnUiThread {
showResult()
}
}.start()
}
}
如果用户在这 10 秒内退出页面,这个线程依旧持有 Activity,导致整棵对象图都无法回收。
更合理的方式是改用生命周期绑定的协程:
lifecycleScope.launch {
delay(10_000)
showResult()
}
页面销毁时,协程会被自动取消。
2. Fragment ViewBinding 泄漏
这是项目里最常见、也最容易被忽略的一类问题。
class DashboardFragment : Fragment(R.layout.fragment_dashboard) {
private var _binding: FragmentDashboardBinding? = null
private val binding get() = _binding!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentDashboardBinding.bind(view)
}
}
如果没有在 onDestroyView() 中把 _binding 置空,那么 Fragment 进入返回栈后,View 层级仍然会被这条引用链持有。
正确写法:
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
3. 错误使用全局协程
GlobalScope.launch(Dispatchers.IO) {
val result = api.load()
withContext(Dispatchers.Main) {
binding.textView.text = result
}
}
这里的问题不是“协程不好用”,而是 GlobalScope 生命周期过长。页面销毁后,这段逻辑仍可能继续执行,并尝试访问已经无效的 UI。
在 Fragment 里,优先使用:
viewLifecycleOwner.lifecycleScoperepeatOnLifecycle
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.data.collect { data ->
binding.textView.text = data
}
}
}
三、推荐的排查顺序
当你线上看到这两类问题时,可以按这个顺序排查:
状态丢失排查
- 是否在异步回调里直接提交 Fragment 事务。
- 事务执行时页面是否已经退后台。
- 是否把 UI 提交时机绑定到了
STARTED/RESUMED生命周期。 - 是否错误使用了
commitAllowingStateLoss()掩盖真实问题。
内存泄漏排查
- Fragment 的
_binding是否在onDestroyView()中置空。 - 是否存在
GlobalScope、手写线程、长生命周期回调。 - 单例或管理器里是否直接持有
Activity/Fragment。 - 是否使用了
viewLifecycleOwner.lifecycleScope而不是 Fragment 自身的 scope。
四、结论
如果把这篇文章压缩成几条规则,其实很简单:
- 不要在生命周期不确定的回调里直接提交 Fragment 事务。
- 不要让
Fragment持有比 View 更长的引用。 - 不要让页面对象暴露给比页面寿命更长的任务。
- 协程、Flow、ViewBinding 都要明确绑定正确的生命周期。
当你把这些边界守住之后,Android 页面稳定性会明显提升,很多“偶现崩溃”和“查不出来的泄漏”也会一起消失。