ViewPager2中的Fragment懒加载实现方式

前言
ViewPager2是官方推出的新控件,从名称上也能看出是用于替代ViewPager的,它是基于RecyclerView实现的,因此可以实现一些ViewPager没有的功能,最实用的一点就是支持竖直方向滚动了。
虽然很早就听说过,但是从一些文章中也多少了解到ViewPager2使用的一些坑,也就一直没有正式使用过。前不久ViewPager2发布了1.0.0正式版,心想是时候尝试一下了。哈哈,可能是因为此前写过两篇懒加载相关的文章吧,我第一时间想到的不是ViewPager新功能的使用,而是在配合Fragment时如何实现懒加载。本文就来具体探究一下ViewPager2中的懒加载问题,关于ViewPager2的使用已经有很多详细的文章了,不是本文的研究重点,因此就不会具体介绍了。

在进入正文之前要强调一下,本文的分析基于ViewPager2的1.0.0版本,是在androidx包下的,因此在使用ViewPager2之前需要做好androidx的适配工作。

利用ViewPager2加载多个Fragment

第一步、首先需要在build.gradle文件中添加ViewPager2的依赖

implementation 'androidx.viewpager2:viewpager2:1.0.0'

第二步、在布局文件中添加ViewPager2


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/view_pager2"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

LinearLayout>

第三步、编写Adapter
需要注意,ViewPager2中加载Fragment时的Adapter类需要继承自FragmentStateAdapter,而不是ViewPager中的FragmentStatePagerAdapter。

public class MyFragmentPagerAdapter extends FragmentStateAdapter {

    private List<Fragment> mFragments;

    public MyFragmentPagerAdapter(@NonNull FragmentActivity fragmentActivity, List<Fragment> fragments) {
        super(fragmentActivity);
        this.mFragments = fragments;
    }

    @NonNull
    @Override
    public Fragment createFragment(int position) {
        return mFragments.get(position);
    }

    @Override
    public int getItemCount() {
        return mFragments.size();
    }
}

第四步、为ViewPager2设置Adapter

ViewPager2 mViewPager2 = findViewById(R.id.view_pager2);
List<Fragment> mFragments = new ArrayList<>();
mFragments .add(new FirstFragment());
mFragments .add(new SecondFragment());
mFragments .add(new ThirdFragment());
MyFragmentPagerAdapter mAdapter = new MyFragmentPagerAdapter(this, mFragments);
mViewPager2.setAdapter(mAdapter);

经过以上几步我们就实现了利用ViewPager2加载多个Fragment,当然我这里是为了简单演示,具体的Fragment类我就不展示了。

Fragment切换时的生命周期方法执行情况

接下来我们具体来看一下Fragment切换时生命周期方法的执行情况。我在测试用例中添加了6个Fragment,在Fragment的生命周期回调方法中打印执行情况,具体执行结果如下:

  • 初始情况显示第一个Fragment
    ViewPager2中的Fragment懒加载实现方式_第1张图片

可以看出此时只创建出了第一个Fragment,生命周期方法执行到了onResume(),其他的几个Fragment并没有创建。

  • 切换到第二个Fragment
    ViewPager2中的Fragment懒加载实现方式_第2张图片

此时创建出了第二个Fragment,生命周期方法同样执行到onResume(),同时,第一个Fragment执行onPause()方法。

  • 切换到第三个Fragment
    ViewPager2中的Fragment懒加载实现方式_第3张图片

和上一种情况相同,创建出第三个Fragment,执行到onResume()方法,同时第二个Fragment执行onPause()方法。

  • 切换到第四个Fragment
    ViewPager2中的Fragment懒加载实现方式_第4张图片

和前两种情况相同,同样是创建出当前Fragment,生命周期方法执行到onResume(),并且上一个Fragment执行onPause()方法。不同的是,此时会销毁第一个Fragment,依次执行onStop()onDestroyView()onDestroy()onDetach()方法。

  • 切换到第五个Fragment
    ViewPager2中的Fragment懒加载实现方式_第5张图片

和上一种情况相同,创建出第五个Fragment,生命周期方法执行到onResume(),第四个Fragment执行onPause()方法,同时销毁第二个Fragment。

  • 切换到第六个(最后一个)Fragment
    ViewPager2中的Fragment懒加载实现方式_第6张图片

可以看出此时创建出了第六个Fragment,生命周期方法执行到onResume(),第五个Fragment执行onPause()方法,如果按照上面两种情况的执行结果来看,此时应该会销毁第三个Fragment,但实际上并没有。
从以上几种情况下Fragment生命周期方法的执行情况来看,不难看出ViewPager2默认情况下不会预先创建出下一个Fragment。但与此同时,Fragment的销毁情况就令我有些不解了,如果不看切换到最后一个Fragment的情况,我们可以猜测是由于ViewPager2内部RecyclerView的缓存机制导致最多可以存在三个Fragment,但是切换到最后一个Fragment的情况就违背了我们的猜测,很明显此时并没有销毁前面的Fragment。接下来我们就根据上述结果来分析一下ViewPager2加载Fragment的几个问题。

ViewPager2中的setOffscreenPageLimit()方法

通过示例中的执行结果我们可以发现ViewPager2默认情况下不会像ViewPager那样预先加载出两侧的Fragment,这是为什么呢,我们可能会想到ViewPager中预加载相关的一个方法:setOffscreenPageLimit(),ViewPager2中也定义了该方法,我们来看一下它们的区别。
首先来看ViewPager中的setOffscreenPageLimit()方法:

private static final int DEFAULT_OFFSCREEN_PAGES = 1;
private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES;

public void setOffscreenPageLimit(int limit) {
    if (limit < DEFAULT_OFFSCREEN_PAGES) {
        Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to "
                + DEFAULT_OFFSCREEN_PAGES);
        limit = DEFAULT_OFFSCREEN_PAGES;
    }
    if (limit != mOffscreenPageLimit) {
        mOffscreenPageLimit = limit;
        populate();
    }
}

方法传入一个整型数值,表示当前Fragment两侧的预加载数量,很多人可能都知道,ViewPager默认的预加载数量为1,也就是会预先创建出当前Fragment左右两侧的一个Fragment。从代码中我们可以看出,如果我们传入的数值小于1,依然会将预加载数量设置为1,这也导致了ViewPager无法取消预加载,也因此才会需要Fragment的懒加载方案。
接下来我们来看ViewPager2中的setOffscreenPageLimit()方法:

public static final int OFFSCREEN_PAGE_LIMIT_DEFAULT = -1;
private int mOffscreenPageLimit = OFFSCREEN_PAGE_LIMIT_DEFAULT;

public void setOffscreenPageLimit(@OffscreenPageLimit int limit) {
    if (limit < 1 && limit != OFFSCREEN_PAGE_LIMIT_DEFAULT) {
        throw new IllegalArgumentException(
                "Offscreen page limit must be OFFSCREEN_PAGE_LIMIT_DEFAULT or a number > 0");
    }
    mOffscreenPageLimit = limit;
    // Trigger layout so prefetch happens through getExtraLayoutSize()
    mRecyclerView.requestLayout();
}

我们可以看出ViewPager2中默认的预加载数量mOffscreenPageLimit为OFFSCREEN_PAGE_LIMIT_DEFAULT也就是-1,我们可以通过传入该默认值或者大于1的整数来设置预加载数量。接下我们来看一下哪里用到了mOffscreenPageLimit,通过全局搜索,我们可以发现在ViewPager2的内部类LinearLayoutManagerImpl中的calculateExtraLayoutSpace()方法中通过getOffscreenPageLimit()方法获取了mOffscreenPageLimit。

@Override
protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state,
                                         @NonNull int[] extraLayoutSpace) {
    int pageLimit = getOffscreenPageLimit();
    if (pageLimit == OFFSCREEN_PAGE_LIMIT_DEFAULT) {
        // Only do custom prefetching of offscreen pages if requested
        super.calculateExtraLayoutSpace(state, extraLayoutSpace);
        return;
    }
    final int offscreenSpace = getPageSize() * pageLimit;
    extraLayoutSpace[0] = offscreenSpace;
    extraLayoutSpace[1] = offscreenSpace;
}

calculateExtraLayoutSpace()方法定义在LinearLayoutManager中,用于计算LinearLayoutManager布局的额外空间,也就是RecyclerView显示范围之外的空间,计算结果在保存参数extraLayoutSpace中,它是一个长度为2的整型数组,extraLayoutSpace[0]表示顶部/左侧的额外空间,extraLayoutSpace[1]表示底部/右侧的额外空间(取决于方向)。LinearLayoutManagerImpl重写了该方法,方法内部首先判断了mOffscreenPageLimit的值,如果等于默认值OFFSCREEN_PAGE_LIMIT_DEFAULT,则直接调用父类方法,不设置额外的布局空间;如果mOffscreenPageLimit的值大于1,则设置左右(或上下)两边的额外空间为getPageSize() * pageLimit,相当于预加载出了两边的Fragment。
看到这里我们就清楚了为什么默认情况下ViewPager2不会预加载出两侧的Fragment,就是因为默认的预加载数量为-1。和ViewPager一样,我们可以通过调用setOffscreenPageLimit()方法,传入大于1的值来设置预加载数量。
在此前的示例中,我们添加下面的代码:

mViewPager2.setOffscreenPageLimit(1);

首次显示第一个Fragment时打印的结果如下:
ViewPager2中的Fragment懒加载实现方式_第7张图片
可以看出此时ViewPager2就会预先创建出下一个Fragment,和ViewPager默认的情况相同。

RecyclerView中的缓存和预取机制

接下来我们来看一下Fragment的销毁情况,探究一下为什么在上面的示例中ViewPager2切换到最后一个Fragment时没有销毁前面的Fragment。在此之前,我们先要了解一下RecyclerView的缓存机制和预取机制。
RecyclerView的缓存机制算是老生常谈的问题了,核心在它的一个内部类Recycler中,Item的回收和复用相关工作都是Recycler来进行的,RecyclerView的缓存可以分为多级,由于我了解得非常浅显,这里就不详细介绍了,大家可以自行查看相关文章。我们直接来看和ViewPager2中Fragment回收相关的缓存——mCachedViews,它的类型是ArrayList,移出屏幕的Item对应的ViewHolder都会被优先缓存到该容器中。Recycler类中有一个成员变量mViewCacheMax,表示mCachedViews最大的缓存数量,默认值为2,我们可以通过调用RecyclerView的setItemViewCacheSize()方法来设置缓存大小。
回到我们的具体场景中,通过查看FragmentStateAdapter类的源码,我们可以看到,此时mCachedViews中保存的ViewHolder类型为FragmentViewHolder,它的视图根布局是一个FrameLayout,Fragment会被添加到对应的FrameLayout中,因此缓存ViewHolder其实就相当于缓存了Fragment,为了简明,我后面就都说成缓存Fragment了,大家清楚这样说是不准确的就好了。在上面的示例中,我们使用ViewPager2加载了6个Fragment,当切换到第四个Fragment时,由于最多只能缓存两个Fragment,此时mCachedViews中缓存的是第二个Fragment和第三个Fragment,因此第一个Fragment就要被销毁,之后切换到第五个Fragment的情况同理,此时会缓存第三个和第四个Fragment,因此第二个Fragment被销毁。接下来问题就来了,如果按照这样的解释,当切换到第六个Fragment时应该销毁第三个Fragment,上面的示例中很明显没有啊,这又是为什么呢?
这就涉及到RecyclerView的预取(Prefetch)机制了,它是官方在support v25版本包中引入的功能,具体表现为在RecyclerView滑动时会预先加载出下一个Item,准确地说是预先创建出下一个Item对应的ViewHolder。默认情况下预取功能是开启的,我们可以调用下面的代码来关闭:

mRecyclerView.getLayoutManager().setItemPrefetchEnabled(false);

那么预取机制会对ViewPager2中Fragment的销毁产生什么影响呢,我们从源码角度来简单分析一下。首先来看RecyclerView的onTouchEvent()方法:
RecyclerView的onTouchEvent()方法

@Override
public boolean onTouchEvent(MotionEvent e) {
    // ...
    switch (action) {
        // ...
        case MotionEvent.ACTION_MOVE: {
            // ...
            if (mGapWorker != null && (dx != 0 || dy != 0)) {
                mGapWorker.postFromTraversal(this, dx, dy);
            }
        }
        break;
        // ...
    }
    // ...
    return true;
}

可以看到在RecyclerView滑动时会调用到mGapWorker的postFromTraversal()方法,将水平和竖直方向上的位移通过参数传入,用于后面计算预取的Item位置。mGapWorker类型为GapWorker,我们来看它的postFromTraversal()方法:
GapWorker的postFromTraversal()方法

/**
 * Schedule a prefetch immediately after the current traversal.
 */
void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy) {
    // ...
    recyclerView.post(this);
    // ...
}

从方法的注释上我们也能看出它和RecyclerView的预取有关,方法内部会调用RecyclerView的post()方法,参数传入了this,也就是当前GapWorker对象,通过查看GapWorker类的定义可以看到它实现了Runnable,因此这里就是提交一个任务到主线程的消息队列中。接下来我们来看GapWorker实现的run()方法:

@Override
public void run() {
    // ...
    long nextFrameNs = TimeUnit.MILLISECONDS.toNanos(latestFrameVsyncMs) + mFrameIntervalNs;
    prefetch(nextFrameNs);
    // ...
}

方法内部会调用prefetch()方法,看到方法名大概可以推测出接下来就要进行预取相关逻辑了,我们接着来看。

void prefetch(long deadlineNs) {
  	// 构建预取任务
    buildTaskList();
  	// 开始执行预取任务
    flushTasksWithDeadline(deadlineNs);
}

prefetch()方法中首先会调用buildTaskList()方法来构建预取任务,主要是通过此前传过来的水平和竖直方向位移确定出预取的位置,接下来会调用flushTasksWithDeadline()方法来执行预取任务,我们这里只看buildTaskList()方法就好。

private void buildTaskList() {
    final int viewCount = mRecyclerViews.size();
    int totalTaskCount = 0;
    for (int i = 0; i < viewCount; i++) {
        RecyclerView view = mRecyclerViews.get(i);
        if (view.getWindowVisibility() == View.VISIBLE) {
            // 关键代码
            view.mPrefetchRegistry.collectPrefetchPositionsFromView(view, false);
            totalTaskCount += view.mPrefetchRegistry.mCount;
        }
    }
    // ...
}

接下来又会调用RecyclerView中mPrefetchRegistry的collectPrefetchPositionsFromView()方法,mPrefetchRegistry的类型为LayoutPrefetchRegistryImpl,它是GapWorker中的一个内部类,我们接着来看它的collectPrefetchPositionsFromView()方法。

void collectPrefetchPositionsFromView(RecyclerView view, boolean nested) {
    mCount = 0;
    // ...
    final RecyclerView.LayoutManager layout = view.mLayout;
    if (view.mAdapter != null
            && layout != null
            && layout.isItemPrefetchEnabled()) {
        // ...
        // momentum based prefetch, only if we trust current child/adapter state
        if (!view.hasPendingAdapterUpdates()) {
            layout.collectAdjacentPrefetchPositions(mPrefetchDx, mPrefetchDy,
                    view.mState, this);
        }

        if (mCount > layout.mPrefetchMaxCountObserved) {
            layout.mPrefetchMaxCountObserved = mCount;
            layout.mPrefetchMaxObservedInInitialPrefetch = nested;
            view.mRecycler.updateViewCacheSize();
        }
    }
}

方法内部首先会将LayoutPrefetchRegistryImpl中的成员变量mCount置为0,接着通过isItemPrefetchEnabled()方法判断RecyclerView是否开启了预取,默认是开启的,接下来会执行layout的collectAdjacentPrefetchPositions()方法,这里的layout是RecyclerView设置的LayoutManager,我们以LinearLayoutManager为例,看一下它的collectAdjacentPrefetchPositions()方法。
LinearLayoutManager的collectAdjacentPrefetchPositions()方法

@Override
public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
                                             LayoutPrefetchRegistry layoutPrefetchRegistry) {
    // ...
    collectPrefetchPositionsForLayoutState(state, mLayoutState, layoutPrefetchRegistry);
}

void collectPrefetchPositionsForLayoutState(RecyclerView.State state, LayoutState layoutState,
                                            LayoutPrefetchRegistry layoutPrefetchRegistry) {
    // ...
    layoutPrefetchRegistry.addPosition(pos, Math.max(0, layoutState.mScrollingOffset));
}

方法内部又会调用collectPrefetchPositionsForLayoutState()方法,接着调用layoutPrefetchRegistry的addPosition()方法,这里的layoutPrefetchRegistry是从上面的collectPrefetchPositionsFromView()方法中传过来的,可以看到参数传的是this,也就是LayoutPrefetchRegistryImpl对象。我们接着来看LayoutPrefetchRegistryImpl的addPosition()方法:
LayoutPrefetchRegistryImpl的addPosition()方法

@Override
public void addPosition(int layoutPosition, int pixelDistance) {
    // ...
    mCount++;
}

可以看到方法最后会将mCount加1,此时mCount的值变为1。接下来我们回到collectPrefetchPositionsFromView()方法,来看方法最后执行的一个判断。

if (mCount > layout.mPrefetchMaxCountObserved) {
    layout.mPrefetchMaxCountObserved = mCount;
    layout.mPrefetchMaxObservedInInitialPrefetch = nested;
    view.mRecycler.updateViewCacheSize();
}

这里判断了mCount和mPrefetchMaxCountObserved的大小关系,mPrefetchMaxCountObserved是LayoutManager中定义的一个整型变量,初始值为0,因此这里会进入到if判断中。接着会将mCount赋值给mPrefetchMaxCountObserved,此时mPrefetchMaxCountObserved的值变为1,最后会调用Recycler的updateViewCacheSize()方法,我们来看一下这个方法。
Recycler的updateViewCacheSize()方法

void updateViewCacheSize() {
    int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0;
    mViewCacheMax = mRequestedCacheMax + extraCache;

    // first, try the views that can be recycled
    for (int i = mCachedViews.size() - 1;
         i >= 0 && mCachedViews.size() > mViewCacheMax; i--) {
        recycleCachedViewAt(i);
    }
}

方法内部首先定义了一个整型变量extraCache,字面上看就是额外的缓存,它的值就是上一步中的mPrefetchMaxCountObserved,也就是1。接下来这一步就重要了,将mRequestedCacheMax + extraCache赋值给mViewCacheMax,我们前面在介绍RecyclerView缓存的时候提到过mViewCacheMax表示mCachedViews的最大缓存数量,mRequestedCacheMax就是我们设置的mCachedViews缓存数量,默认值为2,因此此时mViewCacheMax的值被设置为3,也就是说mCachedViews最多可以保存3个ViewHolder(对于我们的场景来说就是Fragment)。
看到这里我们就大致清楚了示例中Fragment销毁情况产生的原因,当从第一个Fragment切换到第二个Fragment时会执行我们上面分析的预取逻辑,将mCachedViews的最大缓存数量由默认的2置为3。对于切换到第三、第四和第五个Fragment的情况,由于预取的Fragment占据了mCachedViews中的一个位置,因此还是表现为最多缓存2个Fragment。当切换到第六个也就是最后一个Fragment时,不需要再预取下一个Fragment了,但是此时mCachedViews的最大缓存数量依然为3,所以第三个Fragment也可以被添加到缓存中,不会被销毁。
为了验证得出的结论,我们首先通过代码取消ViewPager2内部RecyclerView的预取机制:

((RecyclerView) mViewPager2.getChildAt(0)).getLayoutManager().setItemPrefetchEnabled(false);

然后再来运行一下此前的示例程序,直接来看切换到最后一个Fragment的情况。
ViewPager2中的Fragment懒加载实现方式_第8张图片
可以看出当切换到最后一个Fragment时会销毁掉第三个Fragment,此时缓存的Fragment为第四和第五个,这是由于我们关闭了预取机制,在执行LayoutPrefetchRegistryImpl中的collectPrefetchPositionsFromView()方法时不满足layout.isItemPrefetchEnabled()为true的条件,不会执行后面的逻辑,因此mCachedViews的最大缓存数量始终为2,这就验证了我们的结论是没错的。

ViewPager2中的懒加载方案

由于ViewPager2默认情况下不会预加载出两边的Fragment,相当于默认就是懒加载的,因此如果我们如果没有通过setOffscreenPageLimit()方法设置预加载数量,完全可以不做任何额外处理。但是对于Fragment很多的情况,由于ViewPager2中的RecyclerView可以缓存Fragment的数量是有限的,因此会造成Fragment的多次销毁和创建,如何解决这个问题呢?下面就介绍一下我的解决方案。
首先设置ViewPager2的预加载数量,让ViewPager2预先创建出所有的Fragment,防止切换造成的频繁销毁和创建。

mViewPager2.setOffscreenPageLimit(mFragments.size());

通过此前示例中Fragment切换时生命周期方法的执行情况我们不难发现不管Fragment是否会被预先创建,只有可见时才会执行到onResume()方法,我们正好可以利用这一规律来实现懒加载,具体实现方式和我此前介绍过的androidx中的Fragment懒加载方案相同,这里我再简单说一下。

  • 将Fragment加载数据的逻辑放到onResume()方法中,这样就保证了Fragment可见时才会加载数据。
  • 声明一个变量标记是否是首次执行onResume()方法,因为每次Fragment由不可见变为可见都会执行onResume()方法,需要防止数据的重复加载。
    按照以上两点就可以封装我们的懒加载Fragment了,完整代码如下:
public abstract class LazyFragment extends Fragment {

    private Context mContext;
    private boolean isFirstLoad = true; // 是否第一次加载

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mContext = getActivity();
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = LayoutInflater.from(mContext).inflate(getContentViewId(), null);
        initView(view);
        return view;
    }

    @Override
    public void onResume() {
        super.onResume();
        if (isFirstLoad) {
            // 将数据加载逻辑放到onResume()方法中
            initData();
            initEvent();
            isFirstLoad = false;
        }
    }

    /**
     * 设置布局资源id
     *
     * @return
     */
    protected abstract int getContentViewId();

    /**
     * 初始化视图
     *
     * @param view
     */
    protected void initView(View view) {

    }

    /**
     * 初始化数据
     */
    protected void initData() {

    }

    /**
     * 初始化事件
     */
    protected void initEvent() {

    }
}

当然这只是我认为比较好的一种方案,如果有什么地方考虑得有问题或是大家有自己的见解都欢迎提出。

总结

本文探究了利用ViewPager2加载Fragment时生命周期方法的执行情况,进而得出ViewPager2懒加载的实现方式:
简单来说完全可以不做任何处理,ViewPager2默认就实现了懒加载。但是如果想避免Fragment频繁销毁和创建造成的开销,可以通过setOffscreenPageLimit()方法设置预加载数量,将数据加载逻辑放到Fragment的onResume()方法中。
虽说本文的研究对象是ViewPager2,但是文章大部分篇幅都是在分析RecyclerView,不得不感叹RecyclerView确实是一个很重要的控件,如何使用大家基本都已经烂熟于心了,但是涉及到原理上的东西就不一样了,我对RecyclerView的了解也是甚浅,有时间的话还是有必要深入学习一下的。

参考文章

ViewPager2重大更新,支持offscreenPageLimit
学不动也要学!深入了解ViewPager2
RecyclerView预加载机制源码分析

你可能感兴趣的:(Android)