FragmentStatePagerAdapter在ViewPager中优化了什么

4,203 阅读8分钟

前言

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.1、步骤一分析

步骤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也是一个动态变化的持有关系。

1.2、步骤二分析

很熟悉的方法调用,找不到缓存的Fragment,调getItem(),交给实现方自行初始化Fragment。

然后基于mSavedState对当前Fragment执行一次initSavedState操作。

这里可能有小伙伴会有疑问,新new出来的Fragment为啥有可能会有SavedState呢?

针对这个问题,先简单解释一下(大家可以再后文中得到详细答案):因为这个mSavedState会存在所有实例过的Fragment的状态,但是mFragments里仅仅会存放当前attach的Fragment。因此调用getItem()时初始化的Fragment是有可能之前初始化过,因此这种case下是要恢复其状态的。

1.3、步骤三分析

步骤三做的事情就比较有趣了:

while (mFragments.size() <= position) {
    mFragments.add(null);
}

说白了就是在占位。看到这一步,咱们就能明白:mFragments就是一个“以position为key,fragment为value的Map”。

当我们定位到一个很靠后的position时。那么代码走到这我们得到的mFragments的List很有可能是这样的 :[fragment1,fragment2,null,null,null,接下来要被add的fragment6]

1.4、步骤四分析

步骤四就很简单了,add我们getItem出来的Fragment。

看完这四步,咱们大概也会发现代码并没有什么难的,虽然我们只看了一个方法,但是基本可以猜出FSPA的原理:

  • 只缓存当前attach上的Fragment
  • 缓存所有attach过Fragment的SaveState,以便重新new时的状态恢复

看起来是因为缓存的Fragment数量少了所以内存开销变少了...不过我猜有同学这个时候会提出疑问:即使FSPA里mFragments缓存的Fragment少了,但是FragmentStore里该缓存还是要缓存的啊,这么一看,FSPA甚至多缓存了一份!

接下来咱们就要看另一个方法了,看看FSPA如果解决上述的问题。

二、销毁Fragment

其实有了第二篇文章的分析,咱们已经明确是FragmentManager内存爆炸的原因就是在于FragmentStore在mActive中强引用了所有的Fragment实例,不进行任何回收。

既然FSPA号称更少的开销,那么势必要直面这个问题。所以接下来就让咱们看看,FSPA销毁Fragment的策略。

2.1、destroyItem()

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来说,它并不是通过这种方式来控制内存开销。咱们继续往下看...

2.2、控制Fragment的状态机

上述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有一些不同:

  • 1、只要不在mAdd的Fragment,FSPA都会走getItem()去new Fragment。
  • 2、我们没办法方便的基于FragmentManager去拿到我们想要得到的Fragment实例。(FSPA是基于id去把Fragment添加到mAdd)

3.1、ViewPager中取特定Fragment实例是否合理

这里咱们多聊一句。不知道大家有没有发现,无论上FPA还是FSPA,Google都没有主动提供获取内部持有Fragment的public方法。甚至在FSPA中,移除了任何这种操作的可能行。

如果单纯从这个现象来看,基于ViewPager去变相的获取内部Fragment是一个“不合理”的操作。但是咱们也很清楚需求这种东西,如果都“合理”那就不叫需求了...因此这种操作是无法避免的。所有,咱们需要从FSPA和FPA的不同点来明确咱们该用谁...

  • 如果我们需要FragmentManager去缓存我们的Fragment那么FPA是一个不错的选择。
  • 如果我们拥有大量的Fragment在ViewPager中,那么FSPA是一个不错的选择。

当然鉴于FSPA已经被废弃了,咱们项目中首选还是ViewPager2。关于ViewPager2的分析会在后续放出...

尾声

算上今天的文章,关于Fragment在ViewPager中应用的文章已经三篇了。

尽可能的学的深入,尽可能的发布正确的文章。欢迎大家评论区一起讨论~

我是一个应届生,最近和朋友们维护了一个公众号,内容是我们在从应届生过渡到开发这一路所踩过的坑,以及我们一步步学习的记录,如果感兴趣的朋友可以关注一下,一同加油~

个人公众号:咸鱼正翻身