前言
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
<?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。
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
可以看出此时只创建出了第一个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 < 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,和ViewPager默认的情况相同。
RecyclerView中的缓存和预取机制
接下来我们来看一下Fragment的销毁情况,探究一下为什么在上面的示例中ViewPager2切换到最后一个Fragment时没有销毁前面的Fragment。在此之前,我们先要了解一下RecyclerView的缓存机制和预取机制。
RecyclerView的缓存机制算是老生常谈的问题了,核心在它的一个内部类Recycler中,Item的回收和复用相关工作都是Recycler来进行的,RecyclerView的缓存可以分为多级,由于我了解得非常浅显,这里就不详细介绍了,大家可以自行查看相关文章。我们直接来看和ViewPager2中Fragment回收相关的缓存——mCachedViews,它的类型是ArrayList<ViewHolder>,移出屏幕的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的情况。
可以看出当切换到最后一个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预加载机制源码分析