RecyclerView的缓存机制

1、四级缓存

RecyclerView的缓存的工作主要由其内部类Recycler来完成的,代码如下:
Recycler

public final class Recycler {
        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
        ArrayList<ViewHolder> mChangedScrap = null;

        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

        private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
        int mViewCacheMax = DEFAULT_CACHE_SIZE;

        RecycledViewPool mRecyclerPool;

        private ViewCacheExtension mViewCacheExtension;

        static final int DEFAULT_CACHE_SIZE = 2;
        //省略。。。
}

RecycledViewPool

public static class RecycledViewPool {
        private static final int DEFAULT_MAX_SCRAP = 5;
        static class ScrapData {
            ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
            int mMaxScrap = DEFAULT_MAX_SCRAP;
            long mCreateRunningAverageNs = 0;
            long mBindRunningAverageNs = 0;
        }
        SparseArray<ScrapData> mScrap = new SparseArray<>();
        //省略。。。
}

从上面代码可知,RecyclerView不同于ListView,RecyclerView缓存的是ViewHolder,而ListView缓存的是View。
RecyclerView的缓存优先级从高到低如下:

  • 1、mAttachedScrap:缓存屏幕中可见范围中的ViewHolder。
  • 2、mCachedViews :缓存滑动中即将与RecyclerView分离的ViewHolder,默认最大为2个。
  • 3、mViewCacheExtension:自定义实现的缓存。
  • 4、mRecyclerPool:ViewHolder缓存池,可支持不同的ViewType。

1.1、mAttachedScrap

mAttachedScrap缓存的是当前屏幕上的ViewHolder,对应的数据结构是ArrayList,没有大小限制。在调用LayoutManager#onLayoutChildren方法时对views进行布局,此时会将RecyclerView上的ViewHolder全部暂存到该集合中。

RecyclerView需要做表项的动画,就需要做两次布局:预布局和后布局,分别记录动画的起始状态和终止状态。两次布局意味着
LayoutManager#onLayoutChildren就需要调用两次,为了防止子元素的重复添加以及重复的创建和数据绑定,RecyclerView在每次添加子元素前,先通过detachAndScrapAttachedViews(recycler)移除屏幕上的子元素并暂存在mAttachedScrap中,在下次布局时,直接从mAttachedScrap中取出并添加。

该缓存中ViewHolder的特性是:如果和RecyclerView上的position或者itemId匹配上了,那么就可以直接拿来使用,不需要调用onBindViewHolder重新绑定数据。

1.2、mCachedViews

mCachedViews缓存滑动时即将与RecyclerView分离的ViewHolder,其数据结构为ArrayList,该缓存对大小是有限制的,默认为2个。

该缓存中的ViewHolder的特性是:只要position和itemId匹配上了,则可直接使用,不需要调用onBindViewHolder重新绑定数据。
开发者可以调用setItemViewCacheSize(size)方法来改变缓存的大小,该层级缓存触发的一个常见的场景是滑动RecyclerView。

在滑动过程中,将与RecyclerView分离的ViewHolder存储在mCachedViews中,当mCachedViews的大小超过2个,就会按照FIFO从mCachedViews中移除并添加到RecycledViewPool中。

1.1.3、viewCacheExtension

ViewCacheExtension是需要开发者自己实现的缓存,一般不用。

1.1.4、RecyclerViewPool

ViewHolder缓存池,本质上是一个SparseArray,其中key是ViewType,value是ArrayList<ViewHolder>,默认每个ArrayList中最多存储5个。

ViewHolder存储在缓存池的前会进行重置变成一个干净的ViewHolder,所以在复用时,需要调用onBindViewHolder重绑数据。

2、复用流程

缓存的复用肯定是在填充子元素过程中完成的,而填充子元素的方法为LinearLayoutManager#fill()(这里以LinearLayoutManager为例。)

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    
    if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
        // TODO ugly bug fix. should not happen
        if (layoutState.mAvailable < 0) {
            layoutState.mScrollingOffset += layoutState.mAvailable;
        }
        //滑动时回收与RecyclerView分离的ViewHolder到mCachedViews、mRecyclerPool中
        recycleByLayoutState(recycler, layoutState);
    }
    int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
    //循环填充,直到没有空间
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        //...
        //填充子View
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
    //...
    return start - layoutState.mAvailable;
}

填充子View的逻辑在layoutChunk()中

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
    //获取一个合适的View
    View view = layoutState.next(recycler);
    //...
    LayoutParams params = (LayoutParams) view.getLayoutParams();
    if (layoutState.mScrapList == null) {
        if (mShouldReverseLayout == (layoutState.mLayoutDirection
                == LayoutState.LAYOUT_START)) {
            //添加View
            addView(view);
        } else {
            addView(view, 0);
        }
    } else {
        if (mShouldReverseLayout == (layoutState.mLayoutDirection
                == LayoutState.LAYOUT_START)) {
            addDisappearingView(view);
        } else {
            addDisappearingView(view, 0);
        }
    }
    //子View的测量
    measureChildWithMargins(view, 0, 0);
    //...
    //子View的布局
    layoutDecoratedWithMargins(view, left, top, right, bottom);
    //...
    result.mFocusable = view.hasFocusable();
}

在layoutChunk中通过next获取一个View

View next(RecyclerView.Recycler recycler) {
    if (mScrapList != null) {
        return nextViewFromScrapList();
    }
    final View view = recycler.getViewForPosition(mCurrentPosition);
    mCurrentPosition += mItemDirection;
    return view;
}

在next中调用Recycler的getViewForPosition方法获取View,最终会调用tryGetViewHolderForPositionByDeadline(),复用的主要逻辑就这个方法中。

    @Nullable
    ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
        ViewHolder holder = null;
        // 0) 如果它是改变的废弃的ViewHolder,在scrap的mChangedScrap找
        if (mState.isPreLayout()) {
            holder = getChangedScrapViewForPosition(position);
            fromScrapOrHiddenOrCache = holder != null;
        }
        // 1)根据position分别在scrap的mAttachedScrap、mChildHelper、mCachedViews中查找
        if (holder == null) {
            holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        }

        if (holder == null) {
            final int type = mAdapter.getItemViewType(offsetPosition);
            // 2)根据id在scrap的mAttachedScrap、mCachedViews中查找
            if (mAdapter.hasStableIds()) {
                holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
            }
            if (holder == null && mViewCacheExtension != null) {
                //3)在ViewCacheExtension中查找,一般不用到,所以没有缓存
                final View view = mViewCacheExtension
                        .getViewForPositionAndType(this, position, type);
                if (view != null) {
                    holder = getChildViewHolder(view);
                }
            }
            //4)在RecycledViewPool中查找
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                holder.resetInternal();
                if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
                }
            }
        }
        //5)到最后如果还没有找到复用的ViewHolder,则新建一个
        holder = mAdapter.createViewHolder(RecyclerView.this, type);
    }

可以看到,tryGetViewHolderForPositionByDeadline()方法分别去scrap、CacheView、ViewCacheExtension、RecycledViewPool中获取ViewHolder,如果没有则创建一个新的ViewHolder。

2.1、getChangedScrapViewForPosition

一般情况下,当我们调用adapter的notifyItemChanged()方法,数据发生变化时,item缓存在mChangedScrap和mAttachedScrap中,后续拿到的ViewHolder需要重新绑定数据。此时查找ViewHolder就会通过position和id分别在scrap的mChangedScrap中查找。

   ViewHolder getChangedScrapViewForPosition(int position) {
        //通过position
        for (int i = 0; i < changedScrapSize; i++) {
            final ViewHolder holder = mChangedScrap.get(i);
            return holder;
        }
        // 通过id
        if (mAdapter.hasStableIds()) {
            final long id = mAdapter.getItemId(offsetPosition);
            for (int i = 0; i < changedScrapSize; i++) {
                final ViewHolder holder = mChangedScrap.get(i);
                return holder;
            }
        }
        return null;
    }

2.2、getScrapOrHiddenOrCachedHolderForPosition

如果没有找到视图,根据position分别在scrap的mAttachedScrap、mHiddenViews、mCachedViews中查找,涉及的方法如下。

    ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
        final int scrapCount = mAttachedScrap.size();

        // 首先从mAttachedScrap中查找,精准匹配有效的ViewHolder
        for (int i = 0; i < scrapCount; i++) {
            final ViewHolder holder = mAttachedScrap.get(i);
            return holder;
        }
        //接着在mChildHelper中mHiddenViews查找隐藏的ViewHolder
        if (!dryRun) {
            View view = mChildHelper.findHiddenNonRemovedView(position);
            if (view != null) {
                final ViewHolder vh = getChildViewHolderInt(view);
                scrapView(view);
                return vh;
            }
        }
        //最后从我们的一级缓存中mCachedViews查找。
        final int cacheSize = mCachedViews.size();
        for (int i = 0; i < cacheSize; i++) {
            final ViewHolder holder = mCachedViews.get(i);
            return holder;
        }
    }

可以看到,getScrapOrHiddenOrCachedHolderForPosition查找ViewHolder的顺序如下:

  • 首先,从mAttachedScrap中查找,精准匹配有效的ViewHolder;
  • 接着,在mChildHelper中mHiddenViews查找隐藏的ViewHolder;
  • 最后,从一级缓存中mCachedViews查找。

2.3 getScrapOrCachedViewForId

getScrapOrCachedViewForId和getScrapOrHiddenOrCachedHolderForPosition的逻辑类似,只不过这里是以id进行查找的,而getScrapOrHiddenOrCachedHolderForPosition是以position进行查找的。

2.4 mViewCacheExtension

mViewCacheExtension是由开发者定义的一层缓存策略,Recycler并没有将任何view缓存到这里

2.5 RecycledViewPool

在ViewHolder的四级缓存中,我们有提到过RecycledViewPool,它是通过itemType把ViewHolder的List缓存到SparseArray中的,在getRecycledViewPool().getRecycledView(type)根据itemType从SparseArray获取ScrapData ,然后再从里面获取ArrayList<ViewHolder>,从而获取到ViewHolder。

    @Nullable
    public ViewHolder getRecycledView(int viewType) {
        final ScrapData scrapData = mScrap.get(viewType);//根据viewType获取对应的ScrapData 
        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;
    }

2.6 创建新的ViewHolder

如果还没有获取到ViewHolder,则通过mAdapter.createViewHolder()创建一个新的ViewHolder返回。

  // 如果还没有找到复用的ViewHolder,则新建一个
  holder = mAdapter.createViewHolder(RecyclerView.this, type);

下面是寻找ViewHolder的一个完整的流程图:


327484616-c1811ca79564e15c.png

3、回收流程

RecyclerView回收的入口有很多, 但是不管怎么样操作,RecyclerView 的回收或者复用必然涉及到add View 和 remove View 操作, 所以我们从onLayout的流程入手分析回收和复用的机制。

首先,在LinearLayoutManager中,我们来到itemView布局入口的方法onLayoutChildren(),如下所示。

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
            if (state.getItemCount() == 0) {
                removeAndRecycleAllViews(recycler);
                return;
            }
        }
        ensureLayoutState();
        mLayoutState.mRecycle = false;//禁止回收
        //颠倒绘制布局
        resolveShouldLayoutReverse();
        onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);

        //暂时分离已经附加的view,即将所有child detach并通过Scrap回收
        detachAndScrapAttachedViews(recycler);
    }

在onLayoutChildren布局时,会调用detachAndScrapAttachedViews()方法将屏幕上的item与RecyclerView进行分离并存储到缓存中,在重新布局时,再将ViewHolder重新一个一个放到新位置上去。(RecyclerView为了实现item动画,会进行两次onLayoutChildren的调用。)

将屏幕上的ViewHolder从RecyclerView的布局中拿下来后,存放在Scrap中,Scrap包括mAttachedScrap和mChangedScrap,它们是一个list,用来保存从RecyclerView布局中拿下来ViewHolder列表。

detachAndScrapAttachedViews()只会在onLayoutChildren()中调用,只有在布局的时候,才会把ViewHolder detach掉,然后再add进来重新布局。

但是大家需要注意:

  • Scrap只是保存从RecyclerView布局中当前屏幕显示的item的ViewHolder,不参与回收复用,单纯是为了现从RecyclerView中拿下来再重新布局上去。
  • 对于没有保存到的item,会放到mCachedViews或者RecycledViewPool缓存中参与回收复用。
   public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
        final int childCount = getChildCount();
        for (int i = childCount - 1; i >= 0; i--) {
            final View v = getChildAt(i);
            scrapOrRecycleView(recycler, i, v);
        }
    }

   private void scrapOrRecycleView(Recycler recycler, int index, View view) {
        final ViewHolder viewHolder = getChildViewHolderInt(view);
        if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                && !mRecyclerView.mAdapter.hasStableIds()) {
            removeViewAt(index);//移除VIew
            recycler.recycleViewHolderInternal(viewHolder);//缓存到CacheView或者RecycledViewPool中
        } else {
            detachViewAt(index);//分离View
            recycler.scrapView(view);//scrap缓存
            mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
        }
    }

然后,我们看detachViewAt()方法分离视图,再通过scrapView()缓存到scrap中。

    void scrapView(View view) {
        final ViewHolder holder = getChildViewHolderInt(view);
        if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
            holder.setScrapContainer(this, false);
            mAttachedScrap.add(holder);//保存到mAttachedScrap中
        } else {
            if (mChangedScrap == null) {
                mChangedScrap = new ArrayList<ViewHolder>();
            }
            holder.setScrapContainer(this, true);
            mChangedScrap.add(holder);//保存到mChangedScrap中
        }
    }

然后,我们回到scrapOrRecycleView()方法中,进入if()分支。如果viewHolder是无效、未被移除、未被标记的则放到recycleViewHolderInternal()缓存起来,同时removeViewAt()移除了viewHolder。
在调用notifyDataSetChange方法时,会将viewHolder都标记成无效的,故会进入该判断,将屏幕上的ViewHolder缓存在RecycledViewPool中。

   void recycleViewHolderInternal(ViewHolder holder) {
           ·····
        if (forceRecycle || holder.isRecyclable()) {
            if (mViewCacheMax > 0
                    && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                    | ViewHolder.FLAG_REMOVED
                    | ViewHolder.FLAG_UPDATE
                    | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {

                int cachedViewSize = mCachedViews.size();
                if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {//如果超出容量限制,把第一个移除
                    recycleCachedViewAt(0);
                    cachedViewSize--;
                }
                     ·····
                mCachedViews.add(targetCacheIndex, holder);//mCachedViews回收
                cached = true;
            }
            if (!cached) {
                addViewHolderToRecycledViewPool(holder, true);//放到RecycledViewPool回收
                recycled = true;
            }
        }
    }

如果符合条件,会优先缓存到mCachedViews中时,如果超出了mCachedViews的最大限制,通过recycleCachedViewAt()将CacheView缓存的第一个数据添加到终极回收池RecycledViewPool后再移除掉,最后才会add()新的ViewHolder添加到mCachedViews中。

剩下不符合条件的则通过addViewHolderToRecycledViewPool()缓存到RecycledViewPool中。

    void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
        clearNestedRecyclerViewIfNotNested(holder);
        View itemView = holder.itemView;
        ······
        holder.mOwnerRecyclerView = null;
        getRecycledViewPool().putRecycledView(holder);//将holder添加到RecycledViewPool中
    }

最后,在滑动过程中会调用填充布局调用fill()方法,它会回收移出屏幕的view到mCachedViews或者RecycledViewPool中。

 int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
              recycleByLayoutState(recycler, layoutState);//回收移出屏幕的view
        }
    }

而recycleByLayoutState()方法最终会调用recycleViewHolderInternal()就是用来回收移出屏幕的view,优先回到mCachedViews,当mCachedViews超过2个后,根据FIFO移除ViewHolder并添加到RecycledViewPool中。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容