作者:快乐丸
链接:https://blog.csdn.net/qq_36486247/article/details/103959356
前言
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的依赖
1 | implementation 'androidx.viewpager2:viewpager2:1.0.0' |
第二步、在布局文件中添加ViewPager2
1 2 3 4 5 6 7 8 9 10 11 12 | <?xml version="1.0" encoding="utf-8"?> <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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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
1 2 3 4 5 6 7 | 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
可以看出此时只创建出了第一个Fragment,生命周期方法执行到了
-
切换到第二个Fragment
此时创建出了第二个Fragment,生命周期方法同样执行到
-
切换到第三个Fragment
和上一种情况相同,创建出第三个Fragment,执行到
-
切换到第四个Fragment
和前两种情况相同,同样是创建出当前Fragment,生命周期方法执行到
-
切换到第五个Fragment
和上一种情况相同,创建出第五个Fragment,生命周期方法执行到
-
切换到第六个(最后一个)Fragment
可以看出此时创建出了第六个Fragment,生命周期方法执行到
从以上几种情况下Fragment生命周期方法的执行情况来看,不难看出ViewPager2默认情况下不会预先创建出下一个Fragment。
但与此同时,Fragment的销毁情况就令我有些不解了,如果不看切换到最后一个Fragment的情况,我们可以猜测是由于ViewPager2内部RecyclerView的缓存机制导致最多可以存在三个Fragment,但是切换到最后一个Fragment的情况就违背了我们的猜测,很明显此时并没有销毁前面的Fragment。
接下来我们就根据上述结果来分析一下ViewPager2加载Fragment的几个问题。
ViewPager2中的setOffscreenPageLimit() 方法
通过示例中的执行结果我们可以发现ViewPager2默认情况下不会像ViewPager那样预先加载出两侧的Fragment,这是为什么呢,我们可能会想到ViewPager中预加载相关的一个方法:setOffscreenPageLimit(),ViewPager2中也定义了该方法,我们来看一下它们的区别。
首先来看ViewPager中的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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中的
1 2 3 4 5 6 7 8 9 10 11 12 | 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中的
1 2 3 4 5 6 7 8 9 10 11 12 13 | @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; } |
LinearLayoutManagerImpl重写了该方法,方法内部首先判断了mOffscreenPageLimit的值,如果等于默认值OFFSCREEN_PAGE_LIMIT_DEFAULT,则直接调用父类方法,不设置额外的布局空间;
如果mOffscreenPageLimit的值大于1,则设置左右(或上下)两边的额外空间为
看到这里我们就清楚了为什么默认情况下ViewPager2不会预加载出两侧的Fragment,就是因为默认的预加载数量为-1。和ViewPager一样,我们可以通过调用
在此前的示例中,我们添加下面的代码:
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的
回到我们的具体场景中,通过查看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。默认情况下预取功能是开启的,我们可以调用下面的代码来关闭:
1 | mRecyclerView.getLayoutManager().setItemPrefetchEnabled(false); |
那么预取机制会对ViewPager2中Fragment的销毁产生什么影响呢,我们从源码角度来简单分析一下。首先来看RecyclerView的
RecyclerView的onTouchEvent()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @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的
GapWorker的postFromTraversal()方法
1 2 3 4 5 6 7 8 | /** * Schedule a prefetch immediately after the current traversal. */ void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy) { // ... recyclerView.post(this); // ... } |
从方法的注释上我们也能看出它和RecyclerView的预取有关,方法内部会调用RecyclerView的
1 2 3 4 5 6 7 | @Override public void run() { // ... long nextFrameNs = TimeUnit.MILLISECONDS.toNanos(latestFrameVsyncMs) + mFrameIntervalNs; prefetch(nextFrameNs); // ... } |
方法内部会调用
1 2 3 4 5 6 | void prefetch(long deadlineNs) { // 构建预取任务 buildTaskList(); // 开始执行预取任务 flushTasksWithDeadline(deadlineNs); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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,接着通过
LinearLayoutManager的collectAdjacentPrefetchPositions()方法
1 2 3 4 5 6 7 8 9 10 11 12 | @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)); } |
方法内部又会调用
LayoutPrefetchRegistryImpl的addPosition()方法
1 2 3 4 5 | @Override public void addPosition(int layoutPosition, int pixelDistance) { // ... mCount++; } |
可以看到方法最后会将mCount加1,此时mCount的值变为1。接下来我们回到
1 2 3 4 5 | if (mCount > layout.mPrefetchMaxCountObserved) { layout.mPrefetchMaxCountObserved = mCount; layout.mPrefetchMaxObservedInInitialPrefetch = nested; view.mRecycler.updateViewCacheSize(); } |
这里判断了mCount和mPrefetchMaxCountObserved的大小关系,mPrefetchMaxCountObserved是LayoutManager中定义的一个整型变量,初始值为0,因此这里会进入到if判断中。
接着会将mCount赋值给mPrefetchMaxCountObserved,此时mPrefetchMaxCountObserved的值变为1,最后会调用Recycler的
Recycler的updateViewCacheSize()方法
1 2 3 4 5 6 7 8 9 10 | 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。
接下来这一步就重要了,将
看到这里我们就大致清楚了示例中Fragment销毁情况产生的原因,当从第一个Fragment切换到第二个Fragment时会执行我们上面分析的预取逻辑,将mCachedViews的最大缓存数量由默认的2置为3。
对于切换到第三、第四和第五个Fragment的情况,由于预取的Fragment占据了mCachedViews中的一个位置,因此还是表现为最多缓存2个Fragment。
当切换到第六个也就是最后一个Fragment时,不需要再预取下一个Fragment了,但是此时mCachedViews的最大缓存数量依然为3,所以第三个Fragment也可以被添加到缓存中,不会被销毁。
为了验证得出的结论,我们首先通过代码取消ViewPager2内部RecyclerView的预取机制:
1 | ((RecyclerView) mViewPager2.getChildAt(0)).getLayoutManager().setItemPrefetchEnabled(false); |
然后再来运行一下此前的示例程序,直接来看切换到最后一个Fragment的情况。
可以看出当切换到最后一个Fragment时会销毁掉第三个Fragment,此时缓存的Fragment为第四和第五个,这是由于我们关闭了预取机制,在执行LayoutPrefetchRegistryImpl中的
ViewPager2中的懒加载方案
由于ViewPager2默认情况下不会预加载出两边的Fragment,相当于默认就是懒加载的,因此如果我们如果没有通过
首先设置ViewPager2的预加载数量,让ViewPager2预先创建出所有的Fragment,防止切换造成的频繁销毁和创建。
1 | mViewPager2.setOffscreenPageLimit(mFragments.size()); |
通过此前示例中Fragment切换时生命周期方法的执行情况我们不难发现不管Fragment是否会被预先创建,只有可见时才会执行到
- 将Fragment加载数据的逻辑放到
onResume() 方法中,这样就保证了Fragment可见时才会加载数据。 - 声明一个变量标记是否是首次执行
onResume() 方法,因为每次Fragment由不可见变为可见都会执行onResume() 方法,需要防止数据的重复加载。
按照以上两点就可以封装我们的懒加载Fragment了,完整代码如下:
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | 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频繁销毁和创建造成的开销,可以通过
虽说本文的研究对象是ViewPager2,但是文章大部分篇幅都是在分析RecyclerView,不得不感叹RecyclerView确实是一个很重要的控件,如何使用大家基本都已经烂熟于心了,但是涉及到原理上的东西就不一样了,我对RecyclerView的了解也是甚浅,有时间的话还是有必要深入学习一下的。
点关注,获得更多Android开发技能~~