Android Fragment实现原理:生命周期、事务与状态恢复全解析
2026/9/8 3:10:39 网站建设 项目流程

Fragment 是 Android 开发里绕不开的话题,从早期的支持库一路走到现在的 Jetpack Fragment,它已经不只是“块 UI 那么简单了。很多人用 Fragment 用得很熟,但一谈到实现原理就有点懵:为什么 Fragment 会有生命周期?它和 Activity 之间到底怎么通信?事务提交后发生了什么?状态又是怎么保存和恢复的?这篇文章不打算从头到尾复述官方文档,而是从源码和实际架构的角度,把 Fragment 实现原理里几个最关键的部分拆开讲透,顺便把我在项目里踩过的一些坑也一并带上。适合已经写过一段时间 Fragment、想深入理解内部机制、或者在排查 Fragment 相关疑难 bug 的同学阅读。

我一直觉得,理解一个框架的运行机制,比背一堆接口用法要重要得多。因为只有理解了背后的设计和约束,你才能在该用 Fragment 的地方用对,在它出问题的时候快速定位,而不是靠网上搜到的各种“玄学解法”去试。

1. Fragment 的整体设计:为什么它会存在,又解决了什么问题

1.1 Fragment 不是 View,而是被托管的小型控制器

很多人一开始学 Fragment 时,容易把它当成一种 View 容器,就觉得 Fragment 就是往界面上贴一块东西。这个认知其实从一开始就跑偏了。

Fragment 从设计上更接近一个“轻量级的 Activity”,它是一个挂在 Activity 内部的控制器对象,持有自己的布局、自己的生命周期、自己的状态保存能力,但它没有 Window,也没有独立的返回事件处理机制,这些还是依赖宿主 Activity。这也是 Fragment 被设计出来的最初动机:在大屏设备上,我们希望同一个界面逻辑能在不同布局模式下复用。比如平板横屏时左边是列表、右边是详情,竖屏时则变成两个独立的页面,这种动态适配如果只靠 Activity 是做不到的。

所以 Fragment 的本质是一个状态机,它的状态变化由 FragmentManager 来驱动。FragmentManager 像是 Fragment 的管家,负责创建 Fragment、恢复 Fragment、分发生命周期事件、处理事务、维护回退栈。

这里有一个值得注意的点:Fragment 的视图(View)可以被销毁,但 Fragment 的实例和状态仍然可以保留。这意味着 Fragment 的 onDestroyView 之后,它持有的 mView 会置空,但 Fragment 对象本身还活着,还在 FragmentManager 的 mActive 集合里待着。这是理解 Fragment 生命周期和状态恢复的重要基础。

1.2 一个宿主多个 Fragment 的调度模型

一个 Activity 里可能有多个 Fragment 同时存在。比如一个典型的主页界面,底部有 Tab,每个 Tab 管理自己的 Fragment,有些界面还会做 Fragment 的嵌套。这种情况下,宿主 Activity 只需要通过 FragmentManager 来操作这些 Fragment,而不需要关心它们的内部实现。

从源码角度看,FragmentManager 是所有 Fragment 的中枢。它维持了几个关键的数据结构:

  • mBackStack:回退栈,里面存的是事务记录 Op 的列表
  • mActive:当前还活跃的 Fragment 列表(包括 view 被销毁但实例还在的)
  • mAdded:当前已经添加到 Activity 上的 Fragment 列表
  • mCreated、mPostponed 等状态集合:用于管理 Fragment 生命周期事件的分发

每次事务提交,最终都会走到一个 moveToState 的方法里,FragmentManager 会根据目标状态把 Fragment 的状态一点点推过去。Fragment 状态一共有八个层级:INITIALIZING、ATTACHED、CREATED、VIEW_CREATED、AWAITING_EXIT_EFFECTS、ACTIVITY_CREATED、STARTED、RESUMED。每次 moveToState 都会触发对应的回调,比如从 ATTACHED 到 CREATED,会调用 onCreate;从 CREATED 到 VIEW_CREATED,会调用 onCreateView。

这些状态之间的升降,不是一次跳到的,而是一步一步走的。这样设计的好处是任何一步出错,你都能知道卡在哪,也方便 FragmentManager 统一调度。

2. Fragment 生命周期背后的状态机

2.1 状态值是怎么定义的

Fragment 的状态定义在源码里其实是一组常量,不同版本有细微差别,但大致维持这样一个递增关系:

INITIALIZING 状态是 Fragment 刚被实例化时,还没有进入生命周期体系。ATTACHED 时已经和宿主关联,能通过 getActivity 拿到宿主。CREATED 时已经完成了构造和属性恢复,但视图可能还没有创建。到了 VIEW_CREATED,说明 onCreateView 的返回值已经赋值给 mView 了,但可能因为动画等原因还处在一个中间态。ACTIVITY_CREATED 说明宿主 Activity 的 onCreate 已经走完,Fragment 可以安全访问 Activity 内的视图了。STARTED 和 RESUMED 则代表 Fragment 完全对用户可见、可交互。

这个状态机最妙的一点:Fragment 的每个生命周期方法,基本都能映射到一次状态迁转。比如 onPause 对应从 RESUMED 回退到 STARTED,onStop 对应从 STARTED 回退到 ACTIVITY_CREATED(早期版本是 CREATED)。你在生命周期回调里做的事,本质上就是在响应状态变化。

2.2 状态上移与下移:每个 Fragment 都逃不过这两条路

状态上移(比如从 INITIALIZING 到 RESUMED)时,FragmentManager 会依次调用 onAttach、onCreate、onCreateView、onViewCreated、onActivityCreated、onStart、onResume。状态下移时则反过来,从 onPause、onStop、onDestroyView、onDestroy、onDetach 一路下来。

这里面有个很多开发者忽视的细节:Fragment 的 onDestroyView 和 onDestroy 是分开的。onDestroyView 只销毁视图,但 Fragment 还活着,还在 FragmentManager 的 mActive 里待着。onDestroy 才是真正销毁 Fragment。这也就是为什么官方建议在 onDestroyView 里把对视图的引用置空,避免内存泄漏。

下移的时候,FragmentManager 还会做一个特殊操作:如果 Fragment 的状态一路回退到 CREATED 以下,它会清除 mSavedViewState、mView 等视图相关数据;但 Fragment 的实例和参数(arguments)仍然保留。这样下次再进入时,可以快速从 mActive 里找到这个 Fragment,而不是重新 new 一个。

我在项目里就踩过这样一个坑:在 Fragment 的 onStop 里保存了一个 View 的引用,想着恢复界面时直接复用,结果从后台回到前台发现 View 已经 detach 了,界面崩了。后来才明白,onStop 之后 View 有可能已经被销毁,不能假设视图还一直存在。正确做法是保存数据,而不是保存 View 引用。

2.3 宿主生命周期如何映射到 Fragment 生命周期

Fragment 的宿主是 Activity,但 Fragment 并不是直接监听 Activity 的生命周期回调,而是由 FragmentManager 在 Activity 的生命周期方法里主动分发。以 ComponentActivity 为例,它在 onCreate 里会调用 mFragmentLifecycleRegistry 相关的逻辑,间接把生命周期事件传给 FragmentManager,再由 FragmentManager 去推进各个 Fragment 的状态。

这种设计带来的一个结果是:Fragment 的生命周期永远滞后于 Activity 一点点。比如 Activity 已经走完 onStart,Fragment 的 onStart 才会被调用。你如果在 Activity 的 onCreate 里去 getFragmentManager 找 Fragment,能找到实例,但此时 Fragment 可能还处于 CREATED 状态。

理解这个映射关系,对于排查生命周期相关的诡异问题非常有帮助。比如在 Fragment 的 onActivityCreated 里试图访问 Activity 的视图,这时候基本是安全的,因为 Activity 的 onCreate 已经结束了。但在 onViewCreated 里访问 Activity 的某些资源时,就要格外小心,因为 Fragment 的视图创建可能比 Activity 的某些初始化更早发生。

3. Fragment 事务机制与状态管理的核心

3.1 事务为什么要“先记录,再执行”

使用 Fragment 的人一定写过类似下面的代码:

getSupportFragmentManager().beginTransaction() .replace(R.id.container, new ExampleFragment(), "tag") .addToBackStack(null) .commit();

这里面有个容易被忽略的细节:commit() 并不是立即执行事务内容,而是先把事务记录进队列,再在某个时机异步执行。

为什么要异步执行?因为 FragmentManager 的状态管理是统一调度的,如果在 Activity 的任何生命周期阶段都能立刻执行事务,很容易出现状态不一致。比如你在一个 Fragment 的 onPause 里又去提交了一个事务,如果立即执行,FragmentManager 的状态机可能正处于 RECOMMENDED 阶段,再强行推进状态,就会导致其他 Fragment 的生命周期错乱。

源码里有一个 ArrayList 的 mPendingActions,所有 commit 的事务都会被包装成 OpGenerator 加进这个队列,等 FragmentManager 执行到 execPendingActions 时再统一处理。execPendingActions 的调用时机包括:主线程消息循环空闲时、Activity 生命周期事件发生时、以及外部主动调用 executePendingTransactions() 时。

所以 commit() 之后别指望事务马上生效,如果你真的需要立即生效,得用 commitNow()。但 commitNow 和 commit 还有一个重要区别:commitNow 不支持 addToBackStack,如果要加入回退栈,必须用 commit。

3.2 事务操作符的底层表示

一个事务里可以有 add、remove、replace、hide、show、detach、attach 这些操作。这些操作在源码里都被封装成了 Op 对象,每个 Op 保存了操作类型、涉及的 Fragment、以及一些附加参数。一个事务对应一个 BackStackRecord,它内部有一个 ArrayList 。

当 FragmentManager 执行事务时,会遍历这些 Op,逐个调用对应的方法。比如 add 操作会调用 addFragment,remove 操作会调用 removeFragment。这里有一个很重要的细节:replace 本质上不是独立操作,而是先 remove 掉所有同容器的已有 Fragment,再 add 新的 Fragment。

用 replace 时,FragmentManager 会给被替换的 Fragment 创建退出动画,新 Fragment 创建进入动画。动画执行过程中,新旧 Fragment 可能同时处于 RESUMED 状态(如果动画时长较长),这就是为什么你在做转场动画时偶尔能看到两个 Fragment 视图短暂重叠。解决这个问题的思路一般是调整动画时长,或者改用 detach 和 attach 的组合操作。

3.3 回退栈到底是怎么工作的

addToBackStack 这个功能很多人在用,但很少去想它内部的机制。

当你调用 addToBackStack(null) 后,这个 BackStackRecord 不会在事务执行完就丢弃,而是被 push 到 FragmentManager 的 mBackStack 里。当用户按下返回键时,Activity 会调用 FragmentManager 的 popBackStackImmediate() 或 popBackStack(),此时 FragmentManager 会从 mBackStack 里取出栈顶的事务,反向执行里面的所有 Op。

反向执行的意思是:原来 add 的就变成 remove,原来 remove 的就变成 add,原来是 hide 的就变成 show。这样就能把界面状态还原到事务提交之前。

回退栈还有几个容易踩坑的点:

  • 回退栈里的记录不是 Fragment,而是事务记录。如果你连续提交了多个事务,回退栈里会有多条记录。
  • 用 popBackStack(String tag, 0) 只能弹出标记为 tag 的那条记录之上的事务,而不是只弹那一条。
  • 回退栈支持 popBackStack 到某个指定记录,也支持用 POP_BACK_STACK_INCLUSIVE 标志把该记录一起弹出。
  • commit 后紧接着 popBackStack 不会立即执行,需要在 Activity 空闲时才会执行,所以有时候代码里写的 popBackStack 效果不明显,其实是时序问题。

我实际项目中曾经遇到一个 bug:连续切换 Tab 时,Fragment 越来越多,内存持续攀升。后来排查发现,是因为每次切换都用 replace 但没调用 addToBackStack,导致旧 Fragment 的被替换后本应销毁,但因为 replace 内部的事务没有正确清理旧 Fragment 的引用,导致旧实例泄漏。解决办法其实很简单:确保 replace 之前没有把 Fragment 加进回退栈,并且容器在替换时要把旧 Fragment 移除干净。

4. Fragment 状态保存与恢复

4.1 状态保存在哪个环节

Fragment 的状态保存发生在 Activity 的 onSaveInstanceState 阶段。但 Fragment 不是直接把自己的状态写进 Activity 的 Bundle,而是通过 FragmentManager 统一收集。FragmentManager 会遍历 mActive 列表里所有 Fragment,调用每个 Fragment 的 performSaveInstanceState,把每个 Fragment 的状态打包成一个 Bundle,再放进一个大的 Bundle 里,最后交给 Activity 一起保存。

这个状态包里包含的内容有:

  • Fragment 的额外状态(通过 onSaveInstanceState 自己存的数据)
  • Fragment 的视图状态(例如 EditText 里的文本、列表滚动位置)
  • Fragment 当前的状态等级
  • Fragment 的 mWho、mTag、mFragmentId 等标识信息
  • 回退栈相关状态

视图状态的保存比较特殊:Fragment 会给自己的视图根节点设置一个唯一的 ID,作为 View 状态保存在 SparseArray 里的 key。恢复时再根据这个 key 从 SparseArray 里取回状态。这也是为什么 Fragment 的布局根节点最好设置一个 ID,否则视图状态恢复可能会错乱。

4.2 重建时 Fragment 是从 newInstance 来的吗

这是 Fragment 恢复机制里最迷人也最容易踩坑的部分。

当系统因为配置变更(比如旋转屏幕)或进程被杀死后重建 Activity 时,Activity 会通过 FragmentManager 的 restoreSaveState 恢复之前保留下来的 Fragment 事务和状态。FragmentManager 会读取保存的 FragmentState 数组,根据里面记录的类名,通过反射创建 Fragment 实例,并重新组装 FragmentManager 内部的数据结构。

也就是说,重建后的 Fragment 实际上不是你在代码里 new 出来的实例,而是反射创建出来的新实例。所以你再怎么用 newInstance 传参数都没用,参数必须放在 Fragment 的 arguments 里,系统恢复时会自动把 arguments 传给新的实例。

这个机制导致了一个特别常见的坑:用空构造器约束。系统反射创建 Fragment 时依赖无参构造器,如果你在 Fragment 里定义了一个带参构造器,并且没有显式提供无参构造器,恢复时直接抛异常。

如果进程被杀死前,Fragment 被 add 进回退栈时设置了 tag,恢复后这个 tag 也会被还原。所以你在恢复后通过 findFragmentByTag("example") 能找到那个重新创建的 Fragment。

4.3 谁在管理 Fragment 的 mWho 和唯一标识

每个 Fragment 都有一个唯一的标识 mWho,它是在 FragmentManager 把 Fragment 加进 mActive 时生成的。mWho 的生成规则比较简单:由宿主的标识加一个递增的数字组成,例如 0#1、0#2。这个 mWho 是 Fragment 内部的重要标识,它被用来生成 Fragment 默认的 Tag,也用于 ViewModelStore 的 key。

Fragment 的 ViewModel 就是通过 mWho 来标识作用域的。所以 Fragment 重建后,即使 Fragment 实例变了,只要 mWho 没变,就能拿到同一个 ViewModelStore,从而实现 Fragment 内 ViewModel 在配置变更时数据不丢失。这个设计非常关键,但很多开发者并不知道 ViewModel 能否恢复的秘密就在这里。

5. 并发修改与重复操作:Fragment 实现里的两大隐患

5.1 为什么会出现 "Fragment already added" 和 "No fragment id"

Fragment 使用中最常见的两个异常:

  • IllegalStateException: Fragment already added
  • IllegalArgumentException: No fragment id found for fragment

这两个异常本质上都是因为对 FragmentManager 的状态管理不够理解导致的操作冲突。

"Fragment already added" 出现的原因是你试图把同一个 Fragment 实例 add 两次。比如你在某个方法里写了 add(fragment),但这个方法可能被多次调用,因为 FragmentManager 会校验 Fragment 的 mAdded 标识。解决方案有两个:一是每次用 new 创建新实例再 add;二是 add 之前先判断 fragment.isAdded()。

"No fragment id found" 出现的原因是你试图用 replace 或 add 到一个不存在的容器 ID 上。比如容器 View 的 ID 写错了,或者这个容器所在的布局还没有被 inflate 出来。这种问题最常见的场景是在 Activity 的 onCreate 里直接操作一个还没加载出来的子布局里的容器。

5.2 commit 和 executePendingTransactions 的使用时机

commit 是异步的,executePendingTransactions 会强制立即执行所有待处理的事务。但强制立即执行并不意味着安全。如果在 Activity 的 onSaveInstanceState 之后调用 commit,会直接抛出 IllegalStateException,因为此时 FragmentManager 已经保存过状态,无法再安全地修改。

所以 Google 在 FragmentTransaction 上提供了 commitAllowingStateLoss(),让你即使在这个阶段也能提交事务。但这个方法是双刃剑:它会把状态丢失的风险交给你自己承担。比如你在 onSaveInstanceState 之后 commit 了一个 Fragment,进程被系统杀死后,这个 Fragment 可能不会被恢复,导致下次启动时界面状态和预想不一致。

我个人的习惯是:凡是在生命周期回调之外提交 Fragment 事务,都先判断当前时机是否安全。如果是 DialogFragment 的点击事件里提交,没问题;如果在异步回调里提交,就要注意宿主 Activity 是否已经 stop 或 saveInstanceState。

6. 嵌套 Fragment 与 ChildFragmentManager

6.1 嵌套 Fragment 是如何管理的

每个 Fragment 内部都可以有自己的子 Fragment,这些子 Fragment 由 ChildFragmentManager 管理,而不是 FragmentManager。ChildFragmentManager 是 FragmentManager 的一个特化实例,每个 Fragment 实例都持有一个自己的 ChildFragmentManager。

这带来的结果是:子 Fragment 的生命周期完全由父 Fragment 控制。父 Fragment 走到 RESUMED,子 Fragment 才可能 RESUMED;父 Fragment 被销毁,子 Fragment 也会被销毁。这种层级派生关系,在源码里是通过 Fragment 的 mChildFragmentManager 和 FragmentManager 的 mParent 互相引用来实现的。

嵌套 Fragment 在开发时有个容易踩的坑:用 getActivity().getSupportFragmentManager() 去操作子 Fragment,或者用 getFragmentManager() 去找子 Fragment 找不到。正确的方式是:(getChildFragmentManager())。如果父 Fragment 是在 ViewPager2 里,那每个页面 Fragment 里的子 Fragment 管理,也是通过 getChildFragmentManager()。

6.2 子 Fragment 的状态保存也走同一个体系

嵌套 Fragment 的状态保存同样由 FragmentManager 统一处理,但区别在于:子 Fragment 的状态是保存在父 Fragment 的状态包里的。父 Fragment 的 performSaveInstanceState 会把 ChildFragmentManager 收集到的状态写进一个专门的字段,然后一起打包。

恢复时也类似:父 Fragment 恢复时,会先从自己的状态包里取出 ChildFragmentManager 的状态,再让 ChildFragmentManager 去恢复它的 mActive 列表。这就是为什么嵌套 Fragment 在配置变更后能够保持完整状态的原因。

6.3 嵌套 Fragment 的常见病:IllegalStateException 和重复添加

嵌套 Fragment 最常见的异常有两种,一个是上面提到的 "Fragment already added",另一个是 "Fragment no longer exists for key"。后者通常出现在父 Fragment 被销毁但子 Fragment 还试图恢复时。

我还遇到过一个很隐蔽的问题:在 ViewPager2 的 Fragment 里再嵌套一个 Fragment,每次滑动到该页面时都重新 add 一个新的子 Fragment,导致子 Fragment 视图不断叠加。后来排查发现,原因是 Fragment 的 onCreateView 被多次调用,add 操作也在 onCreateView 里执行了。正确做法是先在 onCreate 里判断 childFragmentManager 里是否已经有这个 Fragment,如果没有才 add。

7. Fragment 的性能优化与内存泄漏排查

7.1 Fragment 的生命周期比 Activity 更频繁

Fragment 的生命周期事件比 Activity 多很多,尤其是在 View 销毁和创建上。onCreateView 和 onDestroyView 几乎在每次切换、动画、重建时都会被调用。如果你在 onCreateView 里有大量资源初始化操作,或者 inflate 了一个非常复杂的布局,都会直接影响页面切换的流畅度。

针对这种情况,我一般有两种处理思路:

  • 布局方面,尽量用 ConstraintLayout 减少层级,避免在 Fragment 里嵌套太多深层次的 LinearLayout。
  • 逻辑方面,把和视图无关的数据初始化放到 onCreate 而不是 onCreateView;把网络请求和数据库读写放到 onStart 之后,避免频繁重复请求。

7.2 常见内存泄漏点

Fragment 相关的内存泄漏,最常见的几个点:

  • 在 Fragment 里持有 Activity 的引用不释放。比如一个静态变量持有当前 Activity,这种在 Fragment 销毁时,Activity 就可能泄漏。
  • Handler 或 Runnable 在 Fragment 销毁后还持有 Fragment 的引用。
  • 异步回调(retrofit、RxJava)在 Fragment 销毁后成功回调,如果不做判空,直接操作 View 就会崩,或者导致 Fragment 无法被回收。
  • Dialog 和 BottomSheet 相关的对象在 Fragment 销毁后没 dismiss。

排查内存泄漏的常用手段,就是用 Android Studio 的 Memory Profiler 或 LeakCanary,dump 出堆栈,看谁持有 Fragment 引用。

还有一个我每次都会提醒新人的点:在 Fragment 的 onDestroyView 里,一定要把对 View 的引用置为 null。因为 Fragment 的视图销毁后,如果 View 还挂在 window 上或者被某个对象引用,外层 Fragment 就不会被回收。

8. 常见问题排查与避坑手册

8.1 为什么 onCreate 后 onViewCreated 空指针

这个问题的原因通常是你在 Fragment 里过度依赖 onCreateView 返回的 View。onCreateView 和 onViewCreated 之间有一个完整的 view 创建过程,如果你在 onCreateView 里就把返回的 view 赋值给一个成员变量,没问题。但如果你在 onCreateView 里就对某些子 View 调用了 findViewById,而在 onViewCreated 时又去操作,就可能因为时序问题拿到 null。

更安全的写法是:onCreateView 只负责 inflate 布局,onViewCreated 里统一做 findViewById 和视图初始化。不要在 onCreateView 里做太多的事情,尤其不要访问依赖子 Fragment 已经创建的视图。

8.2 进程被杀死恢复时 Fragment 重复创建

这个问题一般出现在你没有正确处理状态的保存和恢复时。比如 Activity 的 Intent 里带了一些参数,你根据这些参数创建 Fragment。但系统恢复时不是重新走 onCreate,而是直接 restore 之前的状态,如果你在 onCreate 里无脑 new 一个 Fragment 再 add,就会和恢复出来的 Fragment 叠加。

正确写法是:

if (savedInstanceState == null) { getSupportFragmentManager().beginTransaction() .add(R.id.container, new ExampleFragment()) .commit(); }

这样系统自动恢复时就不会再重复添加。如果想更严谨,可以在 onCreate 里先查找一下是否有之前的 Fragment 实例,再决定是否创建。

8.3 回退栈弹出后 Fragment 不见了

有时候你明明调用了 addToBackStack,返回键按了几次之后发现 Fragment 找不到了。这通常是因为你在事务里用了 replace,replace 会先移除旧的 Fragment 再添加新的,出栈时再反向操作。如果旧的 Fragment 被 replace 后离开了 mAdded 列表,但在回退栈里还有它的恢复记录,出栈时它会被重新 add 回来。看起来就像“明明找不到了,怎么又回来了”。

还有一种情况是 addToBackStack 和 commitAllowingStateLoss 配合使用不当。如果在 Activity 已停止的状态下提交了带 addToBackStack 的事务,返回键弹出时可能因为状态还没准备好,导致 Fragment 显示异常。这种情况尽量用 commitNow 或在合适的生命周期时机提交。

8.4 Fragment 动画导致的空指针

Fragment 切换动画时,新旧 Fragment 可能同时处于可见状态,此时如果你在动画结束时操作旧的 Fragment 视图,可能因为视图被清理而崩溃。这种情况在 Fragment 里有动画监听器时尤其容易出现。

解决办法是使用官方的动画监听回调 API,或者在动画结束后判断 fragment.isRemoving() 再决定是否操作视图。isRemoving() 是一个很实用的方法,专门用于判断当前 Fragment 是否正在被移除。

8.5 一个通用的排查思路

遇到 Fragment 相关的疑难 bug,我推荐的排查顺序:

  • 先看崩溃堆栈,确认是哪一个类、哪个生命周期抛出的异常。
  • 再查所有事务提交的地方,判断是否有重复 add、replace、popBackStack 的时序问题。
  • 然后检查 FragmentManager 的状态,可以用反射或者调试工具查看 mActive 和 mAdded 列表里有几个 Fragment。
  • 最后怀疑状态恢复问题,看一下 savedInstanceState 里保存的数据是不是已经过期。

9. 从源码角度看 FragmentManager 的几个关键设计

9.1 为什么要让事务变成 Op 的列表

Fragment 的事务设计成 Op 列表,最大的好处是“可逆”。add 和 remove 互为逆向操作,hide 和 show 互为逆向操作,detach 和 attach 互为逆向操作。回退栈弹出时,只要把这个列表反向执行一遍,就能接近完美地还原之前的状态。

这种设计还让事务支持批量处理。一次 commit 里可以连续 add 三个 Fragment,它们会被统一调度,而不是每个 Fragment 独立走一次生命周期。这对性能有明显好处。

9.2 mActive 列表为什么不能随意退出

mActive 保存了所有活跃的 Fragment 实例,包括在回退栈里的。即使某个 Fragment 的视图已经被销毁,只要它还在回退栈中或还在 active 状态,FragmentManager 就不会清理它。这个设计保证了回退栈弹出后能快速恢复 Fragment,而不需要重新创建。

但反过来,这也带来了内存开销。如果一个 Fragment 在回退栈里待了很久,它持有的数据(比如大图Bitmap)也会一直保留,无法被回收。所以如果一个 Fragment 不再需要,记得把它从回退栈里移出,或者用 popBackStack 清理。

9.3 FragmentManager 是线程不安全的

这里要特别强调:FragmentManager 不是线程安全的。所有对 FragmentManager 的操作都必须发生在主线程。但 commit() 本身是异步的,它是把事务投递到主线程的队列里。所以即便你在子线程调用 commit(),也不会立刻崩溃,但实际操作还是会在主线程执行。

因此在子线程里不要直接操作 FragmentManager。如果非要在子线程里更新 Fragment,通过 Handler 或 LiveData 切回主线程再做。

10. 我对 Fragment 实现原理的几点体会

用了这么多年 Fragment,踩过各种奇奇怪怪的坑之后,我形成了几个核心体会。

第一,Fragment 的状态迁移模型是整个框架的基石。只要涉及 Fragment 的疑难问题,十有八九都能归结为状态问题。所以与其背生命周期回调的顺序,不如去理解状态的升降规则,以及每个状态对应的视图和数据是否还在。

第二,回退栈和状态保存这两个机制是 Fragment 的精华。很多人抱怨 Fragment 难用,其实很多时候是他们没有正确使用这两个机制。我建议每个 Android 开发者都花时间读一下 FragmentManager 和 BackStackRecord 的源码,读完之后很多以前觉得莫名其妙的现象都能解释通。

第三,合理使用嵌套 Fragment 和 ChildFragmentManager,可以构建出非常灵活的 UI 架构。但嵌套层级不要太深,超过三层就很难管理了。另外 ViewPager2 和 Fragment 的组合是目前的标配,但记住 ViewPager2 默认会预加载相邻页面,这会放大 Fragment 状态管理的复杂度。

最后一点,能不用 Fragment 就不要用 Fragment。如果你的界面逻辑足够简单,一个 Activity + 自定义 View 完全够用,没必要为了用而用。Fragment 的价值在于跨配置变更的状态保留、生命周期管理和大屏适配。当你的项目确实需要这些能力时,才有理由引入 Fragment。否则,每多一个 Fragment,就多一分状态管理的复杂度。

回到开头的那个问题,Fragment 实现原理能给我们带来什么好处?在我看来,它最大的价值不是让你写出更炫酷的代码,而是让你在面对复杂界面状态时,心里有一个清晰的图谱:这个 Fragment 现在处于什么状态,它手头有哪些数据,它即将往哪个状态迁移,以及它一旦出错,问题会出在哪一步。有了这个图谱,你在 Android 里的很多开发问题都会变得简单很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询