ViewPager2是基于RecyclerView实现的,因此我们首先要了解RecyclerView的缓存机制
RecyclerView的缓存复用
通常在RecyclerView中存在着四级缓存,从低到高分别为:
-
可直接重复使用的临时缓存(
mAttachedScrap
/mChangedScrap
)mAttachedScrap
中缓存的是屏幕中可见范围的ViewHoldermChangedScrap
只能在预布局状态下重用,因为它里面装的都是即将要放到mRecyclerPool
中的Holder
,而mAttachedScrap
则可以在非预布局状态下重用
-
可重用的缓存(
mCachedViews
):缓存滑动时即将与RecyclerView分离的ViewHolder,默认最大2个,另外如果RecyclerView增加了prefetch功能,即此时缓存池大小为2+prefetch个数,默认prefetch个数为1。所以默认开启prefetch功能后,mCachedViews
大小为3。 -
自定义实现的缓存(
ViewCacheExtension
):通常忽略; -
需要重新绑定数据的缓存(
RecycledViewPool
):ViewHolder缓存池,可以支持不同的ViewType,返回的ViewHolder需要重新Bind数据;
由于绝大多数情况下无需自定义缓存,因此通常我们说RecyclerView有三级缓存,具体可参考:juejin.cn/post/724118…
ViewPager2的页面预加载
要设置 ViewPager2 的页面预加载数量,可以使用 setOffscreenPageLimit()
方法。setOffscreenPageLimit()
方法用于设置 ViewPager2 在当前页面附近预加载的页面数量。
默认情况下,ViewPager2 的页面预加载数量为 1,即当前页面的左右各一个页面会被预加载。可以根据需要增加或减少预加载的页面数量。
以下是示例代码,展示了如何设置 ViewPager2 的页面预加载数量为 2:
val viewPager: ViewPager2 = findViewById(R.id.viewPager)
viewPager.offscreenPageLimit = 2
在上述示例中,我们通过 viewPager.offscreenPageLimit
属性将页面预加载数量设置为 2。这意味着在当前页面的左右各两个页面会被预加载。
我们来具体看一下setOffscreenPageLimit()
方法做了什么:
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();
}
上述方法的具体实现是将mOffscreenPageLimit
的值置为设置值,并调用mRecyclerView.requestLayout()
,从而出发layout
接下来,我们看mOffscreenPageLimit
在什么时候被使用呢?我们发现:
// androidx.viewpager2.widget.ViewPager2.LinearLayoutManagerImpl#calculateExtraLayoutSpace
@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
方法中会获取mOffscreenPageLimit
,并将其乘以屏幕宽度或高度(取决于页面滑动的方向)后赋值给extraLayoutSpace[0]
和extraLayoutSpace[1]
,calculateExtraLayoutSpace
方法是重写的LinearLayoutManager.calculateExtraLayoutSpace
,而该方法会在LinearLayoutManager中的onLayoutChildren方法中被使用:
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
calculateExtraLayoutSpace(state, mReusableIntPair);
int extraForStart = Math.max(0, mReusableIntPair[0])
+ mOrientationHelper.getStartAfterPadding();
int extraForEnd = Math.max(0, mReusableIntPair[1])
+ mOrientationHelper.getEndPadding();
...
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
detachAndScrapAttachedViews(recycler);
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
// 将offscreenSpace 赋值给mLayoutState.mExtraFillSpace,从而进行layout
mLayoutState.mExtraFillSpace = extraForStart;
fill(recycler, mLayoutState, state, false);
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForEnd;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
if (mLayoutState.mAvailable > 0) {
// end could not consume all. add more items towards start
extraForStart = mLayoutState.mAvailable;
updateLayoutStateToFillStart(firstElement, startOffset);
mLayoutState.mExtraFillSpace = extraForStart;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
}
} else {
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
final int lastElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForStart += mLayoutState.mAvailable;
}
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForStart;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
}
}
可以看到,在onLayoutChildren
中将offscreenSpace
赋值给mLayoutState.mExtraFillSpace
,接着调用fill
函数,进行布局,fill
函数的作用就是执行真正layout
的过程:
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
// max offset we should set is mFastScroll + available
final int start = layoutState.mAvailable;
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
// TODO ugly bug fix. should not happen
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
// 将布局位置扩大为屏幕宽度+ offscreenSpace
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
if (RecyclerView.VERBOSE_TRACING) {
TraceCompat.beginSection("LLM LayoutChunk");
}
layoutChunk(recycler, state, layoutState, layoutChunkResult);
...
}
...
}
可以看到,整个布局的区域被扩大了offscreenSpace
, 而相同的过程在另外一个方向也会进行一次,因此对于ViewPager2来说,整个区域被扩大了两倍(offScreenLimit
按默认值1来计算):
FragmentStateAdapter的原理
FragmentStateAdapter 是用于 ViewPager2 的适配器,负责管理 ViewPager2 的页面和数据。
-
FragmentStateAdapter
是基于RecyclerView.Adapter
的设计,它通过创建和绑定Fragment
来实现ViewPager2
的页面管理。 -
FragmentStateAdapter
继承自RecyclerView.Adapter
,重写了一些方法,如onCreateViewHolder()
、onBindViewHolder()
和getItemCount()
,用于创建和绑定Fragment
,并返回页面数量。
onCreateViewHolder
我们先来看一下onCreateViewHolder()
方法:
@NonNull
@Override
public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return FragmentViewHolder.create(parent);
}
其直接返回FragmentViewHolder.create(parent)
:
@NonNull static FragmentViewHolder create(@NonNull ViewGroup parent) {
FrameLayout container = new FrameLayout(parent.getContext());
container.setLayoutParams(
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
container.setId(ViewCompat.generateViewId());
container.setSaveEnabled(false);
return new FragmentViewHolder(container);
}
可以看到,FragmentViewHolder.create
方法中,新建了一个FrameLayout
,并设置了viewId
,从这里也可以知道,FragmentViewHolder
直接绑定的其实是FrameLayout
onBindViewHolder
接着我们来看一下onBindViewHolder
干了些什么:
@Override
public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) {
final long itemId = holder.getItemId();
final int viewHolderId = holder.getContainer().getId();
final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH
if (boundItemId != null && boundItemId != itemId) {
removeFragment(boundItemId);
mItemIdToViewHolder.remove(boundItemId);
}
mItemIdToViewHolder.put(itemId, viewHolderId); // this might overwrite an existing entry
ensureFragment(position);
/** Special case when {@link RecyclerView} decides to keep the {@link container}
* attached to the window, but not to the view hierarchy (i.e. parent is null) */
final FrameLayout container = holder.getContainer();
if (ViewCompat.isAttachedToWindow(container)) {
if (container.getParent() != null) {
throw new IllegalStateException("Design assumption violated.");
}
container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (container.getParent() != null) {
container.removeOnLayoutChangeListener(this);
placeFragmentInViewHolder(holder);
}
}
});
}
gcFragments();
}
该方法的作用是将数据绑定到 ViewHolder,并确保对应的 Fragment 正确地显示在 ViewHolder 中。它还处理了特殊情况下的容器视图布局改变,并执行了对 Fragment 的回收操作,具体步骤包括
-
首先,获取当前
ViewHolder
的itemId
和viewHolderId
。itemId
是用于唯一标识ViewHolder
的值 (在FragmentStateAdapter
中itemId == position
),而viewHolderId
是ViewHolder
的容器视图的id
,也就是在创建ViewHolder
时,FrameLayout
的id
-
接下来,通过
itemForViewHolder(viewHolderId)
方法获取当前绑定到该ViewHolder
上的itemId
。如果已经存在绑定的itemId
,并且当前itemId
不同于之前绑定的itemId
,就需要移除之前绑定的Fragment
,并从映射表中移除之前的itemId
对应的ViewHolder
。 -
将当前的
itemId
和viewHolderId
存入映射表mItemIdToViewHolder
,以便之后能够根据itemId
找到对应的ViewHolder
。 -
调用
ensureFragment(position)
方法,确保当前位置对应的Fragment
已经存在。如果对应位置已经有Fragment则直接返回,没有则会新建 -
在合适的时机将Fragment展示在ViewPager2上
-
最后,调用
gcFragments()
方法,执行对 Fragment 的回收操作,清理不再使用的 Fragment。
这里提到了映射表 mItemIdToViewHolder
,其实还存在另外一个映射表mFragments
mItemIdToViewHolder与mFragments
在 FragmentStateAdapter
中,mFragments
和 mItemIdToViewHolder
是两个关键的数据结构,用于管理 Fragment
和 ViewHolder
的映射关系。
-
mFragments
: Adapter的数据源, 这是一个LongSparseArray<Fragment>
类型的数据结构,用于保存当前已创建的 Fragment 实例。它以页面的位置(索引)为键,对应的 Fragment 实例为值。通过mFragments
,FragmentStateAdapter
能够快速获取指定位置的 Fragment,避免重复创建和销毁 Fragment 实例。 -
mItemIdToViewHolder
: 这是一个 LongSparseArray 类型的数据结构,用于保存ViewHolder
的itemId
和viewHolderId
的映射关系。它以ViewHolder
的itemId
为键,对应的viewHolderId
(ViewHolder 的容器视图的 ID)为值。通过 mItemIdToViewHolder,FragmentStateAdapter 能够根据 itemId 查找到对应的 ViewHolder,用于更新和绑定数据到正确的 ViewHolder 上。
这两个数据结构在 FragmentStateAdapter 中的作用如下:
-
当 ViewPager2 需要显示特定位置的页面时,FragmentStateAdapter 会首先检查 mFragments 中是否已存在对应位置的 Fragment 实例,如果存在则直接使用。这样可以避免重复创建 Fragment,提高性能和内存管理效率。是Adapter的数据源
-
当数据发生变化时,例如页面位置改变或数据项更新,
FragmentStateAdapter
使用mItemIdToViewHolder
来查找与viewHolderId
相关联的itemId
,然后根据itemId
在mFragments
中找到对应的Fragment
实例,并将新的数据绑定到正确的ViewHolder
上。
综上所述,mFragments
和 mItemIdToViewHolder
在 FragmentStateAdapter
中起到了管理 Fragment
实例和 ViewHolder
的映射关系的作用,以提供正确的数据绑定和页面展示。
缓存复用机制
FragmentStateAdapter
是基于RecyclerView.Adapter
的实现,整体的复用机制仍然是RecyclerView缓存复用那一套,而用户定义的Fragment是作为Adapter的数据源,而非View, 所以当 ViewPager2 页面切换时,FragmentStateAdapter 会根据 offscreenPageLimit
的设置来决定预加载和缓存的 Fragment
数量。超出预加载和缓存范围的 Fragment 会被销毁,只保留最近使用的 Fragment 实例。
示例
我们将offScreenPageLimit
设为1 ,因此ViewPager2
一下子能展示3屏Fragment,左右各显示一屏,即屏幕被“扩大了两倍”
-
初始化时,Fragment1左边没有数据,屏幕只有1和2,由于用户没有操作,预取策略不生效
-
往右滑到2时,1、2、3显示在屏幕上,同时预取 4放入
mCachedViews
中 -
往右滑到3时,2、3、4显示在屏幕上,1 放入
mCachedViews
中,同时预取5 到mCachedViews
中 -
往右滑到4时,3、4、5显示在屏幕上,1、2、6 放入
mCachedViews
中; -
往右滑到5时,4、5、6显示在屏幕上,2、3、7放入mViewCaches,1被回收到
mRecyclerPool
缓存池中。与此同时,Fragment1
从mFragments
中删除掉,即被销毁