Fragment 生命周期与状态恢复-《Android深水区(四)》

Fragment 生命周期与状态恢复

前言

Fragment 的问题往往不是"生命周期回调顺序背不下来",而是下面这些线上现象:

  • 页面返回后再次进入,列表重复请求、重复观察 LiveData 或 Flow。
  • onDestroyView() 执行了,但 Fragment 实例还在,旧 ViewBinding 泄漏了一整棵 View。
  • 屏幕旋转后输入框文本还在,但接口数据、分页位置、临时筛选条件丢了。
  • commit() 偶发 Can not perform this action after onSaveInstanceState
  • ViewPager2 中相邻页面只到 STARTED,却误以为所有可见 Fragment 都是 RESUMED
  • 进程被系统回收后从最近任务恢复,Fragment 参数、View 状态和 ViewModel 状态表现不一致。

这些问题的根源是:Fragment 同时拥有"实例生命周期"和"View 生命周期",并且状态被拆成了 Arguments、ViewState、SavedState、NonConfig、外部持久化数据几个层次。只看 onCreate()onDestroy() 的回调表,很难解释真实项目里的恢复、泄漏和事务时机。

本文基于 Android Developers 官方 Fragment lifecycle / saving state 文档、AndroidX Fragment 1.8.x 发布说明,以及 AndroidX androidx-main 分支源码复查。涉及源码的结论以本文验证分支为准;项目中如果使用较旧 Fragment 版本,尤其是 1.5 以前的版本,建议结合 release notes 复核细节。

前置知识

建议先掌握:

  • A020:Activity 生命周期完整解析。
  • A021:Activity 启动模式与任务栈。
  • BundleViewModelSavedStateHandle 的基本用法。
  • FragmentManagerFragmentTransactionNavigation 返回栈的基本概念。

A020 关注单个 Activity 与系统交互;A021 关注 Activity 如何进入 Task。本文进入 Activity 内部,讨论一组 Fragment 如何随着宿主状态、返回栈、View 创建销毁和进程恢复而迁移。

核心概念

Fragment 生命周期不是 View 生命周期

一个 Fragment 实例可以比它的 View 活得更久。最典型的场景是 Fragment 被放入返回栈:用户离开页面时,Fragment 的 View 会被销毁,但 Fragment 实例和它的非配置状态可能仍被 FragmentManager 保留,等待用户返回时重新创建 View。

这也是官方长期强调使用 viewLifecycleOwner 的原因:UI 观察者、ViewBinding、Adapter 和动画监听器应该绑定到 View 生命周期,而不是 Fragment 实例生命周期。

生命周期对象 代表什么 典型开始点 典型结束点 适合放什么
Fragment lifecycle Fragment 实例被 FragmentManager 管理的过程 onAttach() / onCreate() onDestroy() / onDetach() 参数解析、非 UI 依赖、FragmentResult 监听
View lifecycle Fragment 返回的根 View 及其子树 onCreateView() / onViewCreated() onDestroyView() ViewBinding、Adapter、UI Flow 收集、点击监听
ViewModel lifecycle 与 Fragment 或 NavBackStackEntry 关联的非配置状态 首次获取 ViewModel owner 真正销毁 UI 状态、业务数据、异步任务协调

结论很直接:只要代码要触碰 View,就默认考虑 viewLifecycleOwner

状态恢复分成四类

官方 saving state 文档把 Fragment 状态拆成几类。工程里可以按下面这张表落地:

状态类型 例子 适合存放位置 配置变更 进程死亡恢复 注意点
Arguments userId、入口来源、只读参数 Fragment.setArguments() / by navArgs() 保留 保留 创建后不要当可变状态改来改去
View State EditText 文本、滚动位置、选中状态 View 自己的 onSaveInstanceState() 保留 保留 View 必须有稳定唯一 ID
SavedState 当前 tab、筛选条件、是否展开编辑区 onSaveInstanceState()SavedStateHandle 保留 保留 只放小而可序列化的数据
NonConfig 已加载列表、Repository 返回的领域数据 ViewModel 保留 默认不保留 进程死亡后需要通过 SavedState + 数据源重建
Persistent 用户草稿、离线缓存、服务端数据 Room/DataStore/文件/网络 保留 保留 不应塞进 Bundle

一个常见误区是把 ViewModel 当成"万能状态保险箱"。ViewModel 可以跨旋转保留,但进程死亡后会重新创建;如果要恢复到用户可感知的同一位置,需要把最小恢复键写入 SavedStateHandle 或持久层。

STARTED 是很多 UI 操作的安全线

官方生命周期文档建议把生命周期感知组件绑定到 Fragment 的 STARTED 状态。原因有两个:

  • STARTED 时,如果 Fragment 有 View,View 已经可用。
  • 这个阶段在 child FragmentManager 上执行 Fragment 事务是安全的;当状态下降到 STOPPED 后,宿主可能已经保存状态。

这也是 repeatOnLifecycle(Lifecycle.State.STARTED) 常被用于收集 UI Flow 的原因。它会在 View 到达 STARTED 后开始收集,在 STOPPED 时取消收集,避免后台页面继续刷新 UI。

整体架构

Fragment 生命周期由宿主 Activity、FragmentManagerFragmentStateManager、View 树和 Lifecycle 组件共同推进。可以先把参与者拆开:

flowchart TD Host["宿主 Activity 生命周期"] FM["FragmentManager"] FSM["FragmentStateManager"] Fragment["Fragment 实例生命周期"] ViewTree["Fragment View 树"] ViewLifecycle["viewLifecycleOwner"] Store["FragmentStore / Saved State"] VM["ViewModelStore / SavedStateHandle"] Host --> FM FM --> FSM FSM --> Fragment FSM --> ViewTree ViewTree --> ViewLifecycle FSM --> Store Fragment --> VM VM --> Store

这张图说明两个边界:

  • FragmentManager 不是简单调用回调表,而是根据宿主状态、事务、最大生命周期、返回栈、动画/转场、容器可用性等信号计算 Fragment 的目标状态。
  • Fragment 的 View 状态和 Fragment 实例状态分别保存;恢复 View 时要等 View 创建完成,恢复 Fragment 自身状态则可以在 onCreate() 阶段进行。

工作流程

向上迁移:从 attach 到 resume

典型 Fragment 创建流程如下:

stateDiagram-v2 [*] --> ATTACHED: onAttach ATTACHED --> CREATED: onCreate CREATED --> VIEW_CREATED: onCreateView/onViewCreated VIEW_CREATED --> VIEW_STATE_RESTORED: onViewStateRestored VIEW_STATE_RESTORED --> STARTED: onStart STARTED --> RESUMED: onResume RESUMED --> STARTED: onPause STARTED --> CREATED: onStop CREATED --> VIEW_DESTROYED: onDestroyView VIEW_DESTROYED --> DESTROYED: onDestroy DESTROYED --> [*]: onDetach

关键不是把图背下来,而是理解每个阶段能做什么:

阶段 可以做什么 不建议做什么
onAttach() 获取 Context、注册与宿主相关但不触碰 View 的依赖 发起 UI 渲染
onCreate() 解析 arguments、初始化非 UI 对象、恢复 Fragment 自身 SavedState 访问 bindingrequireView()
onCreateView() 创建或 inflate View 启动长生命周期收集
onViewCreated() 初始化 View、Adapter、点击事件、使用 viewLifecycleOwner 收集 UI 状态 把 View 引用交给比 View 更长寿的对象
onViewStateRestored() 读取 View 已恢复后的控件状态 重置已经由 View 自动恢复的输入
onStart() 启动可见期间的轻量 UI 协调 做主线程重活
onResume() 处理真正可交互的焦点、相机预览等 假设所有可见 Fragment 都会到 RESUMED

ViewPager2 会把离屏 Fragment 的最大生命周期限制在 STARTED,所以"可见"和"可交互"不是同一个概念。

向下迁移:保存状态与销毁 View

当页面离开前台、事务移除 Fragment、或者宿主保存状态时,生命周期会向下走。官方文档强调:向下迁移时,View lifecycle 先收到事件,然后 Fragment lifecycle 再收到事件。

这对资源释放很重要:

  • onPause():暂停需要立即停止的交互,例如相机对焦、传感器高频回调。
  • onStop():页面不可见,停止 UI 相关任务;在现代 Fragment 中,ON_STOP 是状态保存前的安全边界。
  • onDestroyView():释放所有 View 引用、清空 binding、移除 Adapter 对 View 的引用。
  • onDestroy():释放 Fragment 实例级资源;如果只是进返回栈,不一定马上调用。

