RecyclerView 源码分析(二) —— 缓存机制

在前一篇文章 RecyclerView 源码分析(一) —— 绘制流程解析 介绍了 RecyclerView 的绘制流程,RecyclerView 经过将绘制流程从 View 中抽取出来,放到 LayoutManager 中,使得 RecyclerView 在不一样的 LayoutManager 中,拥有不一样的样式,使得 RecyclerView 异常灵活,大大增强了 RecyclerView 使用场景。html

固然,RecyclerView 的缓存机制也是它特有的一个优势,减小了对内存的占用以及重复的绘制工做,所以,本文意在介绍和学习 RecyclerView 的缓存设计思想。数组

当咱们在讨论混存的时候,必定会经历建立-缓存-复用的过程。所以对于 RecyclerView 的缓存机制也是按照以下的步骤进行。缓存

建立 ViewHolder(VH)

在讲到对子 itemView 测量的时候,layoutChunk 方法中会先得到每个 itemView,在获取后,在将其添加到 RecyclerView 中。因此咱们先来看看建立的过程:微信

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

next 就是调用 RecyclerView 的 getViewForPosition 方法来获取一个 View 的。而 getViewForPosition 方法最终会调用到 RecyclerView tryGetViewHolderForPositionByDeadline 方法。数据结构

tryGetViewHolderForPositionByDeadline

这个方法很长,可是其实逻辑很简单,整个过程前面部分是先从缓存尝试获取 VH,若是找不到,就会建立新的 VH,而后绑定数据,最后将再将 VH 绑定到 LayoutParams (LP) 上。架构

        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
            if (position < 0 || position >= mState.getItemCount()) {
                throw new IndexOutOfBoundsException("Invalid item position " + position
                        + "(" + position + "). Item count:" + mState.getItemCount()
                        + exceptionLabel());
            }
            boolean fromScrapOrHiddenOrCache = false;
            ViewHolder holder = null;
            // 省略从缓存查找 VH 的逻辑,下面是若是仍是没找到,就会建立一个新的if (holder == null) {
                    long start = getNanoTime();
                    if (deadlineNs != FOREVER_NS
                            && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
                        // abort - we have a deadline we can't meet
                        return null;
                    }
            // 建立 VH holder
= mAdapter.createViewHolder(RecyclerView.this, type); if (ALLOW_THREAD_GAP_WORK) { // only bother finding nested RV if prefetching RecyclerView innerView = findNestedRecyclerView(holder.itemView); if (innerView != null) { holder.mNestedRecyclerView = new WeakReference<>(innerView); } } long end = getNanoTime(); mRecyclerPool.factorInCreateTime(type, end - start); if (DEBUG) { Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder"); } } } // This is very ugly but the only place we can grab this information // before the View is rebound and returned to the LayoutManager for post layout ops. // We don't need this in pre-layout since the VH is not updated by the LM. if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) { holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST); if (mState.mRunSimpleAnimations) { int changeFlags = ItemAnimator .buildAdapterChangeFlagsForAnimations(holder); changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT; final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState, holder, changeFlags, holder.getUnmodifiedPayloads()); recordAnimationInfoIfBouncedHiddenView(holder, info); } } 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()) { if (DEBUG && holder.isRemoved()) { throw new IllegalStateException("Removed holder should be bound and it should" + " come here only in pre-layout. Holder: " + holder + exceptionLabel()); } final int offsetPosition = mAdapterHelper.findPositionOffset(position);
          // 进行数据绑定 bound
= tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs); } final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); final LayoutParams rvLayoutParams;
       // 下面逻辑就是将 VH 绑定到 LP, LP 又设置到 ItemView 上
if (lp == null) { rvLayoutParams = (LayoutParams) generateDefaultLayoutParams(); holder.itemView.setLayoutParams(rvLayoutParams); } else if (!checkLayoutParams(lp)) { rvLayoutParams = (LayoutParams) generateLayoutParams(lp); holder.itemView.setLayoutParams(rvLayoutParams); } else { rvLayoutParams = (LayoutParams) lp; } rvLayoutParams.mViewHolder = holder; rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound; return holder; }

 即便省略了中间从缓存查找 VH 的逻辑,剩下部分的代码仍是很长。那我再归纳下 tryGetViewHolderForPositionByDeadline 方法所作的事:app

  1. 从缓存查找 VH ;less

  2. 缓存没有,那么就建立一个 VH;函数

  3. 判断 VH 需不须要更新数据,若是须要就会调用 tryBindViewHolderByDeadline 绑定数据;源码分析

  4. 将 VH 绑定到 LP, LP 又设置到 ItemView 上,互相依赖;

到这里关于建立 VH 的逻辑就讲完了。

缓存

在介绍添加到缓存的逻辑时,仍是须要介绍缓存相关的类和变量。

缓存总体设计

由图可知,RecyclerView 缓存是一个四级缓存的架构。固然,从 RecyclerView 的代码注释来看,官方认为只有三级缓存,即mCachedViews是一级缓存,mViewCacheExtension 是二级缓存,mRecyclerPool 是三级缓存。从开发者的角度来看,mAttachedScrap 和 mChangedScrap 对开发者是不透明的,官方并未暴露出任何能够改变他们行为的方法。

缓存机制 Recycler 详解

Recycler 是 RecyclerView 的一个内部类。咱们来看一下它的主要的成员变量。

  • mAttachedScrap 缓存屏幕中可见范围的 ViewHolder

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

  • mChangedScrap 缓存滑动时即将与 RecyclerView 分离的ViewHolder,按子View的position或id缓存,默认最多存放2个

    ArrayList<ViewHolder> mChangedScrap = null;

  • mCachedViews  ViewHolder 缓存列表,其大小由 mViewCacheMax 决定,默认 DEFAULT_CACHE_SIZE 为 2,可动态设置。

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

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

    private ViewCacheExtension mViewCacheExtension;

  • RecycledViewPool ViewHolder 缓存池,在有限的 mCachedViews 中若是存不下 ViewHolder 时,就会把 ViewHolder 存入 RecyclerViewPool 中。

    RecycledViewPool mRecyclerPool; 

添加到缓存

VH 被建立以后,是要被缓存,而后重复利用的,那么他们是何时被添加到缓存的呢?此处仍是以 LinearLayoutManager 举例说明。在 RecyclerView 源码分析(一) —— 绘制流程解析 一文中曾提到一个方法:

 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
     // ...
     detachAndScrapAttachedViews(recycler);  
     // ...
  }

 onLayoutChildren 是对子 view 进行绘制。在对子 view 会先调用 detachAndScrapAttachedViews 方法,下面来看看这个方法。

detachAndScrapAttachedViews

下面来看下这个方法:

       // recyclerview      
       public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
            final int childCount = getChildCount();
            for (int i = childCount - 1; i >= 0; i--) {
                final View v = getChildAt(i);
          // 每一个 view 都会放到里面 scrapOrRecycleView(recycler, i, v); } }
private void scrapOrRecycleView(Recycler recycler, int index, View view) { final ViewHolder viewHolder = getChildViewHolderInt(view); if (viewHolder.shouldIgnore()) { if (DEBUG) { Log.d(TAG, "ignoring view " + viewHolder); } return; }
        // 若是 VH 无效,而且已经被移除了,就会走另外一个逻辑
if (viewHolder.isInvalid() && !viewHolder.isRemoved() && !mRecyclerView.mAdapter.hasStableIds()) { removeViewAt(index); recycler.recycleViewHolderInternal(viewHolder); } else {
          // 先 detch 掉,而后放入缓存中 detachViewAt(index); recycler.scrapView(view); mRecyclerView.mViewInfoStore.onViewDetached(viewHolder); } }

也就是在上面的逻辑里,被放到缓存中。这里就能够看到

  1. 若是是 remove,会执行 recycleViewHolderInternal(viewHolder) 方法,而这个方法最终会将 ViewHolder 加入 CacheView 和 Pool 中,

  2. 而当是 Detach,会将 View 加入到 ScrapViews 中

须要指出的一点是:须要区分两个概念,Detach 和 Remove

  1. detach: 在 ViewGroup 中的实现很简单,只是将 ChildView 从 ParentView 的 ChildView 数组中移除,ChildView 的 mParent 设置为 null, 能够理解为轻量级的临时 remove, 由于 View此时和 View 树仍是藕断丝连, 这个函数被常常用来改变 ChildView 在 ChildView 数组中的次序。View 被 detach 通常是临时的,在后面会被从新 attach。

  2. remove: 真正的移除,不光被从 ChildView 数组中除名,其余和 View 树各项联系也会被完全斩断(不考虑 Animation/LayoutTransition 这种特殊状况), 好比焦点被清除,从TouchTarget 中被移除等。

recycleViewHolderInternal

下面来看 Recycler 两个的具体逻辑方法:

        /**
         * internal implementation checks if view is scrapped or attached and throws an exception
         * if so.
         * Public version un-scraps before calling recycle.
         */
        void recycleViewHolderInternal(ViewHolder holder) {
       // ...省略前面的代码,前面都是在作检验
final boolean transientStatePreventsRecycling = holder .doesTransientStatePreventRecycling(); @SuppressWarnings("unchecked") final boolean forceRecycle = mAdapter != null && transientStatePreventsRecycling && mAdapter.onFailedToRecycleView(holder); boolean cached = false; boolean recycled = false; if (DEBUG && mCachedViews.contains(holder)) { throw new IllegalArgumentException("cached view received recycle internal? " + holder + exceptionLabel()); } 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.add(targetCacheIndex, holder); cached
= true; } if (!cached) { addViewHolderToRecycledViewPool(holder, true); recycled = true; } } else { } // even if the holder is not removed, we still call this method so that it is removed // from view holder lists. mViewInfoStore.removeViewHolder(holder); if (!cached && !recycled && transientStatePreventsRecycling) { holder.mOwnerRecyclerView = null; } }

该方法所作的事具体以下:

  1. 检验该 VH 的有效性,确保已经再也不被使用;

  2. 判断缓存的容量,超了就会进行移除,而后找一个合适的位置进行添加。

  3. 若是不能加入到 CacheViews 中,则加入到 Pool 中。

mCachedViews

mCachedViews 对应的数据结构也是 ArrayList 可是该缓存对集合的大小是有限制的,默认是 2。该缓存中 ViewHolder 的特性和 mAttachedScrap 中的特性是同样的,只要 position或者 itemId 对应上了,那么它就是干净的,无需从新绑定数据。开发者能够调用 setItemViewCacheSize(size) 方法来改变缓存的大小。该层级缓存触发的一个常见的场景是滑动 RV。固然 notifyXXX 也会触发该缓存。该缓存和 mAttachedScrap 同样特别高效。

RecyclerViewPool

RecyclerViewPool 缓存能够针对多ItemType,设置缓存大小。默认每一个 ItemType 的缓存个数是 5。并且该缓存能够给多个 RecyclerView 共享。因为默认缓存个数为 5,假设某个新闻 App,每屏幕能够展现 10 条新闻,那么必然会致使缓存命中失败,频繁致使建立 ViewHolder 影响性能。因此须要扩大缓存size。

scrapView

接下去看 scrapView 这个方法:

        void scrapView(View view) {
            final ViewHolder holder = getChildViewHolderInt(view);
            if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                    || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
                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." + exceptionLabel());
                }
                holder.setScrapContainer(this, false); // 这里的 false 
 mAttachedScrap.add(holder);             } else {
                if (mChangedScrap == null) {
                    mChangedScrap = new ArrayList<ViewHolder>();
                }
                holder.setScrapContainer(this, true);  // 这里是 true
 mChangedScrap.add(holder);             }
        }

 

该方法就比较简单了,没有那么多须要检验的逻辑。这里根据条件,有两种缓存类型能够选择,具体就不展开了,你们均可以看懂。这里讲解下两个 scrapView 的缓存。

mAttachedScrap

mAttachedScrap 的对应数据结构是ArrayList,在 LayoutManager#onLayoutChildren 方法中,对 views 进行布局时,会将 RecyclerView 上的 Views 所有暂存到该集合中,以备后续使用,该缓存中的 ViewHolder 的特性是,若是和 RV 上的 position 或者 itemId 匹配上了,那么认为是干净的 ViewHolder,是能够直接拿出来使用的,无需调用 onBindViewHolder 方法。该 ArrayList 的大小是没有限制的,屏幕上有多少个 View,就会建立多大的集合。

触发该层级缓存的场景通常是调用 notifyItemXXX 方法。调用 notifyDataSetChanged 方法,只有当 Adapter hasStableIds 返回 true,会触发该层级的缓存使用。

mChangedScrap

mChangedScrap 和 mAttachedScrap 是同一级的缓存,他们是平等的。可是mChangedScrap的调用场景是notifyItemChanged和notifyItemRangeChanged,只有发生变化的ViewHolder才会放入到 mChangedScrap 中。mChangedScrap缓存中的ViewHolder是须要调用onBindViewHolder方法从新绑定数据的。那么此时就有个问题了,为何同一级别的缓存须要设计两个不一样的缓存?

在 dispatchLayoutStep2 阶段 LayoutManager onLayoutChildren方法中最终会调用 layoutForPredictiveAnimations 方法,把 mAttachedScrap 中剩余的 ViewHolder 填充到屏幕上,因此他们的区别就是,mChangedScrap 中的 ViewHolder 在 RV 填充满的状况下,是不会强行填充到 RV 上的。那么有办法可让发生改变的 ViewHolder 进入 mAttachedScrap 缓存吗?固然能够。调用 notifyItemChanged(int position, Object payload) 方法能够,实现局部刷新功能,payload 不为空,那么发生改变的 ViewHolder 是会被分离到 mAttachedScrap 中的。

使用缓存

下面进入到最后一节,使用缓存。这个在以前绘制篇幅也有提到,下面直接看对应的方法:

//根据传入的position获取ViewHolder 
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    ---------省略----------
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
    //预布局 属于特殊状况 从mChangedScrap中获取ViewHolder
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    if (holder == null) {
        //一、尝试从mAttachedScrap中获取ViewHolder,此时获取的是屏幕中可见范围中的ViewHolder
        //二、mAttachedScrap缓存中没有的话,继续从mCachedViews尝试获取ViewHolder
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
     ----------省略----------
    }
    if (holder == null) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        ---------省略----------
        final int type = mAdapter.getItemViewType(offsetPosition);
        //若是Adapter中声明了Id,尝试从id中获取,这里不属于缓存
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
        }
        if (holder == null && mViewCacheExtension != null) {
            3、从自定义缓存mViewCacheExtension中尝试获取ViewHolder,该缓存须要开发者实现
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
            }
        }
        if (holder == null) { // fallback to pool
            //四、从缓存池mRecyclerPool中尝试获取ViewHolder
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                //若是获取成功,会重置ViewHolder状态,因此须要从新执行Adapter#onBindViewHolder绑定数据
                holder.resetInternal();
                if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
                }
            }
        }
        if (holder == null) {
            ---------省略----------
          //五、若以上缓存中都没有找到对应的ViewHolder,最终会调用Adapter中的onCreateViewHolder建立一个
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
        }
    }

    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        //六、若是须要绑定数据,会调用Adapter#onBindViewHolder来绑定数据
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
    ----------省略----------
    return holder;
}

上述逻辑用流程图表示:

 

总结一下上述流程:经过 mAttachedScrap、mCachedViews 及 mViewCacheExtension 获取的 ViewHolder 不须要从新建立布局及绑定数据;经过缓存池 mRecyclerPool 获取的 ViewHolder不须要从新建立布局,可是须要从新绑定数据;若是上述缓存中都没有获取到目标 ViewHolder,那么就会回调 Adapter#onCreateViewHolder 建立布局,以及回调 Adapter#onBindViewHolder来绑定数据。

ViewCacheExtension

咱们已经知道 ViewCacheExtension 属于第三级缓存,须要开发者自行实现,那么 ViewCacheExtension 在什么场景下使用?又是如何实现的呢?

首先咱们要明确一点,那就是 Recycler 自己已经设置了好几级缓存了,为何还要留个接口让开发者去自行实现缓存呢?

关于这一点,来看看 Recycler 中的其余缓存:

  1. mAttachedScrap 用来处理可见屏幕的缓存;

  2. mCachedViews 里存储的数据虽然是根据 position 来缓存,可是里面的数据随时可能会被替换的;

  3. mRecyclerPool 里按 viewType 去存储 ArrayList< ViewHolder>,因此 mRecyclerPool 并不能按 position 去存储 ViewHolder,并且从 mRecyclerPool 取出的 View 每次都要去走 Adapter#onBindViewHolder 去从新绑定数据。

假如我如今须要在一个特定的位置(好比 position=0 位置)一直展现某个 View,且里面的内容是不变的,那么最好的状况就是在特定位置时,既不须要每次从新建立 View,也不须要每次都去从新绑定数据,上面的几种缓存显然都是不适用的,这种状况该怎么办呢?能够经过自定义缓存 ViewCacheExtension 实现上述需求。 

RecyclerView & ListView缓存机制对比

结论援引自:Android ListView 与 RecyclerView 对比浅析--缓存机制

ListView和RecyclerView缓存机制基本一致:

  1. mActiveViews 和 mAttachedScrap 功能类似,意义在于快速重用屏幕上可见的列表项ItemView,而不须要从新 createView 和 bindView;

  2. mScrapView 和 mCachedViews + mReyclerViewPool功能类似,意义在于缓存离开屏幕的 ItemView,目的是让即将进入屏幕的 ItemView 重用.

  3. RecyclerView 的优点在于

    1. mCacheViews 的使用,能够作到屏幕外的列表项 ItemView 进入屏幕内时也无须bindView快速重用;

    2. mRecyclerPool 能够供多个 RecyclerView 共同使用,在特定场景下,如 viewpaper+ 多个列表页下有优点.客观来讲,RecyclerView 在特定场景下对 ListView 的缓存机制作了补强和完善。

不一样使用场景:列表页展现界面,须要支持动画,或者频繁更新,局部刷新,建议使用 RecyclerView,更增强大完善,易扩展;其它状况(如微信卡包列表页)二者都OK,但ListView在使用上会更加方便,快捷。

 

参考文章

https://www.jianshu.com/p/2b19e9bcda84

https://www.jianshu.com/p/6e6bf58b7f0d

https://www.jianshu.com/p/e1b257484961

RecyclerView加载了那么多图,为何就是不崩呢?

相关文章
相关标签/搜索