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

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

当然,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 方法所做的事:

  1. 从缓存查找 VH ;

  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) {
        //1、尝试从mAttachedScrap中获取ViewHolder,此时获取的是屏幕中可见范围中的ViewHolder
        //2、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
            //4、从缓存池mRecyclerPool中尝试获取ViewHolder
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                //如果获取成功,会重置ViewHolder状态,所以需要重新执行Adapter#onBindViewHolder绑定数据
                holder.resetInternal();
                if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
                }
            }
        }
        if (holder == null) {
            ---------省略----------
          //5、若以上缓存中都没有找到对应的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);
        //6、如果需要绑定数据,会调用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加载了那么多图,为什么就是不崩呢?

posted @ 2021-01-23 21:53  huansky  阅读(855)  评论(0编辑  收藏  举报