如何正确处理 Fragment 的状态保存与恢复,避免内存泄漏?
解读
在国内大厂面试中,这道题常被用来区分“会用 Fragment”与“懂 Fragment”。
面试官想听到的不只是「重写 onSaveInstanceState」这种入门答案,而是:
- 系统销毁 Fragment 的几种场景(配置变更、内存回收、后台被杀)
- 状态保存的完整链路(FragmentManager、SavedStateRegistry、ViewModel-SavedState)
- 恢复时如何避免持有旧引用导致内存泄漏(View、Drawable、Animator、Listener、RxJava 订阅、协程 Job)
- 在复杂场景(ViewPager2+Fragment、嵌套 Fragment、Navigation 组件、Hilt 注入)下的最佳实践
- 国内 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 泄漏
答案
分三步回答:保存、恢复、防泄漏。
-
保存
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
。 -
恢复
a. 在onViewCreated()
或onCreate()
中通过savedInstanceState
取回数据;若使用 SavedStateHandle 可直接读取,无需判断 null。
b. 对于 ViewPager2 场景,给FragmentStateAdapter
传入lifecycleOwner
与SavedStateRegistryOwner
,确保 offscreen 页面销毁后状态仍写入 SavedStateRegistry。
c. 若使用 Navigation 组件,避免在onCreate
中直接依赖navController.currentBackStackEntry
做恢复,改用SavedStateViewModelFactory
保证跨页面恢复。 -
防泄漏
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。
- RxJava:
一句话总结:
“用 SavedStateRegistry 统一保存,用 ViewModel-SavedState 做业务恢复,用 onDestroyView 清理所有引用,LeakCanary 验收。”
拓展思考
- 如果业务要求“页面切换不重建 Fragment”,但又要保存 RecyclerView 滚动位置,你会把 LayoutManager 状态放到 SavedStateHandle 还是自定义 SavedStateProvider?为什么?
- 当应用被国内 ROM 强制回收后,系统通过 ActivityRecreator 重建,此时 SavedState 里的 Parcel 大小超过 200 KB 会触发 TransactionTooLargeException,如何拆分与压缩?
- 在 Compose Navigation 中,没有 Fragment 了,状态保存依赖
rememberSaveable
与SavedStateHandle
,如何与旧 Fragment 方案共存做渐进迁移? - 如果团队使用 Hilt,Fragment 通过
@HiltViewModel
注入 SavedStateHandle,但单元测试需要 Mock 该 Handle,如何搭建 Robolectric + Mockito 环境验证状态恢复逻辑?