尧图精选

Android Fragment原理拆解:FragmentManager、生命周期与状态恢复

🕒 发布时间:2026/9/9 18:10:19 📁 来源:尧图网络
Fragment这个组件在Android开发里待过几年的应该都不陌生。但说实话很多人对它的理解停留在写一个Fragment类重写onCreateView然后在Activity里用FragmentManager替换一下这个层面。真正遇到问题的时候——比如App切后台被杀后Fragment重叠、状态丢失、事务提交崩溃——才发现自己根本不了解它的实现原理。这篇文章我打算把Fragment整个运行机制从头到尾拆一遍从FragmentManager怎么管理工作到事务提交后底层到底执行了什么再到生命周期和状态恢复的完整链路。不管你是刚接触Fragment的新手还是已经被Fragment各种诡异问题折磨过的老手都可以看看。搞懂原理之后再写代码很多坑其实是能提前避开的。1. 整体设计思路Fragment到底在解决什么问题Fragment的诞生背景得从平板的适配说起。早年Android手机和平板屏幕尺寸差太多同一个Activity在小屏上是单栏到大屏上就要双栏如果全部用Activity来做你得为每个屏幕尺寸重新组合页面工作量爆炸。Fragment本质上就是把这个页面拆成一个个可以独立存活、独立管理的界面单元然后在运行时根据需要组合它们。1.1 核心需求解析为什么Activity不够用Activity有一个天然的限制它必须注册在Manifest里它的生命周期由系统管控你自己很难按需创建和销毁。而且一个Activity对应的是一整个窗口想在某个时刻把窗口的一部分换掉Activity做不到。Fragment的设计目标就很明确了它是一个挂在Activity内部的子页面。它有自己的布局、自己的生命周期、自己的状态保存机制但它不能独立存在必须依附于Activity。这种设计让页面拆装变成了一件可编程的事——你想什么时候替换、添加、移除一个区域的内容直接通过FragmentManager操作就行。我自己的理解是Fragment本质上是把页面这个概念从Activity里解耦出来了。Activity变成容器和托管者Fragment变成真正的页面内容载体再加上FragmentManager作为中间调度层三者的分工非常清晰。1.2 方案选型背后的考量为什么是管理器回退栈Fragment这套设计里最有意思的不是Fragment本身而是它周边的那套机制。FragmentManager管着所有Fragment的添加、移除、显示隐藏事务FragmentTransaction定义了每一次操作的最小单元而且可以压入回退栈回退栈则让用户按返回键时能一步步撤销操作。这套模式跟操作系统的进程管理、编辑器的撤销重做好像是一回事——都是把变化封装成操作再把操作放进队列或栈里。这样设计的好处是系统可以在一个安全的时机统一执行这些操作避免多步操作之间状态互相干扰。我记得最开始我用Fragment时有个困惑为什么提交事务不能立即生效非要等一轮Looper循环后才执行后来才明白这是为了把多个事务合并批处理避免频繁创建和销毁View造成性能浪费。2. FragmentManager管理一切的中枢FragmentManager这个名字很直白它就是Fragment的管理者。但很多人没意识到它其实是一个非常重的角色——所有Fragment的创建、销毁、状态恢复、事务执行都是它在一手操办。2.1 FragmentManager的初始化与获取方式FragmentManager是在Activity启动过程中被创建的。具体来说Activity的attach方法里会创建FragmentController进而拿到FragmentManagerImpl实例。一个Activity对应一个FragmentManager实例它管理着这个Activity里挂载的所有Fragment。获取方式也简单// 在Activity中 FragmentManager fm getSupportFragmentManager(); // 在Fragment中 FragmentManager childFm getChildFragmentManager();这里有个容易搞混的点getSupportFragmentManager()拿到的是Activity级的管理器而getChildFragmentManager()拿到的是当前Fragment私有的子管理器。每个Fragment都可以有自己的嵌套Fragment这个嵌套层级可以一直往深处走。FragmentManager之间是树形结构的关系Activity的FragmentManager是根节点。很多人在嵌套Fragment时来回切换getFragmentManager()和getChildFragmentManager()用错了就看不清Fragment到底属于哪个层级。我的建议是永远记住一个准则谁要添加Fragment就用谁的FragmentManager。在Fragment A里添加Fragment B就必须用A的childFragmentManager否则B会跑到Activity层去层级就乱了。2.2 FragmentManager如何保存Fragment的状态FragmentManager的核心数据结构是mActive和mAdded两个列表。mActive存的是所有还活着的Fragment包括detach状态的mAdded存的是已添加到Activity上的Fragment。这两个列表看着简单其实是Fragment状态管理的基石。当系统内存紧张或配置变更比如旋转屏幕时Activity会销毁重建。FragmentManager会把所有mActive里的Fragment序列化成FragmentState数组存进Bundle里。重建时再从Bundle里恢复FragmentState依据这些状态重建Fragment实例。这个先序列化、再反序列化的过程就是Fragment状态恢复的本质。很多情况下Fragment状态异常往往就出在这个序列化和反序列化的边界条件上——有些Field没被正确保存或者某些状态位比如mUserVisibleHint没有正确恢复结果Fragment从一个不合理的状态继续跑各种诡异问题就出来了。2.3 延迟执行机制事务为什么不会立即生效FragmentManager里有一个很关键的设计——事务不是调用即执行的。你调用commit()时FragmentManager只是把这次事务放进了mPendingActions队列等待下一轮消息循环时再统一执行。// commit的内部逻辑简化版 public int commitInternal(boolean allowStateLoss) { // 把事务标记加入到pending队列 mManager.enqueueAction(new OpGenerator() { Override public boolean generateOps(...) { // 真正执行是在这里 mManager.execSingleAction(this, ...); return false; } }, allowStateLoss); return mIndex; }为什么这么设计我敢打赌很多人想过这个问题。核心原因是事务往往会连续提交多个比如你可能在一个方法里连续调用了三次commit如果每次都立即去执行Fragment操作就需要多次遍历Fragment列表去addView、removeView性能会很差。延迟到下一轮消息循环把多个pending的事务合并成一次执行效率就高很多。但这也带来了著名的坑commit()之后你马上调用findFragmentById()可能找不到新加的Fragment因为它还没真正执行。如果你确实需要立即生效可以用executePendingTransactions()这个方法会强制把队列里的操作立即执行完。不过这个方法确实要慎用它容易掩盖逻辑问题而且如果调用时机不对反而会导致fragment状态错乱。3. 生命周期机制状态机与回调的本质Fragment的生命周期是它最复杂、也是面试最爱问的一块。网上很多文章都是直接列出一堆回调方法然后画个流程图。我这儿想换个角度讲清楚这些回调背后到底是个什么样的状态机。3.1 从状态机视角看生命周期Fragment内部其实维护着一个状态值——mState。这个状态值不是回调方法名而是一个整数从小到大依次是INITIALIZING0初始状态CREATED1Fragment已被创建但还没创建ViewACTIVITY_CREATED2Activity已完成创建Fragment的View已创建STARTED3已启动RESUMED4已恢复处于可交互状态每次状态变化时FragmentManager都会调用Fragment的performXxx系列方法比如performCreateView、performStart、performResume在这些方法内部才会回调到我们重写的onCreateView、onStart、onResume这些生命周期方法。这就是为什么FragmentManager可以从任意状态迁移到任意状态而不出错的根本原因——它只看目标状态然后判断当前状态与目标状态之间差了多少步依次执行中间的回调。比如从RESUMED直接降到CREATED它会先走onPause再走onStop再把View销毁掉最后停在CREATED状态。3.2 生命周期与Activity的联动关系Fragment的生命周期是挂在Activity生命周期上的。当Activity执行onResume时FragmentManager会遍历所有已添加的Fragment依次调它们的onResume当Activity执行onPause时Fragment也依次执行onPause。Activity是主时钟FragmentManager负责把时钟信号分发给所有Fragment。这是一个典型的观察者或事件分发模式。你可以把Activity理解成一个时钟源Fragment是挂钟FragmentManager是传送轴——时针转了一圈所有挂钟都得跟着转。但这里有个细节很多人会忽略Fragment执行生命周期回调的顺序和它被添加的顺序有关而且onPause/onStop这些回调的分发顺序在某些场景下和onResume/onStart是相反的。具体来说Activity的状态变化会让Fragment按照mAdded列表遍历但正处于添加/移除动画中的Fragment会有特殊处理这也是为什么动画期间状态会偶尔错乱的原因。3.3 onSaveInstanceState调用时机与注意点Fragment的onSaveInstanceState调用时机比Activity的更难把握。它不是在每次状态变化时都调用而是在Activity被销毁、但Fragment状态需要被保留时才会调用。比如用户按Home键——通常不调用因为Activity没被销毁但如果系统因为内存原因杀掉后台Activity就会调用。这里有一个非常经典的坑在onSaveInstanceState之后去commit一个事务会抛出IllegalStateException提示你Can not perform this action after onSaveInstanceState。这个异常我记得几乎是每个做过Fragment开发的人都会遇到的。原因很简单状态已经保存了这时候再改变Fragment的状态等Activity重建时保存的状态和实际状态就对不上了。解决方案有两个一是用commitAllowingStateLoss()告诉系统我知道会丢状态但没关系你让我提交二是把提交动作放到onResume或onPostResume里等状态可变更了再操作。我个人建议不到万不得已别用commitAllowingStateLoss。它虽然能避免崩溃但丢状态这事不解决后面可能出现更隐蔽的问题。更好的做法是想清楚为什么这个操作会发生在状态保存之后从根源上避免。4. 事务机制FragmentTransaction到底做了什么FragmentTransaction就是我们常用的beginTransaction()返回的接口实现它在底层对应的是BackStackRecord类。这个类的名字很有意思——它既是事务记录又要承担回退栈的责任。4.1 add、replace、remove背后的实现差异先看三个最常见的操作add()把Fragment添加到Activity相当于往容器里塞一个新的View。remove()销毁Fragment的View并移除它。replace()本质上是remove()容器里现存的所有Fragment add()新Fragment。它是一个组合操作而不是一个原子操作。replace的内部实现就是把当前容器内的所有Fragment标记为要移除然后再把新的Fragment添加进去。所以如果有多个Fragment同时显示在同一个容器里比如用add叠加的replace会一次性把它们全干掉。这三个操作的选择直接关系到性能和状态保存策略。add适合那种需要保留原状态的场景比如底部Tab的页面切换——用addhide/show切页Fragment的onDestroyView不会被调用它的状态能完整保留。而replace一定会走onDestroyView相当于把页面销毁重建。这两种策略我两种都用过。做内容型App的时候用show/hide切Tab能明显感觉到切换更流畅因为不重建View但如果Tab页本身不多replace的写法更简单直接。具体情况具体分析没有绝对的好坏。4.2 hide/show与attach/detach性能与状态的抉择hide和show只是设置View的可见性不会销毁View所以Fragment的状态包括onViewCreated里做的初始化都会保留。代价是如果Fragment太多所有View都在内存里占着内存开销会比较大。attach和detach就狠一些。detach会把Fragment的View完整销毁但Fragment实例还在状态也还在。所以下次attach时会重新执行onCreateView到onViewCreated这一整套流程但onCreate和之前保存的实例状态会保留。这里有一个取舍原则内存敏感、页面多用attach/detach牺牲一点切换速度换内存。切换频繁、页面少用hide/show牺牲一点内存换流畅度。据我观察Android官方推荐的主次架构组件BottomNavigationFragment的主流方案底层也是通过show/hide或类似机制来避免Fragment重建的。4.3 回退栈的实现原理把事务加入回退栈很简单addToBackStack(null)一行代码。但底层实现很有意思BackStackRecord实现了BackStackEntry接口可以在它的popFromBackStack()方法里反向执行之前提交的操作。比如你提交了一个add Fragment A的事务并把事务入栈。按返回键时栈顶事务会被取出然后执行revert()把A从add状态反转成remove状态。这就是回退的底层逻辑——不是去撤销而是把操作反向执行一遍。如果事务里同时有add(A)、remove(B)、show(C)多个操作反向执行时就会变成remove(A)、add(B)、hide(C)。这种方式简单高效但要求每个操作都得有完整的反向语义。如果哪个操作的反向逻辑有问题pop时就会出现意料外的效果比如本来想保留的页面被销毁了。这里有个实用小技巧如果你在addToBackStack之前修改了某些外部数据比如ViewModel里的字段按返回键pop事务时页面会回退但数据不会回滚。所以如果需要事务级数据回滚得自己在pop回调里处理不要把回退栈想得太万能。5. 状态保存与恢复最容易被忽略却又最容易出问题的环节Fragment的重叠问题和状态丢失问题根子都在状态保存与恢复这一环。这部分如果不彻底搞懂Fragment的bug会像影子一样跟随着你。5.1 配置变更时Fragment的完整重生流程当屏幕旋转发生Activity会销毁重建。在销毁之前系统会调用onSaveInstanceStateFragmentManager会把每个Fragment的状态打包进一个ArrayList存进Activity的Bundle里。重建时Activity从Bundle里读回这个ArrayList每条数据对应一个FragmentState。FragmentState里保存了Fragment的Class类型、arguments、以及它自己的状态Bundle。FragmentManager遍历这些FragmentState用反射或工厂方法创建新的Fragment实例然后把之前的state恢复进去。这个过程看起来没什么问题但有几个细节经常被忽略Fragment实例不是恢复时的那个原实例了它是一个全新的对象所有非状态字段比如你在类里直接声明的普通成员变量都会丢失。arguments通过setArguments传入的参数会被保留因为它在FragmentState里。如果恢复时找不到合适的构造函数会抛异常——比如你没有提供无参构造方法或者在构造函数里做了需要Context的操作。5.2 非配置变更的进程被杀死与恢复配置变更只是Activity重建的一种场景更隐蔽的是进程被杀后恢复。比如用户把App切到后台系统内存不够把进程杀了但任务栈还保留着。用户再切回来时系统会重建整个Activity并且从保存的状态里恢复Fragment。这种场景下的恢复路径稍微有一点不同。系统会先恢复Activity的拥有者状态比如View树的状态然后FragmentManager恢复Fragment。但如果Fragment是动态添加的恢复的时候有几个关键约束有Tag或ID的Fragment可以被FragmentManager恢复。完全没有Tag、也没有ID的Fragment恢复后可能无法重新找到它。通过adapter驱动的Fragment比如FragmentPagerAdapter恢复依赖Adapter再次创建所以要确保Adapter的getItem逻辑在恢复时能返回与原来一致的Fragment。我自己遇到的典型情况是在onCreate里判断savedInstanceState null才去添加Fragment但进程被杀后恢复时savedInstanceState可能不为空结果就没添加但FragmentManager又恢复了原来那个Fragment两者叠加页面就会出现重复。5.3 不可序列化字段的处理onSaveInstanceState里只能放可以进Bundle的类型基本类型、String、Parcelable等。如果你有个自定义的对象成员它不是Parcelable想进Bundle就比较麻烦了。有个思路是把这些对象放进ViewModel里。ViewModel在配置变更旋转屏幕时不会销毁所以不用序列化。但在进程被杀场景下ViewModel也不行——这时你还是得想办法把数据放到Bundle里或者用持久化方案比如DataStore/MMKV等绕过。我见过很多项目干脆把数据源全放在Repository层或远端Fragment只存ID或索引完全靠重新加载来恢复。这种无状态设计虽然初期麻烦但是规避了一堆状态恢复的坑长期维护起来反而轻松。6. 常见问题与排查技巧从IllegalStateException到重叠页面这块我打算直接把我在实际项目里踩过、填过的坑整理成速查表方便你对照排查。6.1 典型的Fragment问题对照表问题表现根本原因解决方案屏幕旋转后页面重叠Fragment被恢复后又在onCreate里重新add了一次添加前判断savedInstanceState nullCan not perform this action after onSaveInstanceState在状态保存后commit事务改用commitAllowingStateLoss或换时机提交Fragment not attached to a context在异步回调里使用Fragment的Context使用getContext()前先判断isAdded()Fragment重叠但onCreate只执行了一次使用了add而不是replace导致Fragment叠加显示检查是否重复添加了不同的FragmentgetFragmentManager返回null在Fragment构造或非常早的阶段调用移到onViewCreated或更晚FragmentTransaction冲突同一批次提交了多个涉及同一Fragment的互斥操作合并成一次事务或避免连续提交6.2 Fragment重叠问题最常见的生产事故Fragment重叠问题是我在各种App里见到最多的一种崩溃级Bug。情景基本一致MainActivity里有首页A用户从首页进入详情页B然后又切换到后台进程被杀用户从任务栈恢复结果回到详情页B时首页A和B重叠了。原因链条是这样的系统恢复了FragmentManager里保存的A但Activity的onCreate里又根据逻辑比如savedInstanceState null时才添加添加了B。两种情况叠加A和B同时存在就重叠了。这个问题最稳的解法是所有动态添加Fragment的地方都先判断savedInstanceState是否为空。另外如果使用Navigation组件它底层也是Fragment这种问题会大幅减少因为Navigation已经把状态恢复的细节封装好了。还有一个我常用的排查手法打开Android Studio的Layout Inspector看View层级如果你看到FragmentContainerView里有多个Fragment的View叠加那就是重叠了。6.3 状态丢失与异步操作的回调另一个频繁出现的问题是在异步任务比如网络请求完成后回调里操作Fragment的UI但此时Fragment已经detach或者销毁了。最典型的报错是IllegalStateException: Fragment already added或者Fragment not attached to a context。解决方案很统一在回调里做操作前必须检查Fragment状态。// 在Fragment内部做的安全回调 if (!isAdded()) { return; // 已经不在Activity上了直接放弃 } FragmentManager fm getChildFragmentManager(); if (fm.isStateSaved()) { return; // 状态已经被保存不能再提交事务 }这两个检查是保命的。isAdded()判断Fragment是否还在Activity上isStateSaved()判断当前是否处于不能提交事务的状态。不信你试试把这两行加上之后很多异步回调导致的崩溃直接归零。6.4 遇到Fragment问题时的系统化排查思路最后分享一套排查Fragment问题的思路。我第一次被Fragment的bug折磨时毫无头绪后来总结出了一个比较固定的排查流程现在遇到新问题也基本套用这个流程第一步看崩溃日志里有没有关键信息比如BackStackRecord、FragmentManagerImpl这些类名如果有基本可以断定是Fragment事务或状态恢复的问题。第二步判断问题出现的场景——是冷启动恢复还是配置变更还是直接在正常使用中崩溃不同场景对应的根因方向不同。第三步用FragmentManager的打印事务功能在Activity的onCreate或onResume里加上fm.enableDebugLogging(true); // 或者 Log.d(TAG, fm.toString());这样就能看到每个Fragment的状态、标签、ID方便确认是否重复添加了Fragment。第四步如果还定位不了就在onCreate、onSaveInstanceState、onResume里分别打日志看Fragment的添加和重建时机有没有重叠。这套流程帮我在生产环境定位了很多疑难杂症尤其是重叠和恢复问题基本十拿九稳。7. 性能优化建议Fragment不是越多越好Fragment用多了以后你会感觉到一个问题它真的有点重。每一个Fragment的创建、View的创建、生命周期调度都有不小的开销。7.1 单Activity架构的取舍与误区现在比较流行的单Activity架构确实把Fragment推到了一个新的高度。所有页面全用Fragment承载Manifest里只留一个Activity。这种架构在状态恢复上更统一转场动画也更好控制。但务必要清楚它带来的是统一性不是免费的。如果一个App里有三四十个Fragment每个Fragment都有自己的ViewModel和ViewActivity重建时这几十个Fragment会全部恢复onCreateView、onViewCreated会全部跑一遍。如果没有做懒加载——注意是真正的懒加载不是先建Fragment后加载数据这种——冷启动的卡顿是免不了的。我见过一些团队为了迁就单Activity架构把原本不需要Fragment的简单页面硬做成Fragment结果维护成本翻倍。有些事情不能本末倒置——架构是为业务服务的不是业务为架构服务。7.2 懒加载的正确姿势与View销毁时机懒加载是Fragment性能优化里最核心的一个概念。它的目标就是用户看不到的页面不做数据加载和UI渲染。Fragment的setUserVisibleHint旧版或onResume的组合新版是实现懒加载的基础但要写对很不容易。真正稳定的懒加载方案是配合ViewPager2的setOffscreenPageLimit来控制预加载页面的数量不要贪多预加载1~2个页面就够用了。还有一个容易踩的点onDestroyView之后Fragment的View会被销毁但Fragment实例还在。如果你在onCreateView里做了很多重量级的初始化比如绑定了大量监听器这些监听器如果不释放就会有内存泄漏。我的习惯是跟View强绑定的对象全部在onDestroyView里置空跟业务逻辑强相关的数据放ViewModel里。7.3 使用ViewBinding时的额外注意点现在很多人用ViewBinding替代findViewById。ViewBinding的用法是在onCreateView里inflate的private FragmentHomeBinding binding; Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding FragmentHomeBinding.inflate(inflater, container, false); return binding.getRoot(); } Override public void onDestroyView() { super.onDestroyView(); binding null; // 这句非常关键 }onDestroyView里把binding置空是我在任何项目里都会加的代码。因为ViewBinding持有着View的引用如果Fragment的View被销毁了但binding还在它hold住的View和关联的Context就全泄漏了。8. Fragment与Jetpack Navigation官方方案的实现思路我记得Navigation组件刚出来时我的第一反应是这不是重复造轮子吗。但真正用了以后才发现它确实把Fragment的很多细节问题收敛起来了。Navigation组件底层仍然是FragmentTransaction但它帮你处理了三件事一是状态的自动保存与恢复二是转场动画的标准化管理三是导航图的统一管理。你不需要手写beginTransaction()和addToBackStack只描述从A页面跳到B页面框架全包。但Navigation也不是万能的它也引入了一些新的学习成本——导航图、DeepLink、SafeArgs等等。而且如果项目之前已经有了一套Fragment管理逻辑强行迁移到Navigation中间的状态兼容问题可能比原来更麻烦。关于Navigation和原生Fragment如何取舍我的看法是新项目如果页面关系是明确树状的一个页面跳到另一个页面可以直接用Navigation如果页面关系特别灵活、有很多动态的容器组合那不一定要用Navigation原生Fragment手动管理反而更灵活。写在最后的几句体会说实话Fragment这套设计从2011年引入到现在十多年过去了它的核心原理并没有变过。内存里的那一套状态管理、事务执行、生命周期调度的逻辑无论外面包了多少层新架构底层内核都是稳定的。我自己在实际项目中操作下来最大的体会是Fragment真正难的从来不是怎么用而是怎么理解它为什么这么工作。当你把FragmentManager当做一个事务队列来理解把Fragment生命周期当做一个状态机迁移来理解把状态恢复当做一个序列化与反序列化的过程来理解很多之前觉得玄学的bug其实都能推出来。如果你的App里Fragment用得比较多我还是建议花点时间把源码的关键路径读一遍——不需要逐行读重点看FragmentManagerImpl的moveToState、BackStackRecord的executeOps、FragmentStateManager的restoreState这几个方法就够用了。读完一遍你对Fragment的掌控感会有质的提升。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →