# 前言

OK,填坑篇的文章来了。

当我打开官方文档准备开始了解 FragmentStatePagerAdapter 的时候。我仿佛像是… 闭关蛰伏数十载,准备反清复明;出关时发现大清已经亡了…

img

什么鬼,我还不会用呢,就 tm 废弃了???

# 正文

当然这不妨碍咱们去了解它如何增强了 FragmentPagerAdapter。扶我起来,我还能学!

看 FragmentStatePagerAdapter 之前,咱们还是要先看文档

官网是这么介绍这个类的(我直接用自己蹩脚的英文翻译了一下):

当存在大量 fragment 时,此版本的更加高效。当 Fragment 对用户不可见时,它们的整个 Fragment 可能会被 destory,仅保留该 Fragment 的状态。与 FragmentPagerAdapter 相比会占用更少的内存。

它的用法和 FragmentPagerAdapter(以下简称 FPA)一模一样,这里就不展开了。大家有兴趣可以直接看文档中的 demo。

从文档介绍来看,FragmentStatePagerAdapter 提供更少的内存开销。第二篇文章,咱们也已经明白了 FragmentPagerAdapter 在 FragmentManager 体系下会可能出现大量内存消耗的问题。那么咱们就来看看,FragmentStatePagerAdapter 是如何优化这个问题。

# 一、如果做到更少的内存开销?

FragmentStatePagerAdapter(以下简称 FSPA)的实现比较的简单,解决方式也很简单粗暴。咱们先看一个关键的方法 instantiateItem (),基于这个方法咱们分 4 步来看一下这里的实现原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@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、步骤三分析

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

1
2
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 () 的实现。这里咱们先对比一下二者的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 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 判断:

1
2
3
4
5
6
7
8
9
 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 判断结束后,才会走到真正驱动状态的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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 是会走到这个方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 的分析会在后续放出…

# 尾声

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