Android 平台本身在 API 28 前后调整过 Activity onStop()onSaveInstanceState() 的顺序。AndroidX Fragment 文档明确说明 Fragment 会保证 ON_STOP 事件在状态保存前被调用,这降低了 child Fragment 事务和生命周期感知组件的时序风险。

返回栈:Fragment 可能活着,但 View 已经没了

如果事务调用了 addToBackStack(),返回栈会保存这次事务。当前 Fragment 被替换时,旧 Fragment 常见状态是:

text 复制代码
Fragment 实例:保留在 FragmentManager / back stack 中
Fragment View:销毁
ViewModel:通常保留
ViewState:按需要保存

用户按返回回到旧 Fragment 时,旧 Fragment 不一定重新走 onCreate(),但会重新走 onCreateView() / onViewCreated()。因此下面写法有泄漏风险:

kotlin 复制代码
private var _binding: ArticleBinding? = null
private val binding get() = _binding!!

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    _binding = ArticleBinding.bind(view)
    viewModel.uiState.observe(this) { state ->
        binding.title.text = state.title
    }
}

问题有两个:observe(this) 绑定的是 Fragment 生命周期,旧 View 销毁后观察者仍可能存在;binding 没有在 onDestroyView() 清空。正确做法见下一节。

API 使用

ViewBinding 与 Flow 收集

下面是一个更接近生产代码的 Fragment 写法。它把 View 引用限制在 View 生命周期内,把 UI 状态放到 ViewModel,并用 repeatOnLifecycle(STARTED) 控制收集时机。

kotlin 复制代码
class ArticleListFragment : Fragment(R.layout.fragment_article_list) {

    private var _binding: FragmentArticleListBinding? = null
    private val binding: FragmentArticleListBinding
        get() = checkNotNull(_binding) { "View is not available." }

    private val viewModel: ArticleListViewModel by viewModels()
    private val articleAdapter = ArticleAdapter(
        onClick = { articleId -> viewModel.openArticle(articleId) }
    )

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        _binding = FragmentArticleListBinding.bind(view)

        binding.articleList.adapter = articleAdapter
        binding.retryButton.setOnClickListener {
            viewModel.refresh()
        }

        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                launch {
                    viewModel.uiState.collect { state ->
                        binding.progress.isVisible = state.loading
                        binding.emptyView.isVisible = state.items.isEmpty() && !state.loading
                        articleAdapter.submitList(state.items)
                    }
                }
                launch {
                    viewModel.events.collect { event ->
                        when (event) {
                            is ArticleListEvent.OpenDetail -> findNavController()
                                .navigate(R.id.articleDetail, bundleOf("id" to event.articleId))
                        }
                    }
                }
            }
        }
    }

    override fun onDestroyView() {
        binding.articleList.adapter = null
        _binding = null
        super.onDestroyView()
    }
}

这里有三个刻意的点:

  • 使用 viewLifecycleOwner,不是 this
  • repeatOnLifecycle(STARTED) 会随 View lifecycle 启停收集。
  • onDestroyView() 清理 Adapter 和 binding,避免 RecyclerView、ViewHolder、图片请求间接持有旧 View。

用 SavedStateHandle 维护最小恢复键

假设文章列表支持筛选、滚动和分页。推荐把状态分层:

  • categoryId:Arguments,表示入口参数。
  • querysortSavedStateHandle,进程死亡后要恢复。
  • 已加载列表:Repository + ViewModel,配置变更时复用,进程死亡后按参数重新加载。
  • RecyclerView 滚动位置:ViewState 或 Paging/列表状态辅助恢复。
kotlin 复制代码
@HiltViewModel
class ArticleListViewModel @Inject constructor(
    private val repository: ArticleRepository,
    private val savedStateHandle: SavedStateHandle,
) : ViewModel() {

    private val query = savedStateHandle.getStateFlow("query", "")
    private val sort = savedStateHandle.getStateFlow("sort", ArticleSort.Latest)

    val uiState: StateFlow<ArticleListUiState> = combine(query, sort) { keyword, order ->
        ArticleQuery(keyword = keyword, sort = order)
    }.flatMapLatest { request ->
        repository.observeArticles(request)
    }.map { articles ->
        ArticleListUiState(items = articles, loading = false)
    }.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = ArticleListUiState(loading = true),
    )

    fun updateQuery(value: String) {
        savedStateHandle["query"] = value
    }

    fun updateSort(value: ArticleSort) {
        savedStateHandle["sort"] = value
    }
}

