Android Fragment 生命周期与 FragmentManager 深度解析:从事务队列到状态恢复的全链路

上周排查一个线上 crash,堆栈指向 FragmentManager.executePendingTransactions() 之后访问 View 导致的 NPE。代码逻辑看起来没问题——commit() 之后立刻 findFragmentByTag() 拿到的 Fragment 实例不为 null,但它的 View 却是 null。

这个问题引出了 Fragment 框架中最核心也容易踩坑的一条链路:事务不是同步执行的

FragmentTransaction 的异步本质

FragmentTransaction.commit() 的行为很容易被误解。方法名暗示”提交即生效”,实际上它只是把操作排入一个事务队列(mPendingActions),真正的执行发生在主线程消息队列的下一个空闲时刻。

// 这段代码有隐患
supportFragmentManager.commit {
    replace(R.id.container, MyFragment())
}
val fragment = supportFragmentManager.findFragmentById(R.id.container) as MyFragment
fragment.updateContent(data) // fragment.view 可能还是 null

commit() 返回后,Fragment 实例已创建,onAttach()onCreate() 已执行,但 onCreateView() 尚未调用。Fragment 对象存在,视图树还没构建。

FragmentManager 的处理流程如下:

  1. commit() → 事务入队 mPendingActions
  2. 主线程 Looper 处理完当前消息
  3. FragmentManager.execPendingActions() 被触发
  4. 逐个执行队列中的事务 → 调用 onCreateView()onViewCreated()

需要在 commit 后立即操作 View,用 commitNow() 替代:

supportFragmentManager.commitNow {
    replace(R.id.container, MyFragment())
}
// 此时 view 已创建,可以安全操作

commitNow() 同步执行事务,跳过队列调度。代价是它不能加入回退栈——跳过了状态管理机制。

回退栈与状态保存的博弈

回退栈的机制很精巧,但出问题时也最难排查。它的核心逻辑:addToBackStack() 将一个事务标记为可逆,系统在用户按返回键时执行反向操作。

反直觉的地方在于:popBackStack 不是把 Fragment 销毁重建,而是基于已保存的状态进行恢复

supportFragmentManager.commit {
    replace(R.id.container, FragmentA())
    addToBackStack("tag_a")
}
// 用户操作后,FragmentA 被 FragmentB 替换
supportFragmentManager.commit {
    replace(R.id.container, FragmentB())
    addToBackStack("tag_b")
}
// 按返回键 → popBackStack → FragmentA 恢复

popBackStack() 调用时,FragmentA 的状态(包括 savedInstanceState)从未真正丢失。FragmentManager 内部维护了一个 mBackStack 列表,每个条目保存了事务操作的反向操作(Op 的逆操作)以及 Fragment 的完整状态快照。

这就是为什么 popBackStack 之后,Fragment 的 onCreateView() 会重新调用,但 savedInstanceState 不为 null——看起来像重建,实际是恢复。

踩过的一个坑:popBackStack() 后通过 findFragmentByTag() 拿到的 Fragment 实例,和之前创建的是同一个 Java 对象。如果内部持有对旧 View 的引用而没有在 onDestroyView() 中清理,就会出现诡异的 View 操作异常。

class MyFragment : Fragment() {
    private var rootView: View? = null
    
    override fun onCreateView(...): View {
        return if (rootView == null) {
            rootView = inflater.inflate(R.layout.fragment_my, container, false)
        } else {
            // onDestroyView 后 rootView 还是旧引用
            // 需要重建或者正确处理
            rootView
        }
    }
    
    override fun onDestroyView() {
        super.onDestroyView()
        rootView = null // 这一行常被遗漏
    }
}

ViewModel 与 Lifecycle 的协同治理

配置变更(屏幕旋转)导致 Fragment 重建,但 ViewModel 需要存活——这是 Fragment 生命周期中最常见的两难场景。

Jetpack 的做法是将 ViewModel 的生命周期与 Fragment 的 View 生命周期解耦:

  • ViewModel 绑定到 Fragment.getViewModelStore(),所有者是 Fragment 实例本身
  • Fragment 因配置变更重建时,如果使用了 retainInstance(或在 Navigation 组件中自动处理),ViewModelStore 被保留
  • onDestroy() 调用时,ViewModelStore 不一定被清空——只有非配置变更导致的销毁才会触发 clear()
Fragment 创建 → onCreate() → ViewModelProvider 获取/创建 ViewModel

onCreateView() → View 创建,LiveData 观察 View 状态

配置变更 → onDestroyView() → View 销毁(ViewModel 存活)

onCreateView() → 新 View 创建 → 重新绑定 LiveData

核心事实:ViewModel 的生命周期长于 Fragment 的 View。所以将 View 引用传给 ViewModel 是反模式——配置变更后 ViewModel 持有的 View 引用已失效。

// 反模式:ViewModel 持有 View 引用
class MyViewModel : ViewModel() {
    var textView: TextView? = null // 屏幕旋转后变成悬空引用
}

// 正确做法:通过 LiveData/StateFlow 暴露数据,View 层观察
class MyViewModel : ViewModel() {
    private val _text = MutableLiveData<String>()
    val text: LiveData<String> get() = _text
}

不把 View 传进 ViewModel,这条原则说起来简单,实际项目中依然反复踩坑——尤其是需要调用 View 方法(如 requestFocus()smoothScrollToPosition())时,总有人图方便直接存个引用。

Fragment.getViewLifecycleOwner() 和 Fragment 自身作为 LifecycleOwner 的区别也容易被忽略。前者从 onCreateView()onDestroyView() 之间活跃;后者从 onCreate()onDestroy() 一直活跃。如果在 onCreateView() 中用 this.lifecycleScope 启动协程,View 已销毁但 Fragment 未销毁的阶段,协程可能仍在操作已失效的 View:

// 正确做法:使用 viewLifecycleOwner 限定协程的作用域
viewLifecycleOwner.lifecycleScope.launch {
    // 当 onDestroyView 调用时,协程自动取消
    viewModel.data.collect { updateUI(it) }
}

onSaveInstanceState 的调用时机陷阱

onSaveInstanceState() 的调用规则在 Fragment 中有一套复杂逻辑。最常见的误解是认为它只在 Activity 进入后台时调用,实际触发条件更广:

  1. Activity 的 onSaveInstanceState() 被调用时,会级联调用所有 Fragment 的该方法
  2. commit() 一个事务并 addToBackStack() 后,当事务执行时也会触发
  3. Fragment 进入回退栈时,系统需要保存其状态以备恢复

一个花了半天排查的问题:Fragment 在 onSaveInstanceState() 中保存了一个复杂对象,恢复时发现对象的某个字段为 null。原因是该对象没有实现 Parcelable/Serializable,Fragment 框架在序列化时静默跳过了该字段,没有任何异常提示。

FragmentStateManager.saveState()
  → Fragment.mSavedFragmentState = Bundle()
  → Fragment.performSaveInstanceState(mSavedFragmentState)
      → onSaveInstanceState(outState) // 你的代码在这里
  → FragmentState 被序列化存入 Bundle

我的原则:onSaveInstanceState() 中只存简单的 Parcelable 数据或基本类型。复杂对象用 ViewModel 承载,序列化相关逻辑单独处理。

日常开发的几条经验

做了多年 Android 开发,Fragment 相关的问题依然占据线上 crash 的不小比例。几条反复验证过的经验:

事务同步性:需要即时操作 View 用 commitNow(),需要回退栈能力用 commit(),两者只能择一。commitNow() 不能与 addToBackStack() 组合,API 层面有硬约束。

View 引用生命周期onDestroyView() 中清空所有 View 引用。用 viewLifecycleOwner 替代 this 作为 LiveData 观察和协程作用域的所有者,让框架帮你管理取消时机。

状态保存粒度:区分”配置变更”和”进程死亡”两种保存场景。前者用 ViewModel,后者需要 onSaveInstanceState + Parcelable。不要因为 ViewModel 存在就完全放弃 onSaveInstanceState

Fragment 框架设计于 2011 年,承载了太多历史包袱。理解它的事务队列、回退栈和状态恢复机制,不是为了欣赏设计的优雅,而是为了在踩坑时能迅速定位根因。线上 crash 不会等人——这是被 Fragment 教育了多年后最朴素的感受。