结合源码理解RecyclerView的四级缓存机制

很多人提起RecycleView和ListView的时候必定会提起两者的差异,都说RecycleView好,多布局,多缓存,等等。今天我们结合下源码理清一下RecycleView的多级缓存机制,我研究代码喜欢先从宏观了解下整体,然后再分个进入。所以等会先介绍下管理RecycleView的缓存类,然后会从某个类开始切入,分析。

Recyclerview的缓存类

RecycleView的四级缓存是由三个类共同作用完成的,Recycler、RecycledViewPool和ViewCacheExtension。

Recycler

用于管理已经废弃或者与RecyclerView分离的ViewHolder,这里面有两个重要的成员

  1. 屏幕内缓存 屏幕内缓存指在屏幕中显示的ViewHolder,这些ViewHolder会缓存在mAttachedScrap、mChangedScrap中
    mChangedScrap 表示数据已经改变的viewHolder列表 mAttachedScrap未与RecyclerView分离的ViewHolder列表
  2. 屏幕外缓存 当列表滑动出了屏幕时,ViewHolder会被缓存在 mCachedViews ,其大小由mViewCacheMax决定,默认DEFAULT_CACHE_SIZE为2,可通过Recyclerview.setItemViewCacheSize()动态设置。
RecycledViewPool

RecycledViewPool类是用来缓存ViewHolder用,如果多个RecyclerView之间用setRecycledViewPool(RecycledViewPool)设置同一个RecycledViewPool,他们就可以共享ViewHolder。

ViewCacheExtension

开发者可自定义的一层缓存,是虚拟类ViewCacheExtension的一个实例,开发者可实现方法getViewForPositionAndType(Recycler recycler, int position, int type)来实现自己的缓存。

Recyclerview结合源码分析缓存

看过了上面的介绍你应该有了一个总体的印象,现在带着这些印象我们去源码里面看看到底怎么实现的,我会从第一个Recycler开始,然后理清楚,各个缓存中是如何变化的,如何从一级变到另外一级,其代码具体是怎样的逻辑!首先我们先点进Recycler,映入眼帘的就是这些成员(只截取部分):

   public final class Recycler {
            //一级缓存中用来存储屏幕中显示的ViewHolde
        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<ViewHolder>();
        private ArrayList<ViewHolder> mChangedScrap = null;
          //二级缓存中用来存储屏幕外的缓存
        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
           //暂可忽略 mAttachedScrap的不可变视图
        private final List<ViewHolder>
                mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
            //当前屏幕外缓存大小,数量为2,即本代码片最后一个DEFAULT_CACHE_SIZE 成员的值,可变。
        private int mViewCacheMax = DEFAULT_CACHE_SIZE;
            //四级缓存当屏幕外缓存的大小大于2,便放入mRecyclerPool中缓存。
        private RecycledViewPool mRecyclerPool;
              //三级缓存自定义缓存,根据coder自己定义的缓存规则。
        private ViewCacheExtension mViewCacheExtension;
        //默认屏幕外缓存大小。
        private static final int DEFAULT_CACHE_SIZE = 2;
        }

首先我们拿一级缓存开刀mAttachedScrap 和mChangedScrap ,我们先定位到该方法中。

 void scrapView(View view) {
            final ViewHolder holder = getChildViewHolderInt(view);
            holder.setScrapContainer(this);
            //如果ViewHolder没有改变那么放入mAttachedScrap中,否则产生了变换存入mChangedScrap中。
            if (!holder.isChanged() || !supportsChangeAnimations()) {
                   //如果ViewHolder无效或者被移除,则抛异常
                if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
                    throw new IllegalArgumentException("Called scrap view with an invalid view."
                            + " Invalid views cannot be reused from scrap, they should rebound from"
                            + " recycler pool.");
                }
                mAttachedScrap.add(holder);
            } else {
                if (mChangedScrap == null) {
                    mChangedScrap = new ArrayList<ViewHolder>();
                }
                mChangedScrap.add(holder);
            }
        }

上面代码很容易理解,避开那些不重要的,其实就是判断了一下ViewHolder有没有发生改变,果ViewHolder没有改变那么放入mAttachedScrap中,否则产生了变换存入mChangedScrap中。这就是一级缓存
我们来看下二级缓存,ctrl+f搜索mCachedViews 然后定位到add方法,就可以看到这个代码

 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)) {
                    // Retire oldest cached view
                    int cachedViewSize = mCachedViews.size();
                    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                        recycleCachedViewAt(0);
                        cachedViewSize--;
                    }

                    int targetCacheIndex = cachedViewSize;
                    if (ALLOW_THREAD_GAP_WORK
                            && cachedViewSize > 0
                            && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                        // when adding the view, skip past most recently prefetched views
                        int cacheIndex = cachedViewSize - 1;
                        while (cacheIndex >= 0) {
                            int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                            if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                                break;
                            }
                            cacheIndex--;
                        }
                        targetCacheIndex = cacheIndex + 1;
                    }
                    //加入mCachedViews中
                    mCachedViews.add(targetCacheIndex, holder);
                    cached = true;
                }
                 //如果mCachedViews满了 加入pool中
                if (!cached) {
                    addViewHolderToRecycledViewPool(holder, true);
                    recycled = true;
                }
            } 
        }
        ...

上面的代码太多了我们去掉一些不重要的,可以注意到我注释的两个地方,当Item被移出屏幕区域时,先是缓存进了mCachedViews中,如果mCachedViews满了 加入pool中
因为处于mCachedViews中的ViewHolder是希望被原样重用的,所以没有进行清理工作,也就是说ViewHolder相关的position,flag等标志都一并被缓存了,那么从mCachedViews中取出的ViewHolder就不需要再进行绑定操作而可以直接使用了(实际上所以我们期望的也是在mCachedViews中的ViewHolder能够被重用,并且还是在它原来的位置被重用,这样就不需要再去bind了)
我们再来看看RecycledViewPool

  void setRecycledViewPool(RecycledViewPool pool) {
            if (mRecyclerPool != null) {
                mRecyclerPool.detach();
            }
            mRecyclerPool = pool;
            if (pool != null) {
                mRecyclerPool.attach(getAdapter());
            }
        }

        RecycledViewPool getRecycledViewPool() {
            if (mRecyclerPool == null) {
                mRecyclerPool = new RecycledViewPool();
            }
            return mRecyclerPool;
        }

首先你可以通过set主动设置,通过get得到,如果没有设置,get则会给你new一个
然后我们看下类成员变量

    public static class RecycledViewPool {
    //默认pool大小为5,只能存储5个,这个值可以更改的,有提供set函数
        private static final int DEFAULT_MAX_SCRAP = 5;
        //其他一些成员不必深究,mScrapHeap 是咱们存储的这一类viewholder
        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<>();

        private int mAttachCount = 0;
        }

然后我们看一下如何获取到我们这个池子里的缓存,这是第四级缓存,我们待会会说第三级缓存,先说第四级,这个函数怎么写的

 public ViewHolder getRecycledView(int viewType) {
            final ScrapData scrapData = mScrap.get(viewType);
            if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
                final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
                return scrapHeap.remove(scrapHeap.size() - 1);
            }
            return null;
        }

代码很少,很清晰,就是根据一个int型的viewType来获取,那么viewType是如何确定的?其实是需要我们重写ViewHolder的getItemViewType()方法,如果没有重写那么就默认只有一种View Type,默认为-1。代码中是这样的

public static final int INVALID_TYPE = -1;
...
int mItemViewType = INVALID_TYPE;
...
public final int getItemViewType() {
            return mItemViewType;
        }

再来看看put函数,put函数才是用来将viewholder存入pool中的

   public void putRecycledView(ViewHolder scrap) {
            //获取type类型,根据type类型缓存
            final int viewType = scrap.getItemViewType();
            final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
            //大于默认数值不缓存,直接return
            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);
        }

然后我根据这个函数,查看了一下调用者。

  void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
            clearNestedRecyclerViewIfNotNested(holder);
            if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE)) {
                holder.setFlags(0, ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE);
                ViewCompat.setAccessibilityDelegate(holder.itemView, null);
            }
            if (dispatchRecycled) {
                dispatchViewRecycled(holder);
            }
            holder.mOwnerRecyclerView = null;
            getRecycledViewPool().putRecycledView(holder);
        }

其他不重要,重点关注这个函数dispatchViewRecycled(holder);,这行的意思是清楚所有的position,flag等,相当于扒了层皮再保存的,所有如果我们从池子里取出来的viewholder是要重新绑定数据的。

然后我们看看第三级缓存,自定义缓存。

  public abstract static class ViewCacheExtension {
        public abstract View getViewForPositionAndType(Recycler recycler, int position, int type);
    }

这里面就一个抽象函数,只需要实现一个View getViewForPositionAndType(Recycler recycler, int position, int type);方法就可以实现这级缓存,一般有这么些情形可以使用这层缓存比如其position固定、不会改变的一些item,等等。具体情况根据你自己具体的业务来定是否需要这层来提高效率。

总结

有说四级缓存的,也有说三级缓存的,三级缓存就是默认把一二级缓存说成一层,两者说法不一而已,其实都差不多,缓存流程就是,屏幕中 –屏幕外–用户自定义–pool池。需要注意的是从pool池中拿出来的viewholder是剥了层皮的,需要重新绑定一次数据。

参考:
RecyclerView之三级缓存源码解析
RecyclerView缓存分析

猜你喜欢

转载自blog.csdn.net/HJsir/article/details/81485653