从一个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的状态保存恢复流程更清晰

流程1.png

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

流程2.png

在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,虽然听起来很扯但是确实有用 ; )

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容