Logo SagesTool

Android Activity 与 Fragment 状态丢失和内存泄漏排查指南

Android Activity 与 Fragment 状态丢失和内存泄漏排查指南

Android 页面不稳定,通常绕不开两类问题:

  1. IllegalStateException 这类由状态丢失引发的崩溃。
  2. 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.lifecycleScope
  • repeatOnLifecycle
viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.data.collect { data ->
            binding.textView.text = data
        }
    }
}

三、推荐的排查顺序

当你线上看到这两类问题时,可以按这个顺序排查:

状态丢失排查

  1. 是否在异步回调里直接提交 Fragment 事务。
  2. 事务执行时页面是否已经退后台。
  3. 是否把 UI 提交时机绑定到了 STARTED / RESUMED 生命周期。
  4. 是否错误使用了 commitAllowingStateLoss() 掩盖真实问题。

内存泄漏排查

  1. Fragment 的 _binding 是否在 onDestroyView() 中置空。
  2. 是否存在 GlobalScope、手写线程、长生命周期回调。
  3. 单例或管理器里是否直接持有 Activity / Fragment
  4. 是否使用了 viewLifecycleOwner.lifecycleScope 而不是 Fragment 自身的 scope。

四、结论

如果把这篇文章压缩成几条规则,其实很简单:

  • 不要在生命周期不确定的回调里直接提交 Fragment 事务。
  • 不要让 Fragment 持有比 View 更长的引用。
  • 不要让页面对象暴露给比页面寿命更长的任务。
  • 协程、Flow、ViewBinding 都要明确绑定正确的生命周期。

当你把这些边界守住之后,Android 页面稳定性会明显提升,很多“偶现崩溃”和“查不出来的泄漏”也会一起消失。

相关工具