ViewPager2系列–ViewPager2的预加载与缓存复用机制

ViewPager2是基于RecyclerView实现的,因此我们首先要了解RecyclerView的缓存机制

RecyclerView的缓存复用

通常在RecyclerView中存在着四级缓存,从低到高分别为:

  • 可直接重复使用的临时缓存(mAttachedScrap/mChangedScrap

    • mAttachedScrap中缓存的是屏幕中可见范围的ViewHolder
    • mChangedScrap只能在预布局状态下重用,因为它里面装的都是即将要放到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来计算):

image.png

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 的回收操作,具体步骤包括

  1. 首先,获取当前 ViewHolderitemIdviewHolderIditemId 是用于唯一标识 ViewHolder 的值 (在FragmentStateAdapteritemId == position),而 viewHolderIdViewHolder 的容器视图的 id,也就是在创建ViewHolder时,FrameLayoutid

  2. 接下来,通过 itemForViewHolder(viewHolderId) 方法获取当前绑定到该 ViewHolder 上的 itemId。如果已经存在绑定的 itemId,并且当前 itemId 不同于之前绑定的 itemId,就需要移除之前绑定的 Fragment,并从映射表中移除之前的 itemId 对应的 ViewHolder

  3. 将当前的 itemIdviewHolderId 存入映射表 mItemIdToViewHolder,以便之后能够根据 itemId 找到对应的 ViewHolder

  4. 调用 ensureFragment(position) 方法,确保当前位置对应的 Fragment 已经存在。如果对应位置已经有Fragment则直接返回,没有则会新建

  5. 在合适的时机将Fragment展示在ViewPager2上

  6. 最后,调用 gcFragments() 方法,执行对 Fragment 的回收操作,清理不再使用的 Fragment。

这里提到了映射表 mItemIdToViewHolder ,其实还存在另外一个映射表mFragments

mItemIdToViewHolder与mFragments

FragmentStateAdapter 中,mFragmentsmItemIdToViewHolder 是两个关键的数据结构,用于管理 FragmentViewHolder 的映射关系。

  1. mFragments: Adapter的数据源, 这是一个 LongSparseArray<Fragment> 类型的数据结构,用于保存当前已创建的 Fragment 实例。它以页面的位置(索引)为键,对应的 Fragment 实例为值。通过 mFragmentsFragmentStateAdapter 能够快速获取指定位置的 Fragment,避免重复创建和销毁 Fragment 实例。

  2. mItemIdToViewHolder: 这是一个 LongSparseArray 类型的数据结构,用于保存 ViewHolderitemIdviewHolderId 的映射关系。它以 ViewHolderitemId 为键,对应的 viewHolderId(ViewHolder 的容器视图的 ID)为值。通过 mItemIdToViewHolder,FragmentStateAdapter 能够根据 itemId 查找到对应的 ViewHolder,用于更新和绑定数据到正确的 ViewHolder 上。

这两个数据结构在 FragmentStateAdapter 中的作用如下:

  • 当 ViewPager2 需要显示特定位置的页面时,FragmentStateAdapter 会首先检查 mFragments 中是否已存在对应位置的 Fragment 实例,如果存在则直接使用。这样可以避免重复创建 Fragment,提高性能和内存管理效率。是Adapter的数据源

  • 当数据发生变化时,例如页面位置改变或数据项更新,FragmentStateAdapter 使用 mItemIdToViewHolder 来查找与 viewHolderId 相关联的 itemId,然后根据 itemIdmFragments 中找到对应的 Fragment 实例,并将新的数据绑定到正确的 ViewHolder 上。

综上所述,mFragmentsmItemIdToViewHolderFragmentStateAdapter 中起到了管理 Fragment 实例和 ViewHolder 的映射关系的作用,以提供正确的数据绑定和页面展示。

缓存复用机制

FragmentStateAdapter 是基于RecyclerView.Adapter 的实现,整体的复用机制仍然是RecyclerView缓存复用那一套,而用户定义的Fragment是作为Adapter的数据源,而非View, 所以当 ViewPager2 页面切换时,FragmentStateAdapter 会根据 offscreenPageLimit 的设置来决定预加载和缓存的 Fragment 数量。超出预加载和缓存范围的 Fragment 会被销毁,只保留最近使用的 Fragment 实例。

示例

我们将offScreenPageLimit 设为1 ,因此ViewPager2一下子能展示3屏Fragment,左右各显示一屏,即屏幕被“扩大了两倍”

image.png

  1. 初始化时,Fragment1左边没有数据,屏幕只有1和2,由于用户没有操作,预取策略不生效

  2. 往右滑到2时,1、2、3显示在屏幕上,同时预取 4放入mCachedViews

  3. 往右滑到3时,2、3、4显示在屏幕上,1 放入mCachedViews中,同时预取5 到mCachedViews

  4. 往右滑到4时,3、4、5显示在屏幕上,1、2、6 放入mCachedViews中;

  5. 往右滑到5时,4、5、6显示在屏幕上,2、3、7放入mViewCaches,1被回收到mRecyclerPool缓存池中。与此同时,Fragment1mFragments中删除掉,即被销毁

参考文档

juejin.cn/post/724118…

juejin.cn/post/702542…

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYDQOvXX' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片