事务行为对比:add、replace、remove、show、hide

Fragment 事务的核心操作直接影响 Fragment 的生命周期回调以及视图的创建与销毁。理解 add、replace、remove、show、hide 五类 API 在事务行为、生命周期影响和视图复用上的本质差异,是编写高效、稳定导航逻辑的前提。下面先通过一张整体流程图概览,再逐项深入分析。

Fragment 事务行为对比:生命周期影响与视图复用 FragmentTransaction API 调用 add() 添加 replace() 替换 remove() 移除 show() 显示 hide() 隐藏 生命周期影响摘要 add: 目标 Fragment 走 onAttach→onCreate→onCreateView (视图创建) replace: 先移除旧的 (走完整销毁) 再添加新的 remove: 走完整销毁链 (onPause→onStop→onDestroyView→onDestroy→onDetach) 视图复用 show/hide 不触发生命周期重新走,仅改变 View 可见性 (GONE/VISIBLE) —— 视图完全复用,性能最优 其他操作均会导致视图创建或销毁 推荐适用场景 频繁切换 → show/hide 页面跳转/替换 → add+show / replace 后进先出 → add+remove (BackStack)

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 的标签页导航

  1. 初始化:通过 add 将每个标签页的 Fragment 添加到容器,并立即 hide 除了当前页面以外的 Fragment。
  2. 切换:hide(currentFragment) + show(targetFragment) 组合,无需 addToBackStack。
  3. 状态保持:所有 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。