OK,填坑篇的文章来了。
你的ViewPager八成用错了。
错误的ViewPager用法(续),会产生内存泄漏?内存溢出?
当我打开官方文档准备开始了解FragmentStatePagerAdapter的时候。我仿佛像是...闭关蛰伏数十载,准备反清复明;出关时发现大清已经亡了...
什么鬼,我还不会用呢,就tm废弃了???
当然这不妨碍咱们去了解它如何增强了FragmentPagerAdapter。扶我起来,我还能学!
看FragmentStatePagerAdapter之前,咱们还是要先看文档
官网是这么介绍这个类的(我直接用自己蹩脚的英文翻译了一下):
当存在大量fragment时,此版本的更加高效。当Fragment对用户不可见时,它们的整个Fragment可能会被destory,仅保留该Fragment的状态。与FragmentPagerAdapter相比会占用更少的内存。
它的用法和FragmentPagerAdapter(以下简称FPA)一模一样,这里就不展开了。大家有兴趣可以直接看文档中的demo。
从文档介绍来看,FragmentStatePagerAdapter提供更少的内存开销。第二篇文章,咱们也已经明白了FragmentPagerAdapter在FragmentManager体系下会可能出现大量内存消耗的问题。那么咱们就来看看,FragmentStatePagerAdapter是如何优化这个问题。
FragmentStatePagerAdapter(以下简称FSPA)的实现比较的简单,解决方式也很简单粗暴。咱们先看一个关键的方法instantiateItem(),基于这个方法咱们分4步来看一下这里的实现原理:
@Override public Object instantiateItem(@NonNull ViewGroup container, int position) { // 步骤1 if (mFragments.size() > position) { Fragment f = mFragments.get(position); if (f != null) { return f; } } // 省略代码 // 步骤2 Fragment fragment = getItem(position); if (mSavedState.size() > position) { Fragment.SavedState fss = mSavedState.get(position); if (fss != null) { fragment.setInitialSavedState(fss); } } // 步骤 3 while (mFragments.size() <= position) { mFragments.add(null); } // 省略部分代码 // 步骤4 mFragments.set(position, fragment); mCurTransaction.add(container.getId(), fragment); // 省略部分代码 return fragment; } 复制代码
我们可以看到这里的instantiateItem()和FPA有着极大的不同:这里没有通过FragmentManager去find已经存在的Fragment!这里可以断定FSPA失去了FPA上缓存的逻辑,接下来咱们会通过FSPA的源码来进一步了解二者逻辑上的不同。
步骤1中的mFragments是Adapter里的局部变量private ArrayList<Fragment> mFragments = new ArrayList<>()
,看到着我们第一想法就能够明白FSPA对Fragment的管理,在FragmentManager的基础上包了一层。
这里的处理也很简单粗暴,如果基于position能在mFragments中找到Fragment就直接return。这里有一个点,我们需要注意,这里是直接return。也就是意味着被mFragment持有的Fragment实例是没有从FragmentManager中detach的,因此不需要重新走状态。
此外需要留意的一点是:if (f != null)
,意味着mFragments里是有可能为null的,所以我们可以猜测mFragments对Fragment也是一个动态变化的持有关系。
很熟悉的方法调用,找不到缓存的Fragment,调getItem(),交给实现方自行初始化Fragment。
然后基于mSavedState对当前Fragment执行一次initSavedState操作。
这里可能有小伙伴会有疑问,新new出来的Fragment为啥有可能会有SavedState呢?
针对这个问题,先简单解释一下(大家可以再后文中得到详细答案):因为这个mSavedState会存在所有实例过的Fragment的状态,但是mFragments里仅仅会存放当前attach的Fragment。因此调用getItem()时初始化的Fragment是有可能之前初始化过,因此这种case下是要恢复其状态的。
步骤三做的事情就比较有趣了:
while (mFragments.size() <= position) { mFragments.add(null); } 复制代码
说白了就是在占位。看到这一步,咱们就能明白:mFragments就是一个“以position为key,fragment为value的Map”。
当我们定位到一个很靠后的position时。那么代码走到这我们得到的mFragments的List很有可能是这样的 :[fragment1,fragment2,null,null,null,接下来要被add的fragment6]
步骤四就很简单了,add我们getItem出来的Fragment。
看完这四步,咱们大概也会发现代码并没有什么难的,虽然我们只看了一个方法,但是基本可以猜出FSPA的原理:
看起来是因为缓存的Fragment数量少了所以内存开销变少了...不过我猜有同学这个时候会提出疑问:即使FSPA里mFragments缓存的Fragment少了,但是FragmentStore里该缓存还是要缓存的啊,这么一看,FSPA甚至多缓存了一份!
接下来咱们就要看另一个方法了,看看FSPA如果解决上述的问题。
其实有了第二篇文章的分析,咱们已经明确是FragmentManager内存爆炸的原因就是在于FragmentStore在mActive中强引用了所有的Fragment实例,不进行任何回收。
既然FSPA号称更少的开销,那么势必要直面这个问题。所以接下来就让咱们看看,FSPA销毁Fragment的策略。
FSPA和FPA主要区别就在于对destroyItem()的实现。这里咱们先对比一下二者的实现:
// FSPA @Override public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { Fragment fragment = (Fragment) object; if (mCurTransaction == null) { mCurTransaction = mFragmentManager.beginTransaction(); } while (mSavedState.size() <= position) { mSavedState.add(null); } mSavedState.set(position, fragment.isAdded() ? mFragmentManager.saveFragmentInstanceState(fragment) : null); mFragments.set(position, null); // 注意这里 mCurTransaction.remove(fragment); if (fragment.equals(mCurrentPrimaryItem)) { mCurrentPrimaryItem = null; } } // FPA @Override public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { Fragment fragment = (Fragment) object; if (mCurTransaction == null) { mCurTransaction = mFragmentManager.beginTransaction(); } // 注意这里 mCurTransaction.detach(fragment); if (fragment.equals(mCurrentPrimaryItem)) { mCurrentPrimaryItem = null; } } 复制代码
FSPA调用了remove方法,而FPA调用的是detach方法。接下来咱们就看看这二者有什么不同。其实无论是remove还是detach都会走到executeOps()中的switch判断:
case OP_REMOVE: f.setNextAnim(op.mExitAnim); mManager.removeFragment(f); break; case OP_DETACH: f.setNextAnim(op.mExitAnim); mManager.detachFragment(f); break; 复制代码
但是这里无论是removeFrament()还是detachFragment()。本质调的都是mFragmentStore.removeFragment(fragment);
,这里是把当前Fragment从FragmentStore中的mAdded列表移除还不会动mActive列表。
因此对于FSPA来说,它并不是通过这种方式来控制内存开销。咱们继续往下看...
上述switch判断结束后,才会走到真正驱动状态的地方:
if (!mReorderingAllowed && op.mCmd != OP_ADD && f != null) { // 这里边会走到moveToState()中 mManager.moveFragmentToExpectedState(f); } FragmentManager#moveToState() if (f.mState <= newState) { switch (f.mState) { case Fragment.INITIALIZING:{ if (newState > Fragment.INITIALIZING) { // 省略部分代码 } } case Fragment.ATTACHED:{ // 省略部分代码 } // 省略部分代码 }else if (f.mState > newState) { switch (f.mState) { case Fragment.RESUMED: if (newState < Fragment.RESUMED) { // 省略部分代码 } case Fragment.CREATED: if (newState < Fragment.CREATED) { // 重点在这 boolean beingRemoved = f.mRemoving && !f.isInBackStack(); if (beingRemoved || mNonConfig.shouldDestroy(f)) { makeInactive(fragmentStateManager); } // 省略部分代码 } // 省略部分代码 } // 省略部分代码 } 复制代码
这里状态机的逻辑,大家有兴趣可以自己阅读一下。这里处理状态的逻辑还是挺“骚”的。咱们只关注makeInactive()
。上文我们之后remove和detach的区别,而这个区别的分水岭就在于这个方法。remove是会走到这个方法中:
private void makeInactive(@NonNull FragmentStateManager fragmentStateManager) { // 省略部分代码 mFragmentStore.makeInactive(fragmentStateManager); removeRetainedFragment(f); } void makeInactive(@NonNull FragmentStateManager newlyInactive) { Fragment f = newlyInactive.getFragment(); for (FragmentStateManager fragmentStateManager : mActive.values()) { if (fragmentStateManager != null) { Fragment fragment = fragmentStateManager.getFragment(); if (f.mWho.equals(fragment.mTargetWho)) { fragment.mTarget = f; fragment.mTargetWho = null; } } } mActive.put(f.mWho, null); if (f.mTargetWho != null) { f.mTarget = findActiveFragment(f.mTargetWho); } } 复制代码
可以看到makeInactive()
方法中会对mActive进行回收的操作。因此FSPA比FPA的优化就在于移除掉了对mActive中“不必要”的引用。
我猜看到这大家应该就能够get到FSPA的优化点,不过...问题来了:既然把FragmentManager中mActive移除掉了,那我们的缓存呢?
事实的确如此,咱们在开篇看instantiateItem()
实现的时候就已经发现,FSPA移除了通过FragmentManager去find缓存的逻辑。
咱们基于之前的文章,可以明白FPA的缓存是基于FragmentManager的mActive缓存,也明白FPA内存溢出也是因为FragmentManager的mActive缓存。
因此FSPA的优化原理也很好理解,在FragmentManager中移除掉了mActive的缓存。
这里也就意味着,FSPA和FPA有一些不同:
这里咱们多聊一句。不知道大家有没有发现,无论上FPA还是FSPA,Google都没有主动提供获取内部持有Fragment的public方法。甚至在FSPA中,移除了任何这种操作的可能行。
如果单纯从这个现象来看,基于ViewPager去变相的获取内部Fragment是一个“不合理”的操作。但是咱们也很清楚需求这种东西,如果都“合理”那就不叫需求了...因此这种操作是无法避免的。所有,咱们需要从FSPA和FPA的不同点来明确咱们该用谁...
当然鉴于FSPA已经被废弃了,咱们项目中首选还是ViewPager2。关于ViewPager2的分析会在后续放出...
算上今天的文章,关于Fragment在ViewPager中应用的文章已经三篇了。
尽可能的学的深入,尽可能的发布正确的文章。欢迎大家评论区一起讨论~