RecyclerView缓存机制

733 阅读10分钟

一,前言

本文主要讨论RecyclerView的缓存机制,从缓存本身的概念与意义出发,再讨论RecyclerView滑动时是如何回收,复用ItemView的。复用的ItemView被如何存储的,RecyclerView中各个缓存层次的作用。

二,缓存的意义

(1)概述

缓存的核心概念是:空间换时间。因为某样数据获取 或 创建开销巨大。再高频率使用下每次获取最新的数据非常耗时,会对用户交互产生影响。所以在第一次获取数据后,通过某种手段把数据存储到一个更快速,方便获取的位置。

比如:用户信息在前端属于高频率使用的数据,经常会用到展示头像,用户名之类的信息。 调用接口的时候也可能传入用户标识用于验证身份。

如果不对用户信息进行缓存,每次都从服务端获取用户信息,先不说代码编写的复杂程度,受网络波动影响每次获取用户信息的时间不定,可能很慢也可能很快,或者干脆直接请求超时。

所以习惯上把用户信息缓存到本地数据库,每次进入应用拉去最新数据。如果用户应用中进行用户名,头像之类的修改,在接口调用成功后更新指定的缓存。图方便也可以直接拉取服务端的最新数据。

(2)本地缓存

(a)把数据以文件,数据库的形式存储的到硬盘中。

(b)与从网络获取数据相比,本地缓存只要文件存在就一定能获取到数据,获取时会存在一定的IO开销,数据量不大的情况影响不大,移动端几乎不会产生超大数据的本地缓存。

(c)本地缓存需要考虑数据有效性的问题,如果缓存的网络数据,要考虑更新策略。

(3)内存缓存

(a)数据存储在内存中,常见的JavaBean对象,List集合,都可以说是在内存中存储数据。

(b)内存缓存要注意数据生命周期问题,比如:UserBean保存在UserActivity中,当UserActivity关闭后UserBean就会被JVM回收,其他页面无法访问。

(c)内存缓存在使用时,一定要考虑数据的作用域,用户信息的作用域很明显不局限于一个页面。一般会结合静态变量,单例模式使用,使得可以全局应用。

(d)还有一点要注意内存缓存的存储空间,以强引用存储数据,JVM是不会回收的,不设置缓存阈值,内存会被撑爆的 。Android推荐使用 LruCache 实现内存缓存

本地缓存与内存缓存是不冲突的,两者可以结合使用。第一次进入应用从本地缓存读取数据,保存到内存中。之后每次从内存中获取数据,如果内存数据为空,则再次从本地读取数据,如果本地也没有数据,则从网络获取数据。

(4)小结

缓存的本质是以空间换时间,避免数据创建或获取的耗时,把已经拿到的数据保存到文件或内容中。

套用到RecyclerView上,列表滑动是一个触发很频繁的操作,ItemView布局的解析,创建,数据绑定是耗时操作,利用缓存机制,根据ItemView高度,RecyclerView一次性创建可视范围内的ItemView,之后在ItemView滑入,滑出时进行ItemView的回收,复用。

避免创建每次创建ItemView,影响性能。

本地缓存有IO耗时,速度慢,不适合列表滑动这样频繁触发的场景,所以肯定是使用内存缓存。

内存缓存会考虑:作用域,阈值。可以根据这两点思考RecyclerView的缓存。

三,RecyclerView.Recycler类结构

RecyclerView缓存的核心类与我们之前提到过的内存缓存概念几乎一致,Recycler 类中每个属性都代表一种缓存场景,同时控制缓存数据的数量,防止数据过多导致内存爆炸。

RecyclerView的缓存就是操作:mAttachedScrap ,mChangedScrap ,mCachedViews ,ViewCacheExtension ,RecycledViewPool这些对象存取数据的过程 。

每个对象有不同的职责,作用域,范围。

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 intDEFAULT_CACHE_SIZE= 2;
}

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<>();

}

四,缓存复用

看代码要有切入点,RecyclerView一共1w3k 行代码,事无巨细的看完是不可能呢。ItemView的回收复用主要发生在列表滑动时,那就以滑动为切入点,做过滑动的同学应该都知道,代码实现在onTouchEvent 方法

(1)onTouchEvent

(a)在onTouchEvent 中并没有太多的核心代码,多是初始化代码。

(b)判断LayoutManager 是否存在mLayout == null 。 RecyclerView是利用组件化思想设计的View,把布局,测量,滑动,数据,动画等各个部分抽象成组件,交给用户实现,LayoutManager 负责RecyclerView的布局,测量,view的缓存等功能。

(c) mLayout.canScrollHorizontally()mLayout.canScrollVertically() 滑动方向

(d)VelocityTracker.*obtain*() 收集滑动速度,实现惯性滑动

(e) 多点触控 final int action = e.getActionMasked(); final int actionIndex = e.getActionIndex();

(f)计算滑动距离,int dy = mLastTouchY - y ;嵌套滑动 dispatchNestedPreScroll ;滑动冲突 getParent().requestDisallowInterceptTouchEvent(true)

(e)追踪方法, scrollByInternal--》scrollStep --》 mLayout.scrollVerticallyBy ,发现滑动交给LayoutManager 实现,scrollVerticallyBy 是抽象方法,查看LinearLayoutManager 是如何实现的

(2)LinearLayoutManager.scrollVerticallyBy

(a) 判断滑动方法后 mOrientation == *HORIZONTAL ,调用* scrollBy()

(b)scrollBy() 内部代码并不是很长,第一次看怎么找到代码的核心呢。 代码的最初判断ViewGroup内子View的数量,如果没有子View直接return

(c)之后通过 变量 consumed 再次判断,如果小于0,则return。并打印日志 Don't have any more elements to scroll 没更多可滑动的元素

(d)默认的返回变量 scrolledconsumed 的参与。

(e)经过上述三点,可以判断方法的大概作用是 判断RecyclerView内部是否有可滑动子View,核心代码 final int consumed = mLayoutState.mScrollingOffset+ fill(recycler, mLayoutState, state, false); 下一步的核心方法 fill()

(f)因为方法带有返回值,出现return语句的位置,肯定有逻辑点,可以分析哪里为核心代码。

int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
        // 1111111
				if (getChildCount() == 0 || delta == 0) {
            return 0;
        }
        ensureLayoutState();
        mLayoutState.mRecycle = true;
        final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
        final int absDelta = Math.abs(delta);
        updateLayoutState(layoutDirection, absDelta, true, state);
        final int consumed = mLayoutState.mScrollingOffset
                + fill(recycler, mLayoutState, state, false);
        //22222222
				if (consumed < 0) {
            if (DEBUG) {
                Log.d(TAG, "Don't have any more elements to scroll");
            }
            return 0;
        }
	      //33333333 
			 final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
        mOrientationHelper.offsetChildren(-scrolled);
        if (DEBUG) {
            Log.d(TAG, "scroll req: " + delta + " scrolled: " + scrolled);
        }
        mLayoutState.mLastScrollDelta = scrolled;
        return scrolled;
    }

(3)LinearLayoutManager.fill()

(a)进行到这步位置,仍然没有接触到ItemView的回收复用逻辑。 上一步判断了是否有可滑动的view,距离核心应该不远了。

(b) fill() 单词填充的意思,它的方法注释非常有意思:这是一个神奇的方法,填充由layoutState定义的布局。这个方法的逻辑可以复用,几乎不需要改变就可以作为一个帮助类存在

Google的大神对这段代码非常的自信 哈哈哈。

(c)看注释可以知道一个核心类 layoutState 作为参数传递的。 参数注释:Configuration on how we should fill out the available space. 如何填充可用空间的配置

根据注释信息可以判断,接下来的逻辑是关于:复用的,填充剩余空间。

(d)简单浏览一下 LayoutState 的代码,可以看到有View next() 获取下一个view;boolean hasMore() 是否有更多数据; List<RecyclerView.ViewHolder> mScrapList = null; ViewHodler集合; 明显需要遍历。

(e)fill() 中有一处 while 调用了hasMore() 。 方法中一堆状态判断,但核心代码一般都隐藏在判断中 就是它: layoutChunk()

结合注释读源码也是一个不错的方法, 看不明白就翻译 手动狗头


/**
 * The magic functions :). Fills the given layout, defined by the layoutState. This is fairly
 * independent from the rest of the {@linkLinearLayoutManager}
 * and with little change, can be made publicly available as a helper class.
 *
 *@paramrecyclerCurrent recycler that is attached to RecyclerView
 *@paramlayoutStateConfiguration on how we should fill out the available space.
 *@paramstateContext passed by the RecyclerView to control scroll steps.
 *@paramstopOnFocusableIf true, filling stops in the first focusable new child
 *@returnNumber of pixels that it added. Useful for scroll functions.
 */
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);
    }
    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);
        if (RecyclerView.VERBOSE_TRACING) {
            TraceCompat.endSection();
        }
        if (layoutChunkResult.mFinished) {
            break;
        }
        layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
/**
         * Consume the available space if:
         * * layoutChunk did not request to be ignored
         * * OR we are laying out scrap children
         * * OR we are not doing pre-layout
         */
if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
                || !state.isPreLayout()) {
            layoutState.mAvailable -= layoutChunkResult.mConsumed;
            // we keep a separate remaining space because mAvailable is important for recycling
            remainingSpace -= layoutChunkResult.mConsumed;
        }

        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            recycleByLayoutState(recycler, layoutState);
        }
        if (stopOnFocusable && layoutChunkResult.mFocusable) {
            break;
        }
    }
    if (DEBUG) {
        validateChildOrder();
    }
    return start - layoutState.mAvailable;
}

(4)LinearLayoutManager.layoutChunk()

代码与之前相比好理解多了,三步走:,add添加view,layout子View布局

(a)获取View View view = layoutState.next(recycler);

(b)添加view addView(view);

(c) 子view布局 layoutDecoratedWithMargins(view, left, top, right, bottom);

(d)final View view = recycler.getViewForPosition(mCurrentPosition); 终于进入到主角了 RecyclerView.Recycler。

通过postion获取view ,方法调用链,

getViewForPosition(int position)—》 getViewForPosition(int position, boolean dryRun) —》 tryGetViewHolderForPositionByDeadline()

(5)RecyclerView.Recycler.getViewForPosition

方法注释:Attempts to get the ViewHolder for the given position, either from the Recycler scrap, cache, the RecycledViewPool, or creating it directly.

翻译:尝试从 Recycler scrapcacheRecycledViewPool 中获取缓存的ViewHolder。 如果没有则直接创建。

代码中有注释,说明每一步骤干了哪些事件,接下来跟随官方注释读源码

(a)0) If there is a changed scrap, try to find from there

根据代码看,在 mState.isPreLayout() 条件下触发。在 getChangedScrapViewForPosition() 方法中从集合 mChangedScrap 里获取ViewHodler。

ViewHolder比对条件有两种:

根据position: holder.getLayoutPosition() == position

根据id: final long id = mAdapter.getItemId(offsetPosition); holder.getItemId() == id

-------------------
if (mState.isPreLayout()) {
    holder = getChangedScrapViewForPosition(position);
    fromScrapOrHiddenOrCache = holder != null;
}

---------------------------
ViewHolder getChangedScrapViewForPosition(int position) {
            // If pre-layout, check the changed scrap for an exact match.
            final int changedScrapSize;
            if (mChangedScrap == null || (changedScrapSize = mChangedScrap.size()) == 0) {
                return null;
            }
            // find by position
            for (int i = 0; i < changedScrapSize; i++) {
                final ViewHolder holder = mChangedScrap.get(i);
                if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
                    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                    return holder;
                }
            }
            // find by id
            if (mAdapter.hasStableIds()) {
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                if (offsetPosition > 0 && offsetPosition < mAdapter.getItemCount()) {
                    final long id = mAdapter.getItemId(offsetPosition);
                    for (int i = 0; i < changedScrapSize; i++) {
                        final ViewHolder holder = mChangedScrap.get(i);
                        if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) {
                            holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                            return holder;
                        }
                    }
                }
            }
            return null;
        }

-------------------------

(b)现在 mChangedScrap 缓存只是知道如何获取的,缓存在什么场景下应用呢? 搜索代码 mChangedScrap.add,发现只有一处调用 scrapView()

代码不长,只有一个if,else语句,判断当前ViewHolder的状态。

判断条件有点复杂,感觉不能详细,正确的解释,感觉是和目前正在屏幕上展示的ViewHolder有关。系统定义了很多ViewHolder可能存在状态,如执行动画,新增,删除,更新等操作一共13个,作为静态常量在ViewHolder中定义。

把状态分为通过if,else语句分为两类,一类情况添加到 mAttachedScrap 中备用,另一类添加到mChangedScrap 中备用。

RecyclerView支持ItemView动画,看代码感觉和动画有点关系。

void scrapView(View view) {
    final ViewHolder holder =getChildViewHolderInt(view);
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED| ViewHolder.FLAG_INVALID)
            || !holder.isUpdated() 
						|| canReuseUpdatedViewHolder(holder)) {

        mAttachedScrap.add(holder);
    } else {
        if (mChangedScrap == null) {
            mChangedScrap = new ArrayList<ViewHolder>();
        }
        holder.setScrapContainer(this, true);
        mChangedScrap.add(holder);
    }
}

(c)1) Find by position from scrap/hidden list/cache

从两处缓存中取数据 mAttachedScrap ,刚已经分析过了 可能和动画有关。

另一处是 mCachedViews.get(i)

if (holder == null) {
    holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}

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);
								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);
								return holder;
                }
            }
            return null;
        }

(d) 2) Find from scrap/cache via stable ids, if exists

调用 getScrapOrCachedViewForId() 仍然是从mAttachedScrapmCachedViews 中获取,和上个方法相比不同的是,一个通过position 比对 viewHolder,一个通过id比对viewHolder

(e)ViewCacheExtension

viewHolder仍然为null,通过 获取缓存,ViewCacheExtension 默认为null,官方并没有提供实现,交给程序员自定义缓存逻辑,emm几乎是没人用。

(f)RecycledViewPool

最后一层缓存,通过RecycledViewPool 获取。RecycledViewPool 的数据结构稍微复杂一点,它实现多类型列表时的缓存。

根据类型从 SparseArray<ScrapData> mScrap 中 获取ScrapData

ScrapData 中持有ViewHolder的List集合,默认缓存数量为5。 取缓存的时候并不是通过List.get 而是List.remove ,移除指定下标的对象同时 并 返回对象引用,这样保证不会出现重复引用的问题。

holder = getRecycledViewPool().getRecycledView(type);

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;
        }

(g)创建,绑定ViewHolder

经过上述几层缓存,仍然没有取到数据,则创建一个新的ViewHolder 并绑定数据。

到这里整个复用流程就结束了

holder = mAdapter.createViewHolder(RecyclerView.this, type);

private boolean tryBindViewHolderByDeadline(@NonNull ViewHolder holder, int offsetPosition,
                int position, long deadlineNs) {

		mAdapter.bindViewHolder(holder, offsetPosition);
}

五,缓存回收

上一节撸了一遍复用逻辑,知道了怎么取数据,现在看看如何存数据。

缓存回收同样发生在滑动阶段 ,逻辑起始处也是 LinearLayoutManager.fill() 方法 与 复用一致。

回收发生在复用之前

回收方法: recycleByLayoutState

复用方法:layoutChunk

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,

    if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
        //回收
				recycleByLayoutState(recycler, layoutState);
    }

    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
				//复用
        layoutChunk(recycler, state, layoutState, layoutChunkResult);

  }

}

(1)recycleByLayoutState

根据滑动方向,判断从那个方向回收,逻辑一致,看一个就好

private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
    if (!layoutState.mRecycle || layoutState.mInfinite) {
        return;
    }
    int scrollingOffset = layoutState.mScrollingOffset;
    int noRecycleSpace = layoutState.mNoRecycleSpace;
    if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
        recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);
    } else {
        recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
    }
}

(2)recycleViewsFromEnd

这几个变量和方法调用具体的作用不太清除,应该是获取view的top,bottom,padding之类的数据用于判断ItemView是否超出滑动边界。剩余代码调用链

recycleChildren —> removeAndRecycleViewAt —>recycler.recycleView —>recycleViewHolderInternal

核心代码在 recycleViewHolderInternal

final int limit = mOrientationHelper.getEnd() - scrollingOffset + noRecycleSpace;

for (int i = 0; i < childCount; i++) {
    View child = getChildAt(i);
    if (mOrientationHelper.getDecoratedStart(child) < limit
            || mOrientationHelper.getTransformedStartWithDecoration(child) < limit) {
        // stop here
        recycleChildren(recycler, 0, i);
        return;
    }
}

(3)recycleViewHolderInternal

精简过后的代码如下 首先处理 mCachedViews 缓存,后处理RecycledViewPool 缓存

(a)mViewCacheMaxmCachedViews 的最大缓存数量,默认值为2。mCachedViews 在默认情况下只允许存储两个ViewHolder

(b)注意这个判断 if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) ,如果缓存中有值,先移除第一条数据后,再添加新数据到缓存中。 这个逻辑保证了mCachedViews 的数据,始终是最新移除屏幕的两条数据,造成什么效果呢?

(c)随便写个RecyclerView测试一哈,在 onBindViewHolderonViewRecycled 添加日志,然后慢慢的滑动列表,把第一条数据完全滑出屏幕,在滑入。会发现并没有输出带有 position == 0的日志

(d)第一条数据滑出,又滑入屏幕即没有回收,也没有重新绑定数据,这就是mCachedViews 缓存的效果。把第二条数据完全滑出屏幕后,就会看到 onViewRecycled position:0 的日志,再次重新滑入屏幕后,onBindViewHolder position:0 重新绑定数据。

mCachedViews缓存小结 :缓存最新滑出屏幕的ViewHolder,不会重新绑定数据,始终缓存最新的ViewHolder。

接下来是RecycledViewPool 缓存,viewHolder添加到RecycledViewPool 有两处逻辑。

(a)if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) 逻辑中,移除第一条缓存调用

recycleCachedViewAt(0); 在方法内部会把从mCachedViews 移除的viewHolder直接添加到RecycledViewPool 中。

(b)如果mCachedViews 没有缓存当前ViewHolder ,条件if (!cached) 则会交给RecycledViewPool 处理缓存。

(c)RecycledViewPool 的缓存逻辑比mCachedViews 更复杂一点,它要处理多类型的情况。

(d)先回顾一下RecycledViewPool 的数据结构。RecycledViewPool 中持有SparseArray 以key-value存储数据,key是viewType,value是ScrapDataScrapData 内部持有ArrayList保存ViewHolder,默认每个ArrayList最多保存5条数据。

(e)清楚数据结构,缓存逻辑也就清晰了,实现在putRecycledView 方法中。

(f)根据viewType取出ScrapData ;从ScrapData 获取ViewHolder缓存集合,判断是否超过阈值;如果没超过则调用scrap.resetInternal(); 重置viewHolder状态后存入缓存。从RecycledViewPool 复用的viewHolder都需要重新绑定数据

void recycleViewHolderInternal(ViewHolder holder) {

    boolean cached = false;
    boolean recycled = false;

    if (forceRecycle || holder.isRecyclable()) {
				//省略了viewHolder一些状态判断
        if (mViewCacheMax > 0) {
            // Retire oldest cached view
            int cachedViewSize = mCachedViews.size();
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                //移除缓存后,会被添加到RecycledViewPool
								recycleCachedViewAt(0);
                cachedViewSize--;
            }
						//省略代码
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
        }
        if (!cached) {
            addViewHolderToRecycledViewPool(holder, true);
            recycled = true;
        }
    } 
}

void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
		    //调用view回收的回调接口
		    if (dispatchRecycled) {
           dispatchViewRecycled(holder);
        }    
				getRecycledViewPool().putRecycledView(holder);
        }

public void putRecycledView(ViewHolder scrap) {
            final int viewType = scrap.getItemViewType();
            final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
            if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
                return;
            }
            if (DEBUG && scrapHeap.contains(scrap)) {
                throw new IllegalArgumentException("this scrap item already exists");
            }
            scrap.resetInternal();
            scrapHeap.add(scrap);
        }

----RecycledViewPool数据结构----
public static class RecycledViewPool {
		SparseArray<ScrapData> mScrap = new SparseArray<>();
		
		static class ScrapData {
            final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
            int mMaxScrap = DEFAULT_MAX_SCRAP;
            long mCreateRunningAverageNs = 0;
            long mBindRunningAverageNs = 0;
    }
}

RecycledViewPool 还有一点需要提交,可以几个RecyclerView共用一个RecycledViewPool。适用于在一个页面中有多个RecyclerView,并且itemView的布局类型相同时,这里就不详细介绍使用方法了。

六,总结

回顾一下Recycler的类结构,共有五种缓存容器:mChangedScrap ,mAttachedScrap ,mCachedViews ,ViewCacheExtension ,RecycledViewPool 。

复用:从上述几个容器中获取缓存,如果都没有获取到则通过adatper创建新的ViewHolder。

回收:

(a)发生在列表滑动,逻辑由mCachedViews 和 RecycledViewPool 构成,并没有看到其他缓存层级的参与。

(b)mCachedViews 默认缓存容量2,存储刚刚滑出屏幕的数据,没有清除状态,刚滑出又滑入时不会重新绑定数据,不区分Itemview的类型。

(c)RecycledViewPool 实现多类型缓存,每个类型最多存储5条数据。RecycledViewPool 的作用范围更大,可以几个RecyclerView共用同一个RecycledViewPool 。

(d)ViewCacheExtension 开发自定义缓存,没有默认实现,emm 没用过就不总结了。

(e) mChangedScrap ,mAttachedScrap 在滑动的回收逻辑中没见过。结合网上文章 和 源码注释猜测有两处使用。 一是列表动画,二是Layout布局。仔细研究流程估计可以写另一篇文章了,这里就不详细唠了(懒)。

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 intDEFAULT_CACHE_SIZE= 2;
}

七,参考

【腾讯Bugly干货分享】Android ListView与RecyclerView对比浅析--缓存机制_腾讯Bugly的博客-CSDN博客

【腾讯Bugly干货分享】RecyclerView 必知必会_腾讯Bugly的博客-CSDN博客

让你彻底掌握RecyclerView的缓存机制 - 简书 (jianshu.com)

(35条消息) Android高阶系列——RecyclerView的回收复用机制(多级缓存)【源码分析】_高、远的博客-CSDN博客

(35条消息) 换个姿势看源码 | RecyclerView预布局(pre-layout)_augfun的博客-CSDN博客_recyclerview 预布局