深入剖析 Fragment:从使用指南、核心调度到事务原理
深入剖析 Fragment:从使用指南、核心调度到事务原理
在 Android 开发早期,开发者习惯于将所有的 UI 逻辑与生命周期统统塞进 Activity 中。随着平板电脑和复杂业务的出现,这种方式让 Activity 迅速膨胀为一个难以维护的“大泥球”。更致命的是,作为进程级、基于 Binder 调度的重量级组件,Activity 的切换成本极高,且无法在同一个屏幕上进行灵活的局部组合。
为了解决UI 模块化复用与大屏多面板展示的问题,Android 3.0 引入了 Fragment。 如果将 Activity 比作是一个“戏台”,那么 Fragment 就是戏台上的“活动布景与演员”。它们有着几乎等同于 Activity 的生命周期回调,但更为轻量;它们不具备独立的上下文,必须完全寄生在宿主 Activity 中生存。
本文将摒弃走马观花的介绍,从详尽的工程使用指南出发,一路深入 FragmentManager 的状态机调度机制与 FragmentTransaction 的事务底层源码,并深度剖析实际开发中最容易踩进去的四个“天坑”。
一、 历史渊源:为什么 Fragment 会有两套 API?
很多老 Android 开发者或初学者在翻看源码或阅读旧文章时,常会产生一个巨大的疑惑:为什么 Android 框架里存在两套 Fragment?
一套是 android.app.Fragment(俗称 Framework 版),另一套是 androidx.fragment.app.Fragment(旧称 Support Library 版/v4 包版)。
这个历史遗留问题的核心,折射出的是 Android 系统更新碎片化与组件迭代速度之间的巨大矛盾。
1. 起源:绑定在系统 ROM 中的原生 Fragment
Fragment 最早是在 Android 3.0 (API 11) 为了适配平板大屏而引入的。当时,Google 直接将其硬编码进了 Android 系统框架层,即 android.app.Fragment。
这就带来了一个致命的问题:它只能运行在 Android 3.0 及以上的手机上。 如果开发者想兼容占当时市场主导地位的 Android 2.x 手机,原生的 Fragment 根本无法使用。
2. 破局:Support Library 的降维打击
为了让所有设备都能用上 Fragment,Google 推出了大名鼎鼎的 android.support.v4 包。
他们将 Fragment 的源码从系统层“抽离”出来,打包成一个第三方 Library 供开发者引入。这意味着,哪怕用户的手机系统再老,只要 App 里打包了这个库,也能跑起 Fragment 逻辑。
后来,Google 发现了这种“解耦”模式的巨大威力:
如果是原生系统层的 android.app.Fragment 出了严重 Bug,或者需要新增结合 Lifecycle 的现代特性,Google 毫无办法——只能等各大手机厂商发善心推送系统 OTA 固件更新,这在 Android 阵营几乎是不可能的。
而对于 Support Library 中的 Fragment,Google 只需要发布一个新的版本号,开发者在 build.gradle 里升级一下依赖,Bug 就修好了。
3. 终局:AndroidX 一统江湖
随着时间推移,Support Library 越来越臃肿,包名极其混乱。于是 Google 痛下决心,推出 AndroidX 统一了架构体系,原来的 v4 版 Fragment 摇身一变,成为了 androidx.fragment.app.Fragment。
在 Android 9.0 (API 28) 时,Google 正式下达了“格杀勿论”的指令:原生 android.app.Fragment 被彻底标记为 @Deprecated。
因此,在 2026 年的今天,所有的工程都必须、且只能使用 androidx 版本的 Fragment。原生 Fragment 已经沦为一段不应再被触碰的历史标本。
二、 Fragment 现代工程化使用指南
在过去,Fragment 的使用非常混乱,甚至官方早期的 API 也存在诸多缺陷。以下是基于 2026 年现代 AndroidX 环境下,最规范、最推荐的 Fragment 核心用法。
1.1 Fragment 的声明与容器插入
静态插入
早年间,我们常在 XML 中直接使用 <fragment> 标签。这有一个致命缺陷:该标签内部是通过 LayoutInflater 反射实例化的,这意味着该 Fragment 的生命周期会与宿主 Activity 强绑定,你甚至无法使用 FragmentTransaction 来动态替换它。
现代最佳实践:使用 FragmentContainerView
自 AndroidX 之后,Google 强烈建议使用 <androidx.fragment.app.FragmentContainerView> 作为 Fragment 的宿主容器。它修复了传统的 FrameLayout 在执行 Fragment 动画时 Z 轴层级紊乱的问题。
<!-- activity_main.xml -->
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
动态添加
在宿主 Activity 中,我们需要通过 FragmentManager 来将 Fragment 实例添加到容器中:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 关键点:判空处理。
// 当发生配置改变(如屏幕旋转)或内存重启时,系统会自动恢复之前添加的 Fragment。
// 如果这里不加 savedInstanceState == null 的判断,每次 onCreate 都会再加一个新的,导致 Fragment 重叠!
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.setReorderingAllowed(true) // 优化事务状态变化,推荐开启
.add(R.id.fragment_container, UserProfileFragment(), "user_profile")
.commit()
}
}
}
1.2 Fragment 之间的数据传递与通信
这是 Fragment 演进中变化最大的部分。以前,我们在 Fragment 间通信时,往往需要强制让 Activity 实现某个 Interface,再通过 (MyInterface) getActivity() 进行丑陋且高耦合的中转。
如今,这种方式已被完全淘汰,取而代之的是以下两种现代方案:
方案一:Fragment Result API(适用于一次性数据回调)
用于替代 startActivityForResult 或接口回调。非常适合诸如“弹出选择界面,返回选中的结果”这种场景。
// --- 数据提供方(例如一个选择列表 Fragment) ---
class SelectItemFragment : Fragment() {
fun onItemSelected(itemId: String) {
// 使用 Fragment KTX 扩展,设置返回结果
setFragmentResult("requestKey", bundleOf("selectedId" to itemId))
parentFragmentManager.popBackStack()
}
}
// --- 数据消费方(例如主界面 Fragment) ---
class MainFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 注册监听器。注意这里传递的是 this (Fragment 级别生命周期)
setFragmentResultListener("requestKey") { requestKey, bundle ->
val result = bundle.getString("selectedId")
// 更新 UI
}
}
}
方案二:共享 ViewModel(适用于复杂状态与持续的数据流同步) 如果两个 Fragment 呈现的是同一块业务(比如:列表 Fragment 和详情 Fragment),它们应该监听同一个数据源。
class SharedViewModel : ViewModel() {
val selectedItem = MutableLiveData<Item>()
fun selectItem(item: Item) { selectedItem.value = item }
}
class ListFragment : Fragment() {
// 使用 activityViewModels 委托,获取的是绑定在 Activity 作用域的 ViewModel 实例
private val viewModel: SharedViewModel by activityViewModels()
fun onItemClick(item: Item) { viewModel.selectItem(item) }
}
class DetailFragment : Fragment() {
private val viewModel: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 监听变化,始终保持 UI 和 ViewModel 状态一致
viewModel.selectedItem.observe(viewLifecycleOwner) { item ->
showDetail(item)
}
}
}
三、 核心架构:FragmentManager 的管理哲学
会用 Fragment 仅仅是入门,真正的内功在于理解其背后的管理模型。
在源码层面,Fragment 就是一个普通的 Java 对象,它自己无法感应到手机屏幕的旋转、无法感知系统的按键。它之所以表现得像一个 Activity,全靠宿主的挂载与 FragmentManager 的状态机调度。
2.1 宿主挂载机制:FragmentController 与 HostCallback
我们以 FragmentActivity 为例,Activity 和 Fragment 之间并非直接关联。这其中有一套严密的“经纪人”代理模型:
FragmentActivity(金主):拥有真实的系统窗口(Window)和真实的系统生命周期。FragmentController(经纪公司):Activity 内部持有该对象,它作为统一的入口,负责将 Activity 的生命周期事件向下传递。FragmentHostCallback(经纪人):它持有了 Activity 的Context、Handler和Window,将其“借给”寄生在内部的 Fragment 使用。比如 Fragment 调用getContext()或getSystemService(),本质上都是在调用HostCallback里缓存的 Activity 引用。
2.2 FragmentManagerImpl:核心数据结构
FragmentManager(具体实现类为 FragmentManagerImpl)内部究竟存了什么?它内部持有一个叫做 FragmentStore 的数据结构,这个仓库里有两份非常核心的名单:
mActive(所有存活的 Fragment 列表):这是一个 HashMap。只要 Fragment 实例被创建且未被彻底销毁,都会存在这里。这包括被放入回退栈、视图已经被销毁但实例还在的 Fragment。mAdded(当前展示的 Fragment 列表):这是一个 ArrayList。代表当前正在屏幕上活跃的、其 View 被挂载在布局树中的 Fragment。
2.3 状态机模型:moveToState()
Fragment 的生命周期回调(如 onCreate, onResume)并不是系统直接调用的。
当宿主 Activity 执行 onResume 时,它会通知内部的 FragmentManager:“我现在是 Resumed 状态了,让所有小弟跟进。”
此时,FragmentManager 会遍历 mAdded 列表,对每一个 Fragment 调用极其核心的方法:moveToState(newState)。
Fragment 的状态在内部被定义为整型常量(例如 INITIALIZING = -1, CREATED = 1, ACTIVITY_CREATED = 4, RESUMED = 7 等)。
moveToState 是一个巨大的阶梯式状态机。如果目标状态是 RESUMED (7),而当前 Fragment 是 CREATED (1),状态机会利用 switch-case 跌落的特性,依次执行:
- 从 1 升到 2:回调
onCreateView和onViewCreated。 - 从 2 升到 4:回调
onActivityCreated。 - 从 4 升到 5:回调
onStart。 - 升到 7:回调
onResume。
反之,如果是状态下降(如 Activity 被暂停),则执行相反的销毁生命周期回调。这就是 Fragment 复杂生命周期的底层真理:它只是一堆被 switch-case 按序调用的普通方法而已。
2.4 管理器迷宫:Support、Parent 与 Child 的层级树
在 Fragment 开发中,最容易让人头晕的就是各种各样的 Manager 获取方法。我们需要从历史和架构树的维度来理清它们:
1. 历史遗留:getFragmentManager() vs getSupportFragmentManager()
getFragmentManager():这是 Android 3.0 时代原生android.app.Fragment的管理器。由于原生 Fragment 已被淘汰,在现代 AndroidX 开发中,不应再使用该方法。getSupportFragmentManager():这是FragmentActivity提供的 API,用于获取管理 AndroidX (原 Support 包) Fragment 的顶级管理器。在宿主 Activity 中,它是整棵 Fragment 树的“全局根节点”。
2. 架构树的枝与叶:Parent 与 Child Fragment 支持无限嵌套。为了管理这种嵌套,框架在内部构建了一棵严密的多叉树:
parentFragmentManager(父级管理器):它是把当前 Fragment 添加到容器里的那个 Manager。如果当前 Fragment 是直接添加到 Activity 的,那它的parentFragmentManager本质上就是 Activity 的supportFragmentManager。如果它是被添加到另一个 FragmentA 内部的,那它的parentFragmentManager就是 FragmentA 的childFragmentManager。childFragmentManager(子级管理器):每个 Fragment 实例内部都自带且独享一个私有的 Manager。当你需要在当前 Fragment 内部再嵌套子 Fragment(例如在主 Tab Fragment 中嵌套 ViewPager 页面)时,绝对且只能使用它来进行事务提交。
为什么要分 Parent 和 Child?
如果所有 Fragment(不管嵌套多深)都共用 Activity 的全局 Manager,那么回退栈管理和 ID 冲突将变成一场灾难。childFragmentManager 的存在,相当于给每个 Fragment 划分了一块完全自治的“独立领地”。当父 Fragment 被销毁或出栈时,框架只需通知该父节点的 childFragmentManager,即可有条不紊地将领地内的所有子 Fragment 全部销毁,这就是完美的树状级联生命周期管理。
四、 深度解密 FragmentTransaction 与回退栈
我们天天写 beginTransaction().add().commit(),这个链式调用的底层到底发生了什么?
3.1 事务的本质:命令模式
FragmentTransaction 其实应用了经典的命令模式(Command Pattern)。
每次调用 add()、replace()、hide(),底层并没有立刻去修改 UI,而是创建了一个个操作指令对象 Op(包含操作命令 cmd,比如是 ADD 还是 REMOVE,以及对应的 Fragment 实例)。
这些 Op 对象被添加到了一个叫 mOps 的 ArrayList 中。
而 BackStackRecord 则是 FragmentTransaction 的唯一实现类。顾名思义,它不仅是一次事务操作的容器,也是一条记录。如果这次操作开启了 addToBackStack(),那么这个完整的 BackStackRecord 就会被推入 FragmentManager 的回退栈(BackStack)中。
3.2 提交方式的差异化解析
在积攒完 Op 之后,我们通常有几种提交方式:
-
commit():异步提交。 源码中,它会将这次BackStackRecord打包成一个Runnable,通过主线程Handler.post()送到消息队列排队。当主线程空闲轮询到它时,才会调用execSingleAction真正执行所有的Op操作。 优点:平滑,不会阻塞当前代码的后续执行。 -
commitNow():同步提交。 绕过 Handler 排队,直接在当前调用栈立刻遍历执行所有的Op。 场景:当你添加完 Fragment 后,下一行代码立刻就需要调用该 Fragment 里的某个方法时,必须用这个,否则commit()的话 Fragment 还没跑完生命周期,会报空指针。 -
commitAllowingStateLoss():允许状态丢失的异步提交。 它的内部逻辑和commit()完全一样,唯一的区别是跳过了checkStateLoss()检查。关于这个检查,我们将在后面的“深坑”环节详细说明。
3.3 回退栈(Back Stack)是如何工作的?
当你按下手机的返回键时,Activity 并不会立刻关闭,而是先去询问 FragmentManager 的回退栈里有没有东西。
回退栈里存的不是 Fragment 实例!而是前面提交过的 BackStackRecord 事务记录。
当执行出栈操作时,FragmentManager 会拿出栈顶的 BackStackRecord,然后逆向遍历里面的所有 Op,并执行反操作(Reverse Operation)。
比如,当初的事务是 Op(ADD, FragmentA),那么出栈时就会执行 Op(REMOVE, FragmentA)。这是一种极度精妙的撤销机制设计。
五、 灵魂与肉体:Fragment 的生命周期剥离
这是 Fragment 最具争议,也是初学者最难跨越的心智障碍:Fragment 的实例周期(灵魂)与其关联的 View 视图周期(肉体)是彻底分离的。
- 灵魂周期:从
onCreate到onDestroy。只要FragmentManager的mActive列表里还有它,它就在内存中活着。 - 肉体周期:从
onCreateView到onDestroyView。这是真正在屏幕上渲染出像素的周期。
为什么必须剥离?
因为 Android 的内存是极其宝贵的资源!当执行 replace() 并将事务加入回退栈后:被替换走的旧 Fragment,其 UI 已经完全看不见了。为了节约内存,系统会调用该 Fragment 的 onDestroyView() 销毁其整个庞大的 View 树(肉体);但是,这个 Fragment 的数据、网络请求结果等状态必须保留,以便用户按返回键时能立刻恢复,因此系统不会调用它的 onDestroy(),它的实例(灵魂)依旧活在内存中。
当用户按返回键时,存活的实例会再次被调用 onCreateView(),重新创造一副“肉体”显示到屏幕上。
六、 真实世界中的 Fragment 深坑与自救指南
正是因为上述庞杂的状态机与灵肉分离机制,Fragment 诞生了无数个臭名昭著的异常。
坑一:由于自动恢复机制导致的重叠问题
当 App 被退到后台,由于系统内存吃紧,宿主 Activity 会被直接杀死。但在死之前,Activity 会通过 onSaveInstanceState 将内部所有被 FragmentManager 管理的 Fragment 的状态全部序列化打包。
当用户再切回来时,Activity 重建,系统会自动反序列化,把旧的 Fragment 实例原封不动地全部重新 new 出来并恢复。
如果你在 Activity 的 onCreate() 中没有判断 savedInstanceState,又执行了 beginTransaction().add():
系统帮你恢复了一套,你自己又加了一套,最终屏幕上会呈现多个相同的 Fragment 叠在一起的恐怖景象。
自救方案:始终在首次添加时检查 savedInstanceState == null。
坑二:IllegalStateException: Can not perform this action after onSaveInstanceState
这可能是崩溃排行榜第一的异常。在发起网络请求获取数据后,我们在回调里通过 commit() 切换 Fragment,应用直接崩溃报错。
原理追溯:
在 commit() 源码的入口,第一行代码就是 checkStateLoss()。
Android 要求 UI 状态必须是可恢复的。如果用户按下了 Home 键把 App 切到后台,系统已经拍了一张“快照”(onSaveInstanceState 被执行)。
在这个快照生成之后,如果你再执行 commit() 修改 Fragment 的状态,这个修改是无法被保存进之前拍好的快照中的。这意味着如果 Activity 此时被杀,重建后这段事务的修改就彻底丢失了。
为了防止这种偷偷摸摸的状态丢失,框架设计者选择用最强硬的手段:直接抛出 IllegalStateException 强行终止程序。
自救方案:
- 网络回调属于异步操作,随时可能在用户切后台时返回。尽量避免在无关紧要的异步回调中做涉及 Fragment 增减的操作。
- 明确知道这个 UI 变化不重要,即使系统杀死恢复后丢了也没关系(比如一个无关紧要的弹窗),那就使用
commitAllowingStateLoss()强制绕过该检查。
坑三:LiveData 的多次订阅与 OOM
我们在 Fragment 中经常这样写:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.data.observe(this) { ... }
}
当 Fragment 进入回退栈,其 View 销毁(肉体死亡,走 onDestroyView),但实例存活(灵魂未灭)。当它出栈再次显示时,又一次走了 onViewCreated,又一次执行了 observe(this)。
由于传入的 LifecycleOwner 是 this(灵魂),它一直没销毁,所以之前的订阅依旧有效。于是:一份数据源,被同一个 Fragment 实例绑定了两个观察者回调。触发时,回调执行两次,依此类推……不仅引发诡异的 UI 闪烁,还会造成严重的内存泄漏。
自救方案:
必须使用代表“肉体生命周期”的对象:viewLifecycleOwner。
viewModel.data.observe(viewLifecycleOwner) { ... }
当 View 销毁时,这个 LifecycleOwner 会进入 DESTROYED 状态,LiveData 内部会自动移除观察者,完美闭环。
坑四:ChildFragmentManager 与嵌套回退栈紊乱
在一个 Fragment 中嵌套多个子 Fragment(如 ViewPager 里的 Fragment)。
如果你在子 Fragment 中,使用了 getActivity().getSupportFragmentManager() 来进行事务管理,那么你操作的是最外层 Activity 的全局回退栈。此时如果有复杂的进退逻辑,极易导致状态树直接崩溃。
自救方案:
在 Fragment 内部管理子 Fragment 时,绝对且只能使用 childFragmentManager。它会在当前 Fragment 的内部维护一个独立的嵌套回退栈树,当父 Fragment 被销毁时,其挂载的所有子 Fragment 也会被有条不紊地正确销毁。
七、 现代演进:FragmentFactory 的破局
在很长一段时间里,Fragment 有一个霸道的硬性规定:必须并且只能保留一个无参构造函数。
正如前文“重叠问题”所述,当系统被杀后重建恢复时,FragmentManager 只能通过反射 Class.forName().newInstance() 来重新实例化它。如果你使用了有参构造,反射时就会找不到无参构造器而直接闪退。
这在依赖注入(DI,如 Dagger/Hilt)极其流行的今天,简直是一场灾难。我们无法通过构造器把 ViewModel 或 Repository 直接注入给 Fragment。
AndroidX 意识到了这个架构痛点,正式引入了 FragmentFactory。它将“实例化 Fragment”的最终解释权,交还给了开发者。
// 1. 自定义工厂,告诉系统如何通过反射无法完成的方式来创建我的 Fragment
class InjectionFragmentFactory(
private val myRepository: Repository
) : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
return when (className) {
ProfileFragment::class.java.name -> ProfileFragment(myRepository) // 使用带参构造!
else -> super.instantiate(classLoader, className)
}
}
}
// 2. 在宿主 Activity 中挂载工厂
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// 必须在 super.onCreate() 之前设置!
// 因为 super.onCreate() 内部就会触发那些被系统保存的 Fragment 的自动恢复流程。
supportFragmentManager.fragmentFactory = InjectionFragmentFactory(repo)
super.onCreate(savedInstanceState)
}
}
八、 总结
Fragment 之所以复杂,是因为它试图在一个本身就足够复杂的重量级组件(Activity)内部,硬塞入一套“微型操作系统”。
- 它的底层是 经纪人挂载机制 与 阶梯状态机模型。
- 它的事务管理是 带撤销栈的命令模式。
- 为了应对移动端的极度内存匮乏,它被设计成了 灵魂(实例)与肉体(View)分离 的双生体系。
当你再次面对诸如 commitAllowingStateLoss、viewLifecycleOwner 这样的长串 API 时,只要回想起它们背后的设计动机与原理,所有的死记硬背都会转化为顺理成章的肌肉记忆。