从一个bug开始,理解Fragment和ViewPager2的状态恢复流程

在使用Fragment和ViewPager2时遇到了一个奇怪的bug,于是顺藤摸瓜学习了一下Fragment和View的状态保存恢复流程,解决方法在最后面。
首先看一下崩溃调用栈

java.lang.IllegalStateException: Expected the adapter to be 'fresh' while restoring state.
at androidx.viewpager2.adapter.FragmentStateAdapter.restoreState(FragmentStateAdapter.java:536)
at androidx.viewpager2.widget.ViewPager2.restorePendingState(ViewPager2.java:350)
at androidx.viewpager2.widget.ViewPager2.dispatchRestoreInstanceState(ViewPager2.java:375)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:4099)
at android.view.View.restoreHierarchyState(View.java:20357)
at androidx.fragment.app.Fragment.restoreViewState(Fragment.java:639)
at androidx.fragment.app.Fragment.restoreViewState(Fragment.java:3010)
at androidx.fragment.app.Fragment.performActivityCreated(Fragment.java:3001)

接下来描述一下我遇到这个bug的场景,方便大家对号入座:

首先在创建Activity时将MainFragment添加到了Activity中,MainFragment里又会通过FragmentStateAdapter将Fragment添加到MainFragment的ViewPager2中。然后通过消息推送,让activity调用FragmentManager.FragmentTransaction.replace()移除了MainFragment并添加了SecondFragment(这里还有一行重点代码FragmentManager.FragmentTransaction.addToBackStack(),后面会讲它为什么会导致这个bug的出现),接着再调用同一个FragmentManager的FragmentManager.popBackStack()方法,然后程序崩溃。

然后是排查过程:

首先发现是因为MainFragment只调用了onDestroyView()而没有调用onDestroy()(只销毁了视图,但是实例还存在),而我的FragmentStateAdapter是跟随MainFragment对象一起初始化的,因为对象没有被销毁所以只初始化了一次,并且里面的状态(adapter管理的saveStates和fragments也都保存着),所以在Fragment.performActivityCreated时会判断

if (mView != null) {
    restoreViewState(mSavedFragmentState);
}

然后会调用到viewpager2的dispatchRestoreInstanceState(),内部最终调用FragmentStateAdapter.restoreState()

if (!mSavedStates.isEmpty() || !mFragments.isEmpty()) {
    throw new IllegalStateException(
            "Expected the adapter to be 'fresh' while restoring state.");
}

那么肉眼可见的是,这个bug是和fragment的状态销毁和重建有关的,大概的原因是:在使用FragmentManager.replace()切换fragment时,FragmentManager会将当前将要被销毁的Fragment视图从Activity中移除,并将新的Fragment的视图加载到activity上。因为我们将这个事务加入了返回栈FragmentManager.FragmentTransaction.addToBackStack(),所以FragmentManager不会销毁或者解绑这个fragment实例,只是把视图销毁了。并且FragmentManager会保存Fragment和Adapter的状态再销毁视图,在这个事务弹出返回栈时,FragmentManager又会控制fragment恢复它的视图状态,接着FragmentStateAdapter发现它自己不干净(mSavedStates不为空),于是自爆了。

接下来详细跟一遍fragment和viewpager2状态保存恢复的流程(已简化)

这段的流程有点长,其实大概流程上面已经讲清楚了,只是看了的话会对理解Fragment和View的状态保存恢复流程更清晰

在这里插入图片描述

当我点击/执行了返回操作,触发了FragmentManager.popBackStack(),就会走一遍下面这个流程

在这里插入图片描述

在FragmentStateAdapter准备恢复当前Fragment视图上的ViewPager2的状态时,崩溃就产生了。

一点牢骚

说实话,我觉得官方代码在这里直接抛出异常是很愚蠢的行为,因为通过将Transaction加入返回栈addToBackStack(),加入返回栈的Fragment就只会被销毁视图onDestroyView()而实例仍然被FragmentManager持有(fragment不会与activity解绑,也不会执行onDestroy()),并将在弹出返回栈时恢复这个Fragment的状态,所以如果你不做任何特殊处理,FragmentStateAdapter.mSavedStates必然是不为空的,而且FragmentStateAdapter并没有提供任何方法让我们可以去清除它的缓存(我们甚至都不能重写它的saveState()和restoreState(),太扯淡了),因此看起来就像谷歌让ViewPager2不接受一个复用的adapter。我不明白为什么官方要在这里选择让程序崩溃而不是清空之前的mSavedStates,因为要触发这个崩溃只需要一个很常见的场景和代码。

吐槽完毕接下来就说一下解决方法吧,因为能改动的地方很有限,所以我觉得下面这几个方法都不是很好,而且有利有弊,但是总归是能解决问题。

解决方法

方案1:

将Transaction的replace改成add和hide,避免了fragment重新创建视图,也就不会触发FragmentStateAdapter.restoreState(),所以崩溃的问题就解决了(没有动画的需求用这个方法就行了)。但是通过add和hide,我的mainFragment的渐隐动画没有被触发,mainFragment的视图直接被隐藏了,这样肯定是不能满足我的需求的。

方案2:

既然是视图状态恢复的时候崩溃的,那我禁用掉viewpager2的状态恢复不就可以跳过抛出异常的代码了吗?调用view.setSaveEnabled(false)就可以禁用view的状态保存和恢复。实践结果证明这是可行的,但是我的Fragment消失转场动画也消失了,并且每次返回时都会返回到position 0。

方案3:

不保存adapter的实例,而是在onViewCreated()里每次都创建一个新的FragmentStateAdapter并赋值给viewpager2.adapter,并且在onDestroyView()里将viewpager2的adapter移除掉viewpager2.adapter = null。这个方法的思路和方法2类似,也是通过手动控制避开viewpager2的状态恢复代码。

方案4:

先将MainFragment和SecondFragment都添加到activity中,然后隐藏除了MainFragment以外的其他Fragment

val secondFragment = SecondFragment()
supportFragmentManager.beginTransaction()
    .add(
        vb.container.id,
        MainFragment::class.java,
        null,
        MainFragment::class.simpleName
    )
    .add(
        vb.container.id,
        SecondFragment,
        SecondFragment::class.simpleName
    )
    .hide(pictureDetailsFragment)
    .commit()

然后在需要展示SecondFragment的时候使用FragmentManager.FragmentTransaction.show(secondFragment)FragmentManager.FragmentTransaction.hide(mainFragment)来切换fragment。 这是我认为最好的解决方案。因为这样即避免了fragment的状态保存和恢复流程以及fragment各种创建时的回调代码(提高了性能),也能保证过渡动画的正常运作。不过这个方法也有一个弊端,就是我们需要注意SecondFragment刷新界面(加载布局/动画/刷新数据)的时机,因为我们一开始就将fragment都添加到activity上了,所以fragment会跟随activity走完整个启动的生命周期(例如onCreateView()和onResume()),在切换显示隐藏时SecondFragment只会回调onHiddenChange(isHidden:Boolean)方法,所以我们要注意在SecondFragment真正准备显示出来的时候再执行对应的界面刷新操作

方案5:

把ViewPager2换成ViewPager和FragmentStatePagerAdapter,虽然听起来很扯但是确实有用 ; )

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
img
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值