如何正确处理 Fragment 的状态保存与恢复,避免内存泄漏?

解读

在国内大厂面试中,这道题常被用来区分“会用 Fragment”与“懂 Fragment”。
面试官想听到的不只是「重写 onSaveInstanceState」这种入门答案,而是:

  1. 系统销毁 Fragment 的几种场景(配置变更、内存回收、后台被杀)
  2. 状态保存的完整链路(FragmentManager、SavedStateRegistry、ViewModel-SavedState)
  3. 恢复时如何避免持有旧引用导致内存泄漏(View、Drawable、Animator、Listener、RxJava 订阅、协程 Job)
  4. 在复杂场景(ViewPager2+Fragment、嵌套 Fragment、Navigation 组件、Hilt 注入)下的最佳实践
  5. 国内 ROM 差异:部分厂商后台冻结时会强制回收 Binder 引用,需额外防御

知识点

  • Fragment 生命周期与 SavedStateRegistry 机制
  • onSaveInstanceState / SavedStateProvider / setTargetFragment 废弃后的替代方案
  • ViewModel + SavedStateHandle 自动保存 vs 手动 putBundle
  • View 层级状态:android:saveEnabled、android:id、isSaveFromParentEnabled
  • 内存泄漏根因:匿名内部类持有外部 Fragment 引用、Handler/Runnable 延迟消息、动画未取消、图片未释放、RxJava 未 dispose、协程未 cancel
  • LeakCanary 常见报告:InputMethodManager 持有 View、Fragment.mView 被 Animation 引用、Drawable Callback 未清空
  • 国内加固与热修:Tinker/Shadow 对 SavedState 的额外序列化限制
  • Jetpack Navigation 中 Fragment 销毁但 ViewModel 存活时的状态同步
  • 折叠屏/多窗口配置变更:configChanges 声明与 ActivityRecreator 的兼容
  • 工具链:Systrace 查看 Fragment 销毁耗时、Memory Profiler 观察 Retained Fragment、StrictMode 检测 Activity 泄漏

答案

分三步回答:保存、恢复、防泄漏。

  1. 保存
    a. 轻量状态:在 Fragment 的 onSaveInstanceState(Bundle outState) 中保存基本类型;同时注册 SavedStateProvider 供 SavedStateRegistry 统一收集。
    b. 复杂状态:使用 ViewModel + SavedStateHandle,把业务数据放在 SavedStateHandle 中,系统会在进程死亡后自动恢复。
    c. View 状态:给需要保存的 View 设置唯一 android:id,并确保父容器未关闭 saveEnabled;自定义 View 重写 onSaveInstanceState()/onRestoreInstanceState()
    d. 嵌套 Fragment:子 Fragment 的状态由父 Fragment 的 onSaveInstanceState 自动递归保存,无需手动干预,但务必使用 childFragmentManager

  2. 恢复
    a. 在 onViewCreated()onCreate() 中通过 savedInstanceState 取回数据;若使用 SavedStateHandle 可直接读取,无需判断 null。
    b. 对于 ViewPager2 场景,给 FragmentStateAdapter 传入 lifecycleOwnerSavedStateRegistryOwner,确保 offscreen 页面销毁后状态仍写入 SavedStateRegistry。
    c. 若使用 Navigation 组件,避免在 onCreate 中直接依赖 navController.currentBackStackEntry 做恢复,改用 SavedStateViewModelFactory 保证跨页面恢复。

  3. 防泄漏
    a. 所有异步回调统一在 onDestroyView() 中解注册:

    • RxJava:compositeDisposable.clear()
    • 协程:viewLifecycleOwner.lifecycleScope.cancel()
    • Handler:handler.removeCallbacksAndMessages(null)
      b. 动画与 Transition:在 onDestroyView() 调用 view.clearAnimation()Animator.cancel(),并把 ImageView.setImageDrawable(null) 解除 Drawable callback。
      c. 软键盘:在 onDestroyView() 把焦点置空,调用 InputMethodManager.hideSoftInputFromWindow(windowToken, 0),防止 IMM 持有 View 引用(LeakCanary 高频 case)。
      d. 图片/大图:Glide 使用 Glide.with(fragment).clear(imageView);Coil 使用 imageView.dispose().
      e. 静态变量与单例:禁止把 Fragment 或 View 传给单例;若必须传 Context,使用 requireContext().applicationContext
      f. 国内加固注意:Tinker 热修后 SavedState 的 ClassLoader 可能变化,恢复时若出现 ClassNotFoundException,应在 onCreate catch 并降级重建状态,避免 Crash。

一句话总结:
“用 SavedStateRegistry 统一保存,用 ViewModel-SavedState 做业务恢复,用 onDestroyView 清理所有引用,LeakCanary 验收。”

拓展思考

  1. 如果业务要求“页面切换不重建 Fragment”,但又要保存 RecyclerView 滚动位置,你会把 LayoutManager 状态放到 SavedStateHandle 还是自定义 SavedStateProvider?为什么?
  2. 当应用被国内 ROM 强制回收后,系统通过 ActivityRecreator 重建,此时 SavedState 里的 Parcel 大小超过 200 KB 会触发 TransactionTooLargeException,如何拆分与压缩?
  3. 在 Compose Navigation 中,没有 Fragment 了,状态保存依赖 rememberSaveableSavedStateHandle,如何与旧 Fragment 方案共存做渐进迁移?
  4. 如果团队使用 Hilt,Fragment 通过 @HiltViewModel 注入 SavedStateHandle,但单元测试需要 Mock 该 Handle,如何搭建 Robolectric + Mockito 环境验证状态恢复逻辑?