美文网首页
从一个bug开始,理解Fragment和ViewPager2的状

从一个bug开始,理解Fragment和ViewPager2的状

作者: BlueSocks | 来源:发表于2023-07-09 19:38 被阅读0次

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

    相关文章

      网友评论

          本文标题:从一个bug开始,理解Fragment和ViewPager2的状

          本文链接:https://www.haomeiwen.com/subject/jtisudtx.html