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中删除掉,即被销毁