ViewPager2中的Fragment懒加载实现方式

您所在的位置:网站首页 如何实现页面懒加载 ViewPager2中的Fragment懒加载实现方式

ViewPager2中的Fragment懒加载实现方式

2024-02-05 21:55| 来源: 网络整理| 查看: 265

前言 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

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

public class MyFragmentPagerAdapter extends FragmentStateAdapter { private List mFragments; public MyFragmentPagerAdapter(@NonNull FragmentActivity fragmentActivity, List 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 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

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

切换到第二个Fragment

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

切换到第三个Fragment

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

切换到第四个Fragment

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

切换到第五个Fragment

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

切换到第六个(最后一个)Fragment

可以看出此时创建出了第六个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 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 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,和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 // 关键代码 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的情况。 可以看出当切换到最后一个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预加载机制源码分析



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3