data class ArticleListUiState(
    val items: List<ArticleSummary> = emptyList(),
    val loading: Boolean = false,
)

SavedStateHandle 里不要放大对象、Bitmap、数据库实体列表或不可序列化对象。它应该保存"如何重建页面"的最小键,而不是保存整个页面。

commitcommitNow 与状态已保存

Fragment 事务常见规则:

kotlin 复制代码
fun Fragment.safeNavigateToFilter() {
    val manager = parentFragmentManager
    if (manager.isStateSaved) return

    manager.commit {
        setReorderingAllowed(true)
        replace<FilterFragment>(R.id.container)
        addToBackStack("filter")
    }
}

isStateSaved 为 true 时,说明宿主状态已经保存或正在保存。此时再提交会让系统无法保证恢复后事务仍一致。commitAllowingStateLoss() 只适合真正可丢弃的 UI 辅助事务,例如关闭一个不影响业务状态的临时提示;不要用它绕过导航、支付、登录、提交表单等核心流程。

commitNow() 会同步执行事务,不能和 addToBackStack() 混用;它适合测试、初始化子 Fragment 这类需要立即可见的场景。普通页面导航优先使用异步 commit {} 或 Navigation 组件。

源码分析

本文源码观察点基于 AndroidX androidx-main 分支,核心路径是:

  • fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
  • fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
  • fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java
  • fragment/fragment/src/main/java/androidx/fragment/app/FragmentStore.java

FragmentStateManager 负责计算目标状态

FragmentStateManager.computeExpectedState() 会从多个信号中取一个综合目标:

  • FragmentManager 当前状态。
  • Fragment 自己的 mMaxState,也就是 setMaxLifecycle() 的限制。
  • 是否来自 <fragment> 标签或动态容器。
  • 是否已经 mAdded
  • SpecialEffectsController 中是否有入场/退场动画或转场。
  • 是否正在 remove,且是否在 back stack 中。
  • 是否 defer start。
  • 是否处于 predictive back / transition 相关状态。

这解释了一个常见问题:Fragment 不会只因为你调用了 commit() 就立刻到 RESUMED。它最终到哪里,取决于宿主状态、容器是否可用、最大生命周期、动画和返回栈。

moveToExpectedState() 按阶梯调用生命周期方法

源码中 moveToExpectedState() 会循环比较当前状态与目标状态:

  • 向上:attach() -> create() -> createView() -> activityCreated() -> start() -> resume()
  • 向下:pause() -> stop() -> 保存状态/保存 ViewState -> destroyFragmentView() -> destroy() -> detach()

这个阶梯式设计让 Fragment 可以从任意状态推进到目标状态,而不是依赖单一入口。它也解释了为什么快速连续 replace、pop back stack、转场动画和预测返回会触发复杂 bug:这些操作都在改变目标状态或延迟某个状态边界。

View 创建和销毁是独立阶段

createView() 中,FragmentStateManager 会调用 performCreateView(),然后在有 View 的情况下:

  • 设置 fragment container tag。
  • 把 View 添加到容器。
  • 请求 Window Insets。
  • 调用 performViewCreated()
  • 分发 onFragmentViewCreated callback。
  • 把内部状态推进到 VIEW_CREATED

destroyFragmentView() 则会:

  • 从容器移除 View。
  • 调用 performDestroyView()
  • 分发 onFragmentViewDestroyed
  • 清空 mContainermView
  • mViewLifecycleOwner 置空,并让 mViewLifecycleOwnerLiveData 发出 null。

这就是为什么 ViewBinding 必须在 onDestroyView() 清空:源码层面,Fragment 的 mView 已经被置空,继续保留绑定对象等于绕开了生命周期设计。

状态保存链路

FragmentStateManager.saveState() 会保存几类信息:

  • Fragment library state:例如类名、who、containerId、tag、hidden、max lifecycle 等。
  • 用户 SavedState:调用 performSaveInstanceState(),也就是 Fragment 的 onSaveInstanceState()
  • SavedStateRegistry:给 SavedStateHandle 等组件恢复。
  • child FragmentManager state。
  • View hierarchy state:调用 View 的 saveHierarchyState()
  • View lifecycle owner registry state。
  • Arguments。

注意源码里只有 Fragment 至少到达 CREATED 之后,才会保存用户 onSaveInstanceState() 相关状态。AndroidX Fragment 1.8.8 的 release notes 也修复过 setMaxLifecycle(INITIALIZED) Fragment 保存状态导致崩溃的问题,这说明"还没创建的 Fragment 不应假设有完整可保存状态"是一个真实边界。

线程模型

Fragment 生命周期、事务执行、View 创建销毁都运行在主线程。Repository 请求、数据库查询、图片解码可以在后台线程,但结果回到 UI 时必须经过 View lifecycle 保护。生产代码里最稳的组合是:

  • ViewModel 用 viewModelScope 管理业务协程。
  • Fragment 用 viewLifecycleOwner.lifecycleScope 管理 UI 收集。
  • Flow 用 repeatOnLifecycle(STARTED) 控制订阅窗口。
  • View 层释放在 onDestroyView(),不要等待 onDestroy()

实战案例

场景:订单详情页旋转、返回栈、进程恢复都要稳定

订单详情页有三个状态:

  • orderId:来自列表点击,是页面身份。
  • selectedTab:用户当前看的 tab,需要旋转和进程恢复。
  • orderDetail:接口数据,配置变更不应重复请求,进程死亡后可重新拉取。

推荐设计:

kotlin 复制代码
class OrderDetailFragment : Fragment(R.layout.fragment_order_detail) {

    private val args: OrderDetailFragmentArgs by navArgs()
    private val viewModel: OrderDetailViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel.setOrderId(args.orderId)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val binding = FragmentOrderDetailBinding.bind(view)

        binding.tabs.addOnTabSelectedListener { tab ->
            viewModel.selectTab(tab.position)
        }

        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect { state ->
                    binding.title.text = state.title
                    binding.tabs.selectTab(binding.tabs.getTabAt(state.selectedTab))
                    binding.content.text = state.content
                }
            }
        }
    }
}

@HiltViewModel
class OrderDetailViewModel @Inject constructor(
    private val repository: OrderRepository,
    private val savedStateHandle: SavedStateHandle,
) : ViewModel() {

    private val orderId = MutableStateFlow<String?>(null)
    private val selectedTab = savedStateHandle.getStateFlow("selected_tab", 0)

    val uiState: StateFlow<OrderDetailUiState> = orderId
        .filterNotNull()
        .distinctUntilChanged()
        .flatMapLatest { id ->
            combine(repository.observeOrder(id), selectedTab) { detail, tab ->
                OrderDetailUiState.from(detail, tab)
            }
        }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), OrderDetailUiState())

    fun setOrderId(value: String) {
        if (orderId.value == null) orderId.value = value
    }

    fun selectTab(index: Int) {
        savedStateHandle["selected_tab"] = index
    }
}

这套分层能处理三种破坏性操作:

  • 旋转:View 重建,ViewModel 保留,selectedTaborderDetail 继续可用。
  • 返回栈:旧 View 销毁,Fragment/ViewModel 通常保留,返回时重新创建 View 并恢复 UI。
  • 进程死亡:Fragment arguments 和 SavedStateHandle 恢复,ViewModel 重新拉取 orderDetail

调试建议

Fragment 问题需要看"谁还活着"。推荐先打开日志:

bash 复制代码
adb shell setprop log.tag.FragmentManager VERBOSE
adb logcat -s FragmentManager

再结合下面问题定位:

  • 旧页面重复观察:检查是否用了 viewLifecycleOwner
  • 状态保存后提交事务:检查调用点是否在 onStop() 后、是否忽略了 isStateSaved
  • 返回后 UI 空白:检查 onCreateView() 是否依赖了一次性变量,或者 ViewModel 是否因 owner 选错被清理。
  • 输入状态丢失:检查 View 是否有稳定 ID,自定义 View 是否实现保存恢复。
  • 旋转后接口重复请求:检查请求是否放在 Fragment onViewCreated() 而不是 ViewModel 或冷启动控制逻辑中。

性能优化

控制 View 重建成本

Fragment 进返回栈时 View 可能销毁再创建。复杂页面如果在 onViewCreated() 做大量同步初始化,就会让返回手势或页面切换卡顿。优化方向:

  • Adapter、DiffUtil、图片加载器、播放器控制器等重对象尽量复用,但不要持有旧 View。
  • 大列表使用 ListAdapter / Paging,避免每次恢复全量刷新。
  • onViewCreated() 中只绑定 UI 和启动收集,数据准备放到 ViewModel。
  • 自定义 View 的状态恢复保持轻量,不要在 onRestoreInstanceState() 做 IO。

避免重复请求

重复请求通常不是网络层问题,而是生命周期边界放错:

  • 不要在 onResume() 无条件请求接口。
  • 不要在 onViewCreated() 每次 View 重建都触发不可幂等请求。
  • ViewModel 中使用 stateIn(WhileSubscribed(5_000)) 或明确缓存策略,避免短暂停止/启动导致重新订阅风暴。
  • 用户主动刷新和页面首次加载要分成不同事件。

降低 Bundle 压力

onSaveInstanceState() 走 Binder/Parcel 相关通道时,过大的 Bundle 会带来性能和稳定性风险。经验规则:

  • Bundle 只放 ID、分页 key、tab index、布尔开关、小字符串。
  • 列表数据、图片、富文本、文件内容放 Repository、数据库或磁盘缓存。
  • 自定义 Parcelable 要注意版本兼容和 ClassLoader。
  • 对于可以从服务端或数据库恢复的数据,只保存恢复键。

转场和预测返回

AndroidX Fragment 1.8.x 多个 patch 都和 predictive back、transition、setMaxLifecycle、pop back stack 相关。对复杂动画页面,建议:

  • 避免同一帧内连续提交多个互相冲突的事务。
  • 使用 setReorderingAllowed(true) 让 FragmentManager 优化操作顺序。
  • 对关键导航路径写回归测试,覆盖快速返回、取消返回手势、旋转后返回。
  • 升级 Fragment 版本时阅读 release notes,不只看编译是否通过。

常见问题

1. 为什么 Fragment 的 onDestroyView() 执行了,onDestroy() 没执行?

因为 Fragment 实例和 View 是两套生命周期。进入返回栈时,Fragment 实例可能保留,View 被销毁以释放 UI 资源。此时应清理 ViewBinding、Adapter 对 View 的引用和 UI 收集,不要等 onDestroy()

2. observe(this)observe(viewLifecycleOwner) 有什么区别?

this 绑定 Fragment 实例生命周期;viewLifecycleOwner 绑定 Fragment View 生命周期。UI 更新应使用 viewLifecycleOwner,否则 View 销毁后观察者仍可能尝试更新旧 View,导致泄漏、重复渲染或崩溃。

3. onSaveInstanceState() 能保存接口返回的列表吗?

技术上可以尝试序列化小列表,但不推荐。它应该保存最小恢复状态,例如查询条件、分页 key、选中 tab。接口数据应放 Repository 缓存、数据库或 ViewModel,并在进程死亡后根据恢复键重新加载。

4. 什么时候用 Fragment arguments,什么时候用 SavedStateHandle?

Arguments 适合"页面身份和入口参数",例如 orderIdSavedStateHandle 适合"用户在页面内改变、且进程死亡后要恢复的轻量状态",例如当前 tab、筛选条件、草稿开关。

5. 为什么状态保存后不能提交 FragmentTransaction?

宿主已经保存状态后,再提交事务可能无法被系统记录到恢复快照中。恢复后 UI 栈和业务预期会不一致。核心导航不要用 commitAllowingStateLoss() 绕过,应调整触发时机或在恢复后重放事件。

6. onActivityCreated() 还能用吗?

不建议。AndroidX Fragment 中它已经被废弃。触碰 View 的初始化放到 onViewCreated();非 UI 初始化放到 onCreate();需要观察 Activity 生命周期时,显式使用 Activity 的 Lifecycle

7. Fragment 可以不创建 View 吗?

可以。Fragment 允许 onCreateView() 返回 null。无 View Fragment 没有 View lifecycle,调用 viewLifecycleOwner 会有时机限制。现代项目里无 UI 任务更推荐使用 ViewModel、LifecycleService、WorkManager 或其他更明确的组件。

面试考点

基础题

  1. Fragment 生命周期和 Activity 生命周期有什么关系?
  2. onCreate()onCreateView()onViewCreated() 分别适合做什么?
  3. 为什么 ViewBinding 要在 onDestroyView() 置空?
  4. viewLifecycleOwner 解决了什么问题?
  5. Fragment arguments 和 onSaveInstanceState() 的职责有什么区别?

进阶题

  1. Fragment 进入 back stack 后,实例、View、ViewModel 分别是什么状态?
  2. repeatOnLifecycle(STARTED) 相比在 onViewCreated() 直接 collect 有什么优势?
  3. 为什么 commitAllowingStateLoss() 不是通用修复方案?
  4. ViewPager2 为什么会让离屏 Fragment 停在 STARTED
  5. 进程死亡恢复后,ViewModel、SavedStateHandle、Room 缓存各自负责什么?

源码题

  1. FragmentStateManager.computeExpectedState() 会受哪些因素影响?
  2. moveToExpectedState() 为什么要按状态阶梯循环推进?
  3. saveState() 会保存哪些内容?哪些内容不会保存?
  4. destroyFragmentView() 为什么要把 mViewLifecycleOwnerLiveData 置为 null?
  5. setMaxLifecycle(INITIALIZED) 这类边界为什么容易影响状态保存?

回答源码题时,不要只背方法名。更好的结构是:先说明触发条件,再说明参与类和关键字段,最后说明对业务代码的约束。

总结

Fragment 的稳定使用模型可以压缩成五句话:

  • Fragment 实例生命周期不等于 View 生命周期,UI 观察和 View 引用默认绑定 viewLifecycleOwner
  • 状态要分层:Arguments 表示页面身份,ViewState 交给 View,SavedState 保存最小恢复键,NonConfig 放 ViewModel,持久数据放数据源。
  • STARTED 是 UI 收集和 child Fragment 事务的重要安全线。
  • FragmentStateManager 会综合宿主状态、最大生命周期、返回栈、容器、动画和保存状态来计算目标状态。
  • 遇到生命周期 bug,先判断"实例是否还活着、View 是否还活着、状态保存是否已经发生",再决定修复位置。

下一篇 A023 会进入 Intent、显式/隐式启动和 PendingIntent,继续讨论组件之间如何传递意图和状态。

Review Result

Dimension Score Notes
Accuracy 5 基于 Android Developers 2026-02-26 Fragment 文档、AndroidX Fragment 1.8.x release notes 与 AndroidX androidx-main 源码路径复核。
Structure 5 从线上问题进入,依次讲生命周期、状态分层、API、源码、实战和面试。
Originality 5 示例、状态分层表、调试清单和 Mermaid 图均为本文重构表达。
Practicality 5 覆盖 ViewBinding 泄漏、Flow 收集、事务时机、进程死亡恢复和性能边界。
Source Analysis 5 明确 FragmentStateManagersaveState()destroyFragmentView() 调用链和主线程模型。
Performance 4 覆盖 View 重建、重复请求、Bundle 压力、转场预测返回;未提供量化 benchmark,符合基础篇定位。
SEO 5 Front matter、关键词、标题、描述、FAQ、面试考点和扩展阅读完整。

扩展阅读

相关推荐
萝卜er1 小时前
Intent 显式、隐式与 PendingIntent-《Android深水区(五)》
android
Kapaseker3 小时前
一文吃透 Kotlin 集合操作符
android·kotlin
三少爷的鞋5 小时前
Main-safe:现代Android 架构真正的分水岭
android
沐怡旸13 小时前
深入解析 Android Performance Analyzer (APA) 底层架构与技术原理
android
李斯维20 小时前
从历史的角度看 Android 软件架构
android·架构·android jetpack
plainGeekDev1 天前
Activity 间传值 → Navigation 参数
android·java·kotlin
用户41659673693551 天前
Android WebView 加载 file:// 离线页面调试教程
android·前端
plainGeekDev1 天前
onActivityResult → ActivityResult API
android·java·kotlin
随遇丿而安1 天前
第10周:Activity 基础功能与生命周期优化
android