前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >RecyclerView缓存详解

RecyclerView缓存详解

作者头像
Anymarvel
发布2021-12-08 08:06:48
7650
发布2021-12-08 08:06:48
举报
文章被收录于专栏:Android开发实战Android开发实战

一、RecyclerView基本结构

RecyclerView的运行主要依赖于Adapter、LayoutManager和Recycler这三个类,其中Adapter负责与数据集交互,LayoutManager负责ItemView的布局,Recycler负责管理ViewHolder,其结构如下图。

得益于RecyclerView设计时的解耦,ItemView的创建、绑定和复用对LayoutManager来说都是不可见的,LayoutManager只需要关心如何布局ItemView即可。当LayoutManager要布局数据集中的第i个Item时,它通过recycler.getViewForPosition(i)获取该ItemView,此时Recycler会先查找缓存中是否存在该ItemView,如果不存在就调用Adapter的onCreateViewHolder(…)新建一个。

而Recycler中缓存的ViewHolder也是LayoutManager放进去的,那LayoutManager什么时候将ItemView放入缓存中呢?主要分为两种情况。 ① 数据集发生变化。此时LayoutManager调用detachAndScrapAttachedViews()回收当前屏幕上的所有ItemView,再根据新的数据进行布局。 ② ItemView滑出可视区域。此时LayoutManager会将该ItemView放入Recycler的缓存中。缓存为FIFO结构,当有新的ItemView被放入缓存时,旧的ItemView会被移出。

二、回收复用机制原理

Recycler中有多个缓存池,其定义如下。

代码语言:javascript
复制
// mAttachedScrap在重新layout时使用
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
// mChangedScrap用于动画
ArrayList<ViewHolder> mChangedScrap = null;
// mCachedViews和RecycledViewPool用于滑动时的缓存
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
RecycledViewPool mRecyclerPool;
// 用户自定义缓存,一般不用
private ViewCacheExtension mViewCacheExtension;

当ItemView因为不同的原因被回收时,它们也会进入不同的缓存池,最常见的场景就是数据集发生变化或Item滑出可视区域,下面根据Item回收的场景来看各个缓存池的使用。

场景1—数据集发生变化

mAttachedScrap被称为一级缓存,在重新layout时使用,主要是数据集发生变化的场景。被mAttachedScrap缓存的ItemView大部分会马上得到复用。当LayoutManager通过recycler.getViewForPosition(i)寻找ItemView时会优先去mAttachedScrap中查找。

当数据集发生变化时,LayoutManager的onLayoutChildren(…)方法会被调用,该方法先通过detachAndScrapAttachedViews(Recycler recycler)将当前屏幕上的所有ItemView缓存至mAttachedScrap,之后再重新布局。

举个栗子,假设初始有5个Item,remove掉Data1,此时数据集发生了变化,需要重新布局。LayoutManager的onLayoutChildren(…)方法被调用,初始的5个ItemView都被添加到了mAttachedScrap中,随后重新布局时,有4个ItemView得到了复用。

之前提到,Recycler以ItemView在数据集中的position作为唯一定位,当需要展示数据集中第i项时,LayoutManager通过recycler.getViewForPosition(i)获取对应的ItemView。来看一下Recycler在mAttachedScrap中查找时,是怎么判断缓存中的ItemView是否就是当前所需要的。

代码语言:javascript
复制
    final int scrapCount = mAttachedScrap.size();
    for (int i = 0; i < scrapCount; i++) {
        final ViewHolder holder = mAttachedScrap.get(i);
        if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
                && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
            holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
            return holder;
        }
    }

主要的判断条件就是holder.getLayoutPosition() == position,用于判断ViewHolder Layout时的位置是否与数据集中的position相等。ViewHolder的getLayoutPosition()方法如下。

代码语言:javascript
复制
public final int getLayoutPosition() {
    return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition;
}

这个有个要注意的地方:数据集发生变化后,ViewHolder原本的LayoutPosition很有可能过期,就像上面例子中,Remove掉Data1后,Data2的LayoutPosition应该从2变为1,所以我们断定,RecyclerView一定在某个地方对ViewHolder的位置信息进行了更新,我们来看下RecyclerView是怎么做的。

以上面的示例作为说明,将Data1从数据集移除后,RecyclerView开始重新布局,在dispatchLayoutStep1()中的processAdapterUpdatesAndSetAnimationFlags()方法中更新ViewHolder的position信息。假设没有设置动画,则执行mAdapterHelper.consumeUpdatesInOnePass()。

代码语言:javascript
复制
private void processAdapterUpdatesAndSetAnimationFlags() {
    // ......
    // simple animations are a subset of advanced animations (which will cause a
    // pre-layout step)
    // If layout supports predictive animations, pre-process to decide if we want to run them
    if (predictiveItemAnimationsEnabled()) {
        mAdapterHelper.preProcess();
    } else {
        mAdapterHelper.consumeUpdatesInOnePass();
    }
    // ......
}

mAdapterHelper.consumeUpdatesInOnePass()中判断数据集发生的变化,通过AdapterHelper.Callback回调通知RecyclerView遍历ViewHolder更新position

代码语言:javascript
复制
void consumeUpdatesInOnePass() {
    // ......
    for (int i = 0; i < count; i++) {
        UpdateOp op = mPendingUpdates.get(i);
        switch (op.cmd) {
            case UpdateOp.ADD:
                mCallback.onDispatchSecondPass(op);
                mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
                break;
            case UpdateOp.REMOVE:
                mCallback.onDispatchSecondPass(op);
                mCallback.offsetPositionsForRemovingInvisible(op.positionStart, op.itemCount);
                break;
            case UpdateOp.UPDATE:
                mCallback.onDispatchSecondPass(op);
                mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
                break;
            case UpdateOp.MOVE:
                mCallback.onDispatchSecondPass(op);
                mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
                break;
        }
        if (mOnItemProcessedCallback != null) {
            mOnItemProcessedCallback.run();
        }
    }
    recycleUpdateOpsAndClearList(mPendingUpdates);
    mExistingUpdateTypes = 0;
}

经过一连串的方法调用,最终走到了ViewHolder的offsetPosition(…)方法,如下所示。在我们的示例中,offset参数就是-1。

代码语言:javascript
复制
void offsetPosition(int offset, boolean applyToPreLayout) {
    if (mOldPosition == NO_POSITION) {
        mOldPosition = mPosition;
    }
    if (mPreLayoutPosition == NO_POSITION) {
        mPreLayoutPosition = mPosition;
    }
    if (applyToPreLayout) {
        mPreLayoutPosition += offset;
    }
    mPosition += offset;
    if (itemView.getLayoutParams() != null) {
        ((LayoutParams) itemView.getLayoutParams()).mInsetsDirty = true;
    }
}

dispatchLayoutStep1()最后还会调用clearOldPositions()将ViewHolder中的mOldPosition和mPreLayoutPosition都重置为-1,则getLayoutPosition()方法最终返回的就是ViewHolder中的mPosition变量。

代码语言:javascript
复制
void clearOldPosition() {
    mOldPosition = NO_POSITION;
    mPreLayoutPosition = NO_POSITION;
}

一般情况下,重新onLayout()之后,mAttachedScrap中大部分的ItemView都会得到重用;而得不到复用的ItemView会被放入RecyclerPool中,用于滑动时复用。

场景2—Item滑出可视区域

RecyclerView滑动时使用到的缓存池为mCachedViews和RecycledViewPool。当一个Item滑出可视区域时会被放入缓存;而当某个ItemView滑进可视区域时,会去缓存中查找是否有可用的ViewHolder,没有则新建一个。

mCachedViews的结构比较简单,是一个ArrayList,默认大小为2,其特性与mAttachedScrap类似,缓存到mCachedViews中的Item可以直接复用,效率较高。

当mCachedViews达到上限后,之后的ViewHolder会被缓存至mRecyclerPool,它根据ViewHolder的viewType缓存不同类型的ViewHolder,每个viewType的默认缓存上限为5,RecycledViewPool定义如下。

代码语言:javascript
复制
public static class RecycledViewPool {
    private static final int DEFAULT_MAX_SCRAP = 5;

    static class ScrapData {
        final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
        int mMaxScrap = DEFAULT_MAX_SCRAP;
        long mCreateRunningAverageNs = 0;
        long mBindRunningAverageNs = 0;
    }
    SparseArray<ScrapData> mScrap = new SparseArray<>();
    ......
}

从mRecyclerPool取出的ViewHolder会被重置,因此从mRecyclerPool查找缓存时只需要viewType相等即可,最终会调用Adapter的onBindViewHolder()重新绑定数据。

举个栗子,如图所示。RecyclerView滑动时,刚开始的时候回收了Position0和Position1,它们被添加到了mCachedViews中。随后回收Position2时,达到数量上限,最先进入mCachedViews的Position0被放进了mRecyclerPool中。 再看下方进入可视区域的3个Item,最初的Position6和Position7找不到对应的缓存,只能新建ViewHolder并绑定。当Position8滑入可视区域时,发现mRecyclerPool中有一个ViewType相等的缓存,则将其取出并绑定数据进行复用。

之前提到,LayoutManager通过recycler.getViewForPosition(i)寻找对应位置的ItemView,而Recycler有多个缓存池(mAttachedScrap, mCachedViews…),那么遍历缓存池的顺序又是怎样的呢?我们从代码的角度来分析下。

代码语言:javascript
复制
    public View getViewForPosition(int position) {
        return getViewForPosition(position, false);
    }

    View getViewForPosition(int position, boolean dryRun) {
        return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
    }

关注一下dryRun参数,一般来说,我们从缓存中取出某个ItemView时,会将它从缓存中移除,但是参数dryRun为true时,取出的ViewHolder不会从缓存中移除。 getViewForPosition(int position)直接传了false。而且整个RecyclerView中我没找到dryRun传true的地方,我猜测这个参数应该是给开发人员用于特殊场景的。

言归正传,getViewForPosition(…)最终调用了tryGetViewHolderForPositionByDeadline(…),参数中的FOREVER_NS表示对这个方法的运行时间没有限制,不管花多长时间,这个方法都要返回一个绑定好数据的ItemView,来看精简后的代码。

代码语言:javascript
复制
    ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
        // ......
        boolean fromScrapOrHiddenOrCache = false;
        ViewHolder holder = null;
        // 0) 从mChangedScrap中查找
        if (mState.isPreLayout()) {
            holder = getChangedScrapViewForPosition(position);
            fromScrapOrHiddenOrCache = holder != null;
        }
        // 1) 如果还没找到,依次去mAttachedScrap和mCachedViews中查找
        if (holder == null) {
            holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
            if (holder != null) {
                // 判断该ViewHolder是否有效,如果无效则置空
            }
        }
        if (holder == null) {
            final int offsetPosition = mAdapterHelper.findPositionOffset(position);
            // ......
            final int type = mAdapter.getItemViewType(offsetPosition);
            // 2) 如果存在stableId,依次去mAttachedScrap和mCachedViews中查找
            if (mAdapter.hasStableIds()) {
                holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                        type, dryRun);
                if (holder != null) {
                    // update position
                    holder.mPosition = offsetPosition;
                    fromScrapOrHiddenOrCache = true;
                }
            }
            if (holder == null && mViewCacheExtension != null) {
                // 3) 如果自定义缓存mViewCacheExtension不为空,就去其中查找
            }
            // 4) 之前的缓存中都查找不到,去RecycledViewPool中查找
            if (holder == null) {
                holder = getRecycledViewPool().getRecycledView(type);
                if (holder != null) {
                    // 将ViewHolder重置
                    holder.resetInternal();
                    if (FORCE_INVALIDATE_DISPLAY_LIST) {
                        invalidateDisplayListInt(holder);
                    }
                }
            }
            if (holder == null) {
                // 5) 所有的缓存中都查找不到,调用createViewHolder新建一个
                holder = mAdapter.createViewHolder(RecyclerView.this, type);
        }

        // ......

        boolean bound = false;
        if (mState.isPreLayout() && holder.isBound()) {
            // do not update unless we absolutely have to.
            holder.mPreLayoutPosition = position;
        } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
            // 如果ViewHolder需要重新绑定,则调用tryBindViewHolderByDeadline()绑定数据
            final int offsetPosition = mAdapterHelper.findPositionOffset(position);
            bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
        }

        // ......
        return holder;
    }

在不考虑动画和stableId的情况下,查找缓存的顺序为:mAttachedScrap->mCachedViews->RecycledViewPool,从getScrapOrHiddenOrCachedHolderForPosition(…)方法中我们可以发现,当从mAttachedScrap和mCachedViews中查找ViewHolder时,主要的判断条件都是holder.getLayoutPosition() == position,得到的ViewHolder不用重新绑定。

代码语言:javascript
复制
    ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
        final int scrapCount = mAttachedScrap.size();
        // Try first for an exact, non-invalid match from scrap.
        for (int i = 0; i < scrapCount; i++) {
            final ViewHolder holder = mAttachedScrap.get(i);
            if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
                    && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
                holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                return holder;
            }
        }
        // ......

        // Search in our first-level recycled view cache.
        final int cacheSize = mCachedViews.size();
        for (int i = 0; i < cacheSize; i++) {
            final ViewHolder holder = mCachedViews.get(i);
            if (!holder.isInvalid() && holder.getLayoutPosition() == position
                    && !holder.isAttachedToTransitionOverlay()) {
                return holder;
            }
        }
        return null;
    }

当从RecyclerPool中查找缓存时调用了RecyclerPool的getRecycledView(int viewType)方法,只要ViewType相等即可。

代码语言:javascript
复制
    public ViewHolder getRecycledView(int viewType) {
        final ScrapData scrapData = mScrap.get(viewType);
        if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
            final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
            for (int i = scrapHeap.size() - 1; i >= 0; i--) {
                if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
                    return scrapHeap.remove(i);
                }
            }
        }
        return null;
    }

从RecyclerPool中得到ViewHolder后再调用holder.resetInternal()重置,tryGetViewHolderForPositionByDeadline(…)方法最后会对未绑定的ViewHolder重新绑定数据。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-12-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 喘口仙氣 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、RecyclerView基本结构
  • 二、回收复用机制原理
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档