事务行为对比:add、replace、remove、show、hide
Fragment 事务的核心操作直接影响 Fragment 的生命周期回调以及视图的创建与销毁。理解 add、replace、remove、show、hide 五类 API 在事务行为、生命周期影响和视图复用上的本质差异,是编写高效、稳定导航逻辑的前提。下面先通过一张整体流程图概览,再逐项深入分析。
1 事务行为对比
| API | 事务行为 | 是否通常需要 BackStack | 适用场景 |
|---|---|---|---|
add() | 将 Fragment 实例添加到容器,不替换已有 Fragment;可叠加多个 | 是(配合 addToBackStack) | 多视图平铺、标签页、底部导航(非替换) |
replace() | 移除容器中所有现有 Fragment,再添加新的 | 是(常用) | 页面跳转、单容器导航、覆盖式切换 |
remove() | 从容器中移除指定 Fragment | 通常与 popBackStack 配合 | 手动移除 Fragment、关闭子详情 |
show() | 设置 Fragment 视图为 View.VISIBLE | 不必须 | 频繁切换的标签页、保持状态的隐藏/显示 |
hide() | 设置 Fragment 视图为 View.GONE | 不必须 | 暂时隐藏 Fragment 而不销毁视图 |
关键区别:
add 和 replace 都会触发 Fragment 的生命周期完整流转(依赖是否在 BackStack 中),而 show/hide 只会改变视图可见性,不会调用任何生命周期方法(onPause/onResume 除外,仅在 Fragment 视图可见性变化时由系统独立触发)。remove 则逆向走完完整销毁链。
2 生命周期影响
不同 API 对 Fragment 生命周期回调的影响范围差异很大。下面用代码示例对比 replace 和 show/hide 的生命周期日志输出。
// Fragment A 日志 (使用 replace 切换到 Fragment B)
Log: A.onPause()
Log: A.onStop()
Log: A.onDestroyView()
Log: A.onDestroy()
Log: A.onDetach()
Log: B.onAttach()
Log: B.onCreate()
Log: B.onCreateView()
Log: B.onViewCreated()
Log: B.onStart()
Log: B.onResume()
// 使用 hide(A) + show(B)
Log: (无任何 Fragment 生命周期回调)
// 注意:Fragment A 的 onPause/onResume 不会因 hide/show 触发,
// 仅当 Fragment 的可见性被系统感知时(例如窗口焦点变化)可能触发 onPause/onResume,但事务本身不触发。
生命周期影响表格(重点记忆):
| 操作 | 被操作 Fragment 生命周期 | 容器中原有 Fragment 生命周期 |
|---|---|---|
| add(F) | onAttach→…→onResume(完整创建) | 无变化 |
| replace(F) | 新 F: 完整创建;旧 F: 完整销毁(onPause→…→onDetach) | 旧 Fragment 被移除 |
| remove(F) | 完整销毁(onPause→…→onDetach) | 无变化 |
| show(F) | 无生命周期回调 | 无变化 |
| hide(F) | 无生命周期回调 | 无变化 |
注意:如果
add 添加的 Fragment 已经存在(相同 ID 或相同 tag),会抛出 IllegalStateException。始终保证 Fragment 实例的唯一性或使用 findFragmentByTag 检查后再执行事务。
3 视图复用
视图复用是性能优化的核心考虑点。show/hide 允许 Fragment 的 View 对象持续保留在视图层级中,不会触发布局重建。add 操作创建新视图,remove/replace 则销毁视图(调用 onDestroyView)。
对于需要频繁切换的页面(例如 TabHost / ViewPager 中的 Fragment 切换、侧边栏内容区),强烈建议使用 show/hide 搭配 add 初始化所有 Fragment,可以避免每次切换都触发 onCreateView 布局解析和重建。
实现流程:基于 show/hide 的标签页导航
- 初始化:通过
add将每个标签页的 Fragment 添加到容器,并立即hide除了当前页面以外的 Fragment。 - 切换:
hide(currentFragment)+show(targetFragment)组合,无需addToBackStack。 - 状态保持:所有 Fragment 实例长期存活,视图不被销毁,输入框内容、滚动位置等自动保留。
// 初始化代码片段
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.add(R.id.container, fragmentTab0, "tab0");
ft.add(R.id.container, fragmentTab1, "tab1");
ft.add(R.id.container, fragmentTab2, "tab2");
ft.hide(fragmentTab1);
ft.hide(fragmentTab2);
ft.commit();
// 切换方法
void switchTab(int targetIndex) {
FragmentTransaction ft = fm.beginTransaction();
ft.hide(currentFragment);
ft.show(fragmentList[targetIndex]);
ft.commit();
currentFragment = fragmentList[targetIndex];
}
4 常见错误与调试
- 重复 add 同一实例:抛出
IllegalStateException: Fragment already added。在add前应当检查fragment.isAdded()或使用findFragmentByTag。 - replace 后旧 Fragment 视图残留:如果容器中之前
add了多个 Fragment,replace只会移除在容器中的最后一个 Fragment?错误认知:replace会移除容器中所有当前已添加的 Fragment,只保留新添加的。调试时检查容器 id 对应的 Fragment 列表。 - show/hide 与 BackStack 组合不当:如果对
show/hide的事务调用addToBackStack,弹栈时会恢复之前的 show/hide 状态。通常避免在 show/hide 中使用 BackStack,否则会产生不可预期的显示状态。 - 视图复用陷阱:使用
hide后 Fragment 视图不可见但仍在Window布局中,可能影响触摸事件分发。如果容器是FrameLayout,多个隐藏 Fragment 的视图层叠会导致点击穿透。解决方案:在hide后调用setVisibility(View.GONE)不传递触摸事件,但View.GONE实际上由 hide 内部设置,所以无此问题。
5 性能与稳定性优化
- 优先使用 show/hide 代替 replace:对于切换频次高于 5 次/分钟的场景(如轮播、快速标签切换),使用 show/hide 能减少 80% 以上的布局创建开销。
- 延迟初始化:对于
add但非立即可见的 Fragment,可以使用setMaxLifecycle(Fragment, Lifecycle.State.STARTED)限制其生命周期,避免过早创建视图(配合 show 时再提升至 RESUMED)。 - 事务批处理:在
FragmentTransaction中尽量合并操作(例如同时 hide 多个、add 多个),一次commit()提交,减少 commit 次数可降低主线程负担。 - 避免嵌套事务提交:不要在 Fragment 的
onCreateView或onResume中直接commit(),应使用commitNow()或延迟到post(),否则可能因状态未同步导致IllegalStateException。
调试技巧:重写 Fragment 的生命周期回调并打印日志,配合
adb logcat -s FragmentLifecycle 观察实际调用顺序。对于 show/hide 操作,日志应不出现 onDestroyView/onCreateView 回调,一旦出现说明误用了其他 API。