十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Fragment实现原理深度拆解:从生命周期到状态管理

Fragment实现原理深度拆解:从生命周期到状态管理 1. 为什么必须搞懂Fragment不是开发者的偏执而是安身立命的本事还记得我刚做Android那会儿团队里流传着一句话凡是搞不定的事就甩给Fragment背锅。页面切换异常Fragment的锅。内存泄漏查不出来Fragment的锅。App莫名崩溃还是Fragment的锅。直到后来我扎扎实实用了几周时间把FragmentManager源码一行一行啃下来才明白一个特别朴素的道理Fragment本身不背这些锅是我们压根不知道它内部到底是怎么运转的。Fragment这东西从Android 3.0API 11引入至今已经陪伴开发者十几年了。它的定位是什么官方的定义是一种可复用的UI组件可以嵌入到Activity中。但如果你只把它理解成一个带生命周期的View那后面所有的困惑都会接踵而至。真正的核心是Fragment是一个由FragmentManager统一调度、具备完整状态管理能力的独立单元。它有三层身份——第一层是视图提供者它通过onCreateView返回一棵View树第二层是状态持有者它有自己的状态序列和保存恢复机制第三层是生命周期主体它可以随着宿主的生命周期同步迁移也可以独立于宿主进行状态管理。搞懂这三层你才算真正摸到了Fragment实现原理的门槛。而这三层分别对应了三个核心问题Fragment的View是怎么长出来的FragmentManager是怎么管理它的状态是怎么被保存和找回的本文就围绕这三条主线把Fragment的实现原理彻底拆开讲清楚。无论你是刚入门的小白还是已经用Fragment写了好几年业务代码的老手这篇文章都值得你静下心来读一遍——你会发现很多以前照着网上的方案试出来的结果背后是有明确逻辑支撑的。2. 一个不是View的ViewFragment的视图体系如何构建2.1 onCreateView的返回值去向哪里先看一个最基础的问题每次重写Fragment时我们都会写类似这样的代码override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return inflater.inflate(R.layout.fragment_demo, container, false) }你有没有认真想过这个返回的View最终被加到了哪里答案在FragmentManager的moveToState方法里。当Fragment的状态迁移到ACTIVITY_CREATED时FragmentManager会调用Fragment的performCreateView拿到返回的View然后执行这样一个逻辑如果f.mContainerId不为0就找到Activity中对应的容器ViewGroup也就是你在布局里写的FrameLayout之类的id把非空的View添加进去。这里有个非常关键的细节addView之前FragmentManager会先做一次清理。如果某个fragment已经被detach它的View会被从容器中移除如果同一个容器中有多个fragment在切来切去FragmentManager会极其小心地处理旧View是否还在容器里的问题。你可能会遇到的一个经典bug是Fragment重叠本质就是旧Fragment的View没有被移除干净新Fragment的View又加进来了两个View叠在一起。2.2 为什么是三级包装Fragment → View → ViewGroup另一个值得注意的点是Fragment的View并不是直接挂在Activity的根布局上的而是一个三级结构Activity的ContentView里有一个容器比如id为fragment_container的FrameLayoutFragment的root View也就是onCreateView返回的View被add进这个容器而Fragment内部通过onViewCreated回调让你拿到这个root View的引用。这个三级包装的设计不是拍脑袋定的它解决了一个非常实际的问题View的复用和替换。如果你的FragmentA和FragmentB都要显示在同一个容器里FragmentManager只需要把A的root View从容器中移除再把B的root View添加进来就完成了切换。而Fragment对象本身可以继续存活它的状态比如滚动位置、输入框内容都存在Fragment实例里下次重新创建View时可以通过onViewStateRestored回调恢复回来。2.3 createViewFromLayoutInflater的特殊处理源码里还有一个隐藏很深的逻辑Fragment创建的View如果是通过LayoutInflater.inflate生成的FragmentManager在addView时会对根View做一次特殊标记。具体来说如果这个root View还不是一个ViewGroup系统会用一个名为generateLayoutParams的内部方法将root包一层FrameLayout来保证它可以作为容器子View正确测量和布局。这个细节大多数人一辈子都不会碰到但一旦你在自定义Fragment里返回了一个特别简陋的View比如直接new出来的ImageView就有概率触发奇怪的布局问题。我从自己的踩坑经验来说一律建议onCreateView里返回一个常规的XML布局尽量避免直接构造View可以省掉很多莫名其妙的边界问题。2.4 Fragment、View、Container三者的生命周期时序再补一张大家可能没认真梳理过的时序逻辑事件时序说明onAttach1Fragment与Activity绑定context可用onCreate2Fragment实例初始化此时还没有ViewonCreateView3创建View树此时容器可能已存在onViewCreated4View树已经创建完成可以findViewByIdonViewStateRestored5如果savedInstanceState不为空恢复View状态onStart6View已经可见但不可交互onResume7View可交互这个时序表解决了两个高频痛点第一个是为什么不能在onCreate里操作View——因为此刻View根本不存在第二个是为什么在onActivityCreated里拿宿主Activity没问题——因为此时Activity已经完成了onCreate它的Window和DecorView都在。如果你在onCreateView里尝试获取Activity的某个控件你也不是拿不到但系统并不保证此时Activity已经处于完全可用的状态正确的做法是放到onViewCreated里操作。3. 生命周期状态机Fragment每次状态迁移都有据可循3.1 六种状态的定义很多开发者以为Fragment生命周期只有onCreate、onStart、onResume这类回调方法但真正的实现原理是这些回调之所以会被调用是因为Fragment内部有一套状态机在驱动。Fragment内部有一个int类型字段mState它的取值范围是状态值常量对应阶段0INITIALIZING初始态Fragment刚创建1CREATED已创建但View尚未创建2ACTIVITY_CREATEDActivity已创建View也创建完成3STARTED已启动可见4RESUMED已恢复可交互5SAVED特殊状态用于保存实例注意没有单独为DESTROYED定义一个数值——当Fragment被销毁时mState被置为0然后走完整的destroy流程。这六个状态就是整个生命周期驱动的核心所有回调方法都是状态迁移的副产品。3.2 状态迁移的驱动方式从外层向内层传导状态机的驱动逻辑非常像一张多米诺骨牌Activity的状态发生变化会触发FragmentActivity严格来说叫FragmentActivity而非Activity但三方的AppCompatActivity内部也是同样机制的onStart/onStop/onResume然后FragmentActivity把自己的状态同步到FragmentManagerFragmentManager再递归地对所有Fragment执行moveToState。关键来了这个传导是有优先级和顺序的。Activity的状态一定先于Fragment的状态发生变化而且Fragment的迁移顺序严格遵循它在mAdded列表中的顺序。也就是说后添加的Fragment会晚一点进入RESUMED状态。这一点在实际开发中偶尔会造成一种微妙的时序问题——比如你在A Fragment的onResume里试图跟B Fragment通信大概率B还没进入RESUMED。3.3 moveToState的五种迁移方向moveToState方法接收一个目标state根据当前mState和目标state的大小关系决定是调高提升状态还是调低降低状态提升状态从INITIALIZING升到CREATED走onAttach和onCreate从CREATED升到ACTIVITY_CREATED走onCreateView和onViewCreated从ACTIVITY_CREATED升到STARTED走onStart从STARTED升到RESUMED走onResume。降低状态反过来执行onPause、onStop、onDestroyView、onDestroy、onDetach按倒序执行。每降低一个状态对应的View就会被销毁但Fragment实例不一定销毁。只有当状态降到INITIALIZING并且被标记为移除时才会执行onDestroy和onDetach此时Fragment才真正死亡。3.4 为什么onDestroyView和onDestroy是两回事这是新手最容易混淆的地方。onDestroyView只是销毁View树Fragment实例还活着状态还在还可以再次创建View。什么时候会走onDestroyView但不走onDestroy最常见的就是Fragment被replace时旧Fragment先进入INITIALIZING态调用onDestroyView然后因为被标记为移除继续调onDestroy。但如果是add hide/show的隐藏模式Fragment就只会在hide时被降低到CREATED状态View被销毁但实例一直在下次show时重新创建View。我自己的经验教训凡是需要在View销毁时清理的资源比如图片加载回调、网络请求、监听器注册一定要放在onDestroyView里而不是onDestroy里。否则View已经没了但资源还挂着极易造成内存泄漏。3.5 状态迁移的防御机制if (f.mState newState)源码里有个很关键的条件if (f.mState newState)决定是上调还是下调。但因为FragmentManager支持同时处理多个Fragment的迁移所以并不会有某个Fragment在迁移过程中被其他Fragment插队的问题——所有迁移都在同一轮循环中处理不存在并发。这也是为什么FragmentManager本身不是线程安全的所有操作必须发生在主线程。4. FragmentManager总调度者的内部账本与调度逻辑4.1 三张关键列表mActive、mAdded、mBackStackFragmentManager的整个工作逻辑概括下来就是维护三张表mActive当前所有还活着的Fragment。包括那些在屏幕上显示的、被隐藏的、被detach的只要实例没有被销毁就都在mActive里。用SparseArray实现key是Fragment的mWho一个唯一字符串。mAdded当前真正添加到Activity的Fragment列表。也就是说这个列表里的Fragment是可见或可被看到的它们的View可能显示也可能不显示取决于是否被hide。mBackStack回退栈。保存的是一个个事务记录BackStackRecord每个BackStackRecord里又包含若干操作Op对象。这三张表共同构成了FragmentManager的调度核心。每次你调用fragmentManager.beginTransaction().add(xxx).commit()其实就是在干三件事创建一个BackStackRecord往里面塞Op操作然后把BackStackRecord提交到FragmentManager的待处理队列。4.2 事务的原子性Op对象与BackStackRecord一个事务Transaction本质上是一串操作的集合。你可以在一个事务里同时add、remove、hide、show几个Fragment系统会把这些操作按顺序记录为Op对象链表然后一次性执行。这个设计保证了操作的原子性要么全部生效要么全部不生效不会出现提交了一半的状态。到了执行阶段BackStackRecord的executeOps方法会遍历整个Op链表对每个Op执行对应的操作。比如OP_ADD会调用FragmentManager.addFragmentOP_REMOVE会调用removeFragmentOP_HIDE就调用hideFragment。这些方法最终都会修改Fragment的mState和FragmentManager的mAdded列表。4.3 commit不是立即执行为什么要post到主线程这是很多人一直没搞明白的事。commit()方法内部做的事情是先把BackStackRecord放进一个pendingActions列表然后通过mHost.getHandler().post(() - executePendingTransactions())把真正的执行逻辑post到主线程的消息队列里。这意味着你调用commit()之后Fragment的迁移没有立刻发生。那为什么要这么设计原因很直接保证状态一致性。如果你在一个生命周期回调里连续调用了几个commit这些操作会按顺序排队最终一起执行。如果立即执行就可能导致生命周期回调嵌套调用造成状态错乱。官方建议在onSaveInstanceState之后不要commit就是因为此时系统已经进入状态保存流程再立刻操作Fragment会破坏这个一致性。如果你真的需要立即执行有两条路第一用commitNow()它不走post直接在调用线程上同步执行第二用executePendingTransactions()把之前post的任务提前执行完。但这两条路都要小心commitNow不能和回退栈一起用因为它执行的是同步操作无法在执行过程中保存回退栈状态。4.4 回退栈的弹栈逻辑当你通过addToBackStack(null)把一个事务加入回退栈后按返回键时FragmentManager会调用popBackStack。这个过程的原理是从mBackStack里取出栈顶的BackStackRecord反向执行它的所有Op操作。比如原来的操作是add了FragmentA反向执行时就是remove FragmentA原来的操作是hide了FragmentB反向执行时就是show FragmentB。但这里面有个细节特别容易翻车如果你仅仅hide了一个Fragment而没有把它从mAdded里拿掉回退栈只能恢复它的显示状态不能恢复它的层级顺序——即它在mAdded列表里的位置。这在一些复杂的嵌套场景下会引发界面层级错乱。4.5 兼容包FragmentManager与系统FragmentManager的差异国内很多项目还在用androidx.fragment.app.FragmentManager即Jetpack的Fragment实现它和系统自带的android.app.FragmentManagerAPI 28之前在核心原理上基本一致但细节差异不小。系统版本不允许Fragment嵌套Fragment因为子Fragment管理器不是一个完整的实现API 26之前而androidx版本完全支持嵌套。androidx版本还引入了FragmentFactory、Behavior等新特性创建Fragment的方式变灵活了但这些高级功能都建立在相同的基础调度逻辑之上。我从实现原理的角度强烈建议新项目一律使用androidx的Fragment别再用已经废弃的android.app.Fragment了。旧项目如果历史包袱太重至少新写的代码逐步迁移到androidx否则后面想升级适配会痛不欲生。5. 状态保存与恢复一套被低估的精密机制5.1 保存时机不是在onPause而是在onSaveInstanceState很多开发者以为Fragment的状态保存发生在onPause前后其实并不是。真正的状态保存发生于Activity的onSaveInstanceState回调里。Activity在即将进入后台或者配置变更前会调用onSaveInstanceState这个调用会一路传递到FragmentManager的saveAllState方法。saveAllState的核心工作是遍历mActive列表为每个Fragment生成一个FragmentState对象。FragmentState里保存了Fragment的类名、mWho、mArguments、mSavedState等关键信息其中mSavedState里又保存了Fragment view层级的状态也就是你在onSaveInstanceState里塞进Bundle的那些东西。然后所有这些FragmentState被序列化到一个Parcelable里最终由Activity的savedInstanceState保存下来。5.2 恢复流程工厂方法重建实例当Activity因为配置变更被销毁重建时新的Activity会拿到一个savedInstanceState BundleFragmentActivity从中取出FragmentManagerState然后FragmentManager开始恢复。恢复的关键是Fragment实例不是通过构造方法强行new出来的而是通过FragmentFactory的instantiate方法反射创建。这也是为什么Fragment必须有一个无参构造方法——FragmentFactory依赖无参构造来实例化。如果你在Fragment里定义了带参数的构造方法而没有同时保留无参构造方法系统恢复时就会抛异常。创建好Fragment实例之后FragmentManager会调用restoreViewState方法把FragmentState里保存的mSavedState、mViewState等数据传递给Fragment。如果你在Fragment的onCreate里没有做特殊处理这些状态会在onViewCreated之后通过onViewStateRestored回调给你让你有机会恢复View的细节状态比如RecyclerView的滚动位置。5.3 你已经用到了却不知道的设计retainInstance在androidx的Fragment中有一个setRetainInstance方法设置为true后Fragment实例可以在Activity重建时保留不经过销毁重建。它的实现方式很巧妙FragmentManager在saveAllState时如果发现Fragment的mRetainInstance为true就不会把它写进savedInstanceState里而是在Activity的onRetainNonConfigurationInstance阶段把Fragment转移到新的FragmentManager里。这套机制对旋转屏幕特别有用因为你可以在Fragment里保存一些重量级数据比如网络请求的结果避免Activity重建后重新加载。但它有一个坑retainInstance的Fragment不能持有Activity的引用否则你反而制造了一个内存泄漏。一般来说这个特性在ViewModel出现后已经很少用了但理解它的原理能帮你更好地理解FragmentManager的恢复机制。5.4 onSaveInstanceState与commit的冲突这是Android开发中的一个经典大坑在onSaveInstanceState之后调用commit()会抛出IllegalStateExceptionCan not perform this action after onSaveInstanceState。原因是状态已经保存完了你再提交一个事务恢复时这个事务会被丢掉那么你本次操作就白做了逻辑上前后不一致。从原理层面理解这个限制就很自然了FragmentManager的状态保存在提交事务的那一刻后续的事务如果有依赖关系重建后无法重放。解决办法有两个一是把commit放到onResume之后再做此时已经过了保存点二是用commitAllowingStateLoss明确表示丢弃这次提交也无所谓。但要非常慎重——commitAllowingStateLoss在极端场景下可能导致UI状态和逻辑状态对不上。我的个人建议是能用状态管理框架比如ViewModel LiveData把数据流的节奏控制好就尽量别在生命周期回调里做无谓的commit。次数多了总有一次你会踩到状态丢失的雷。5.5 savedInstanceState里到底装了什么很多人好奇为什么一个Fragment的savedInstanceState可以在Activity销毁重建后还能恢复到原来的View状态。这个机制的底层是BundleBundle本身是一个键值对但它不是普通Map而是一个可以跨进程传递的轻量化Parcelable结构。当FragmentManager把FragmentState写入Bundle时它实际上是把一个Parcelable对象序列化成了字节流。等Activity恢复时再从字节流反序列化出FragmentState。也正因如此savedInstanceState里能放的数据类型是有限制的必须能通过Parcel写入。你要是往里面塞一个自定义的复杂对象要么让它实现Serializable要么实现Parcelable否则运行时会直接抛NotSerializableException。6. 从原理层面解读那些绕不开的Fragment坑6.1 为什么会重叠add还是replace选错之后的必然结果Fragment重叠问题太经典了。用add而不是replaceFragmentA的View没有被移除FragmentB的View又加进来两个View叠在同一个容器里。从原理上看replace操作本质上是先将旧Fragment从mAdded中移除触发onDestroyView再把新Fragment添加进来触发onCreateView所以旧的View必然被删除。但如果你用add旧Fragment还在mAdded里它的View自然还在容器里。实际上正确的add/hide/show使用场景是你希望这几个Fragment并行存在于Activity中切换时只是改变可见性。这时候你应该使用hide/show而不是add多个Fragment让它们都显示。每次遇到页面重叠第一反应应该是去检查自己用的是add还是replace以及hide/show是否成对出现。6.2 为什么popBackStack之后Fragment还活着从回退栈弹出Fragment后很多人发现Fragment还在mActive列表里甚至onDestroyView都没有被调用。这是因为回退栈的记录被弹出了但Fragment没有被标记为remove。要真正销毁一个Fragment必须在事务里显式调用remove或者确保它是replace操作中的被替换方。这个坑在底部导航栏切换场景里特别常见。你用add/hide/show实现底部导航然后每次切换都addToBackStack导致回退栈越积越深按返回键的时候Fragment并不会被移除只是像剥洋葱一样一层层弹开甚至会出现退到首页按返回键又回到上一页的诡异现象。根本原因就是你无意中把hide/show操作也加入回退栈了。6.3 getChildFragmentManager和getParentFragmentManager的边界嵌套Fragment在实现原理上并不复杂就是每个Fragment内部都有一个自己的FragmentManager叫ChildFragmentManager它和Activity的FragmentManager一样也有mActive、mAdded、mBackStack三张表。子Fragment的状态迁移是跟随父Fragment的比如父Fragment进入RESUMED子Fragment才跟着进入RESUMED。但有一点需要注意如果你在子Fragment里调用getFragmentManager不带child的那个旧API实际拿到的是Activity的FragmentManager而不是父Fragment的ChildFragmentManager。这个拿错管理器的后果是你添加的Fragment会成为Activity的直接子Fragment层级彻底错乱。正确的做法是在Fragment内一律使用getChildFragmentManager来管理子Fragment用getParentFragmentManager来和父级交互。6.4 为什么有的Fragment不执行onDestroy来看一个我实际排查过的案例某个页面里add了一个Fragment然后没有remove也没有replace直接退出了Activity。正常情况下Activity销毁时所有mActive里的Fragment都应该走onDestroy。但如果我们用了retainInstance trueFragment就不会销毁而是被转移到一个新的容器里等待复用。这种设计本来是为了旋转屏幕时保留数据但也意味着如果你不小心用了它Fragment的onDestroy永远不触发导致它持有的资源永远释放不掉。还有一个容易忽视的情况如果你在Fragment的onStop里启动了一个新的Activity原Activity被系统回收后FragmentManager恢复时会恢复Fragment但如果你的Activity使用了finish()而不是finishAndRemoveTask()屏幕上看起来Activity被销毁了但它的Fragment其实还在后台存活着。这种情况下Fragment的onDestroy要很久以后甚至永远不会才被调用。排查这类问题最好的办法是在Fragment的各个生命周期回调里加日志把onDestroyView、onDestroy、onDetach全部打出来。一旦发现某个回调缺失再结合具体场景去分析是哪种机制把Fragment挡住了。6.5 什么时候该用hide/show什么时候该用replace从原理角度给出一个明确建议场景推荐方式原因页面切换且不保留旧页状态replace旧View销毁新View创建避免历史状态干扰页面切换且需保留旧页状态如表单、列表位置add/hide/show旧View保留前提是容器足够大不会被系统回收底部导航栏类经典场景show/hide其他 add当前各页面并行存活切换快单容器动态切换页replace避免View叠加内存开销可控注意add/hide/show的代价所有Fragment的View都挂在Activity的View树里即使隐藏了View对象也不会被回收。如果一个Fragment的View非常重比如包含大量图片加载、视频播放器这种模式的内存占用会持续上涨。你以为的保留状态其实是用内存换来的。7. 现在的Fragment原理仍然不过时我听到过一种说法Jetpack Compose都出了Fragment是不是要淘汰了我的观点是短期不会而且即便Compose时代真的全面到来FragmentManager作为状态容器和生命周期组件的思想也依然会以某种形式存在。N年前Navigation组件刚推出时所有人都以为它要取代FragmentManager。但你看一下Navigation的源码就会发现它内部就是包装了FragmentManager用NavController来调度Fragment事务底层还是这一套状态机、事务队列、回退栈。你之前踩过的Fragment坑在Navigation里一个不落只是换了个入口。Compose时代Fragment依然会在Activity持有FragmentFragment承载ComposeView这种混合架构里长期存在。因为对于成熟的商业App来说从View体系迁移到Compose需要一个较长的过渡期而Fragment作为中间桥梁正好能兼容两边。从更深一层来看Fragment教给我们的核心思想——状态机驱动生命周期、事务原子性保证一致性、状态保存与恢复的容错机制——这些设计理念在任何UI框架里都是通用的。我把Fragment的实现原理翻来覆去研究这么多遍最大的收获不是会写Fragment了而是对各种UI框架如何管理复杂状态有了一个全局的认识。最后分享一个最近实际项目中遇到的案例线上版本崩溃统计里有一个高频crash是Fragment not attached to a context。崩溃堆栈指向的是Fragment里调用getActivity()获取了空指针。我让同事检查代码发现他在异步回调里直接用了activity?.let { ... }而回调回来的时候Fragment已经detach了。修复方式是把异步任务绑定到ViewModel的viewModelScope里通过Lifecycle感知自动取消而不是每次都强行拿Activity。这类问题在原理层面看就是一目了然Fragment的detach会先于Activity的销毁而且随时可能发生所以任何需要在Fragment销毁后继续执行的长任务都不能放在Fragment里开线程。这大概就是搞懂原理最大的价值——它不会直接告诉你每一行代码怎么写但它能让你在看到崩溃日志的时候脑子里立刻浮现出那个状态的迁移路线哦原来是走到detach这一步了所以getActivity是空的。这条路走熟了Fragment就不再是背锅侠而是你手里最顺手的工具。
返回列表