RecyclerView缓存机制(咋复用?)

这是RecyclerView缓存机制系列文章的第三篇,系列文章的目录如下:

  1. RecyclerView缓存机制(咋复用?)
  2. RecyclerView缓存机制(回收些啥?)
  3. RecyclerView缓存机制(回收去哪?)

引子


  • 如果列表中每个移出屏幕的表项都直接销毁,移入时重新创建,很不经济。所以RecyclerView引入了缓存机制。
  • 回收是为了复用,复用的好处是有可能免去两个昂贵的操作:1. 为表项视图绑定数据 2. 创建表项视图
  • 下面几个问题对于理解“回收复用机制”很关键:
    1. what:回收什么?复用什么?
    2. where:回收到哪里去?从哪里获得复用?
    3. when:什么时候回收?什么时候复用?

这一篇试着从已知的知识出发在源码中寻觅未知的“RecyclerView复用机制”。
(ps: 下文中的 粗斜体字 表示引导源码阅读的内心戏)

寻觅


触发复用的众多时机中必然包含下面这种:“当移出屏幕的表项重新回到界面”。表项本质上是一个View,屏幕上的表项必然需要依附于一棵View树,即必然有一个父容器调用了addView()。而 RecyclerView继承自 ViewGroup,遂以RecyclerView.addView()为切入点向上搜寻复用的代码。

RecyclerView.java中全局搜索“addView”,发现RecyclerView()并没有对addView()函数重载,但找到一处addView()的调用:

//RecyclerView是ViewGroup的子类
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
    ...
    private void initChildrenHelper() {
        mChildHelper = new ChildHelper(new ChildHelper.Callback() {
            ...
            @Override
            public void addView(View child, int index) {
                if (VERBOSE_TRACING) {
                    TraceCompat.beginSection("RV addView");
                }
                //直接调用ViewGroup.addView()
                RecyclerView.this.addView(child, index);
                if (VERBOSE_TRACING) {
                    TraceCompat.endSection();
                }
                dispatchChildAttached(child);
            }
        }
    }
    ...
}

ChildHelper.Callback.addView()为起点沿着调用链继续向上搜寻,经历ChildHelper.addView()---LayoutManager.addViewInt()---LayoutManager.addView()最终到达LayoutManager.layoutChunk()

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
        //获得下一个表项
        View view = layoutState.next(recycler);
        if (view == null) {
            if (DEBUG && layoutState.mScrapList == null) {
                throw new RuntimeException("received null view when unexpected");
            }
            // if we are laying out views in scrap, this may return null which means there is
            // no more items to layout.
            result.mFinished = true;
            return;
        }
        LayoutParams params = (LayoutParams) view.getLayoutParams();
        if (layoutState.mScrapList == null) {
            //将表项插入到列表中
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addView(view);
            } else {
                addView(view, 0);
            }
        } else {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addDisappearingView(view);
            } else {
                addDisappearingView(view, 0);
            }
        }
        ...
}

addView(view)中传入的view是函数layoutState.next()的返回值。猜测该函数是用来获得下一个表项的。表项不止一个,应该有一个循环不断的获得下一个表项才对。沿着刚才的调用链继续往上搜寻,就会发现:的确有一个循环!

public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
    ...
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        ...
        //recyclerview 剩余空间
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        //不断填充,直到空间消耗完毕
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
            if (VERBOSE_TRACING) {
                TraceCompat.beginSection("LLM LayoutChunk");
            }
            //填充一个表项
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            ...
        }
        ...
    }
}

fill()是在onLayoutChildren()中被调用:

        /**
         * Lay out all relevant child views from the given adapter.
         * 布局所有给定adapter中相关孩子视图
         * 注释太长了,省略了不相关信息
         * @param recycler         Recycler to use for fetching potentially cached views for a
         *                         position
         * @param state            Transient state of RecyclerView
         */
        public void onLayoutChildren(Recycler recycler, State state) {
            Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
        }

看完注释,感觉前面猜测应该是正确的。onLayoutChildren()是用来布局RecyclerView中所有的表项的。回头去看一下layoutState.next(),表项复用逻辑应该就在其中。

    /**
     * Helper class that keeps temporary state while {LayoutManager} is filling out the empty
     * space.
     */
    static class LayoutState {       
       /**
         * Gets the view for the next element that we should layout.
         * 获得下一个元素的视图用于布局
         * Also updates current item index to the next item, based on {@link #mItemDirection}
         *
         * @return The next element that we should layout.
         */
        View next(RecyclerView.Recycler recycler) {
            if (mScrapList != null) {
                return nextViewFromScrapList();
            }
            //调用了Recycler.getViewForPosition()
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        }
    }

最终调用了Recycler.getViewForPosition(),Recycler是回收器的意思,感觉离想要找的“复用”逻辑越来越近了。Recycler到底是做什么用的?

    /**
     * A Recycler is responsible for managing scrapped or detached item views for reuse.
     *  Recycler负责管理scrapped和detached表项的复用
     * <p>A "scrapped" view is a view that is still attached to its parent RecyclerView but
     * that has been marked for removal or reuse.</p>
     */
    public final class Recycler {
      ...
    }

终于找到你~~ ,Recycler用于表项的复用!沿着Recycler.getViewForPosition()的调用链继续向下搜寻,找到了一个关键函数(函数太长了,为了防止头晕,只列出了关键节点):

       /**
         * Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
         * cache, the RecycledViewPool, or creating it directly.
         * 尝试获得指定位置的ViewHolder,要么从scrap,cache,RecycledViewPool中获取,要么直接重新创建
         * @return ViewHolder for requested position
         */
        @Nullable
        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
            ...
            boolean fromScrapOrHiddenOrCache = false;
            ViewHolder holder = null;
            //0 从changed scrap集合中获取ViewHolder
            if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }
            //1. 通过position从attach scrap或一级回收缓存中获取ViewHolder
            if (holder == null) {
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                ...
            }
            
            if (holder == null) {
                ...
                final int type = mAdapter.getItemViewType(offsetPosition);
                //2. 通过id在attach scrap集合和一级回收缓存中查找viewHolder
                if (mAdapter.hasStableIds()) {
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    ...
                }
                //3. 从自定义缓存中获取ViewHolder
                if (holder == null && mViewCacheExtension != null) {
                    // We are NOT sending the offsetPosition because LayoutManager does not
                    // know it.
                    final View view = mViewCacheExtension
                            .getViewForPositionAndType(this, position, type);
                    ...
                }
                //4.从缓存池中拿ViewHolder
                if (holder == null) { // fallback to pool
                    ...
                    holder = getRecycledViewPool().getRecycledView(type);
                    ...
                }
                //所有缓存都没有命中,只能创建ViewHolder
                if (holder == null) {
                    ...
                    holder = mAdapter.createViewHolder(RecyclerView.this, type);
                    ...
                }
            }

            boolean bound = false;
            if (mState.isPreLayout() && holder.isBound()) {
                // do not update unless we absolutely have to.
                holder.mPreLayoutPosition = position;
            }
            //只有invalid的viewHolder才能绑定视图数据
            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);
                //获得ViewHolder后,绑定视图数据
                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            }
            ...
            return holder;
        }
  • 函数的名字以“tryGet”开头,“尝试获得”表示可能获得失败,再结合注释中说的:“尝试获得指定位置的ViewHolder,要么从scrap,cache,RecycledViewPool中,要么直接重新创建。”猜测scrap,cache,RecycledViewPool是回收表项的容器,相当于表项缓存,如果缓存未命中则只能重新创建。
  • 函数的返回值是ViewHolder难道回收和复用的是ViewHolder? 函数开头声明了局部变量ViewHolder holder = null;最终返回的也是这个局部变量,并且有4处holder == null的判断,这样的代码结构是不是有点像缓存?每次判空意味着上一级缓存未命中并继续尝试新的获取方法?缓存是不是有不止一种存储形式? 让我们一次一次地看:

第一次尝试


ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
      ...
      if (mState.isPreLayout()) {
          holder = getChangedScrapViewForPosition(position);
          fromScrapOrHiddenOrCache = holder != null;
      }
      ...
}

只有在mState.isPreLayout()true时才会做这次尝试,这应该是一种特殊情况,先忽略。

第二次尝试


ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
      ...
      if (holder == null) {
          holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
          //下面一段代码蕴含着一个线索,买个伏笔,先把他略去
          ...
      }
      ...
}
  • 当第一次尝试失败后,尝试通过getScrapOrHiddenOrCachedHolderForPosition()获得ViewHolder
  • 这里故意省略了一段代码,先埋个伏笔,待会分析。先沿着获取ViewHolder的调用链继续往下:
//省略非关键代码
        /**
         * Returns a view for the position either from attach scrap, hidden children, or cache.
         * 从attach scrap,hidden children或者cache中获得指定位置上的一个ViewHolder
         */
        ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
            final int scrapCount = mAttachedScrap.size();
            // Try first for an exact, non-invalid match from scrap.
            //1.在attached scrap中搜索ViewHolder
            for (int i = 0; i < scrapCount; i++) {
                final ViewHolder holder = mAttachedScrap.get(i);
                if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
                        && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
                    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                    return holder;
                }
            }
            //2.从移除屏幕的视图中搜索ViewHolder,找到了之后将他存入scrap回收集合中
            if (!dryRun) {
                View view = mChildHelper.findHiddenNonRemovedView(position);
                if (view != null) {
                    final ViewHolder vh = getChildViewHolderInt(view);
                    mChildHelper.unhide(view);
                    int layoutIndex = mChildHelper.indexOfChild(view);
                    ...
                    mChildHelper.detachViewFromParent(layoutIndex);
                    scrapView(view);
                    vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
                            | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
                    return vh;
                }
            }
            // Search in our first-level recycled view cache.
            //3.在缓存中搜索ViewHolder
            final int cacheSize = mCachedViews.size();
            for (int i = 0; i < cacheSize; i++) {
                final ViewHolder holder = mCachedViews.get(i);
                if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
                    ...
                    return holder;
                }
            }
            return null;
        }  

依次从三个地方搜索ViewHolder:1. mAttachedScrap 2. 隐藏表项 3. mCachedViews,找到立即返回。
其中mAttachedScrapmCachedViews作为Recycler的成员变量,用来存储一组ViewHolder

    public final class Recycler {
        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
        ...
        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
        ...
        RecycledViewPool mRecyclerPool;
    }
  • 看到这里应该可以初步得出结论:RecyclerView回收机制中,回收复用的对象是ViewHolder,且以ArrayList为结构存储在Recycler对象中
  • RecycledViewPool mRecyclerPool;看着也像是回收容器,那待会是不是也会到这里拿 ViewHolder?
  • 回到刚才埋下的伏笔,把第二次尝试获取ViewHolder的代码补全:
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
      ...
      if (holder == null) {
          holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
          //下面一段代码蕴含这一个线索,买个伏笔,先把他略去
          if (holder != null) {
               //检验ViewHolder有效性
               if (!validateViewHolderForOffsetPosition(holder)) {
                    // recycle holder (and unscrap if relevant) since it can't be used
                    if (!dryRun) {
                         // we would like to recycle this but need to make sure it is not used by
                         // animation logic etc.
                         holder.addFlags(ViewHolder.FLAG_INVALID);
                         if (holder.isScrap()) {
                              removeDetachedView(holder.itemView, false);
                              holder.unScrap();
                          } else if (holder.wasReturnedFromScrap()) {
                              holder.clearReturnedFromScrapFlag();
                          }
                          //若不满足有效性检验,则回收ViewHolder
                          recycleViewHolderInternal(holder);
                    }
                    holder = null;
               } else {
                    fromScrapOrHiddenOrCache = true;
               }
          }
      }
      ...
}

如果成功获得ViewHolder则检验其有效性,若检验失败则将其回收。好不容易获取了ViewHoler对象,一言不合就把他回收?难道对所有复用的 ViewHolder 都有这么严格的检验吗?暂时无法回答这些疑问,还是先把复用逻辑看完吧:

第三次尝试


ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
      ...
      //只有当Adapter设置了id,才会进行这次查找
      if (mAdapter.hasStableIds()) {
           holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun);
           if (holder != null) {
                // update position
                holder.mPosition = offsetPosition;
                fromScrapOrHiddenOrCache = true;
           }
      }
      ...
}

这一次尝试调用的函数名(“byId”)和上一次(“byPosition”)只是后缀不一样。上一次是通过表项位置,这一次是通过表项id。内部实现也几乎一样,判断的依据从表项位置变成表项id。为表项设置id属于特殊情况,先忽略。

第四次尝试


ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
      ...
      if (holder == null && mViewCacheExtension != null) {
           // We are NOT sending the offsetPosition because LayoutManager does not
           // know it.
          final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
           if (view != null) {
                //获得view对应的ViewHolder
                holder = getChildViewHolder(view);
                if (holder == null) {
                    throw new IllegalArgumentException("getViewForPositionAndType returned"
                                    + " a view which does not have a ViewHolder"
                                    + exceptionLabel());
                } else if (holder.shouldIgnore()) {
                   throw new IllegalArgumentException("getViewForPositionAndType returned"
                                    + " a view that is ignored. You must call stopIgnoring before"
                                    + " returning this view." + exceptionLabel());
                }
            }
      }
      ...
}

经过从mAttachedScrapmCachedViews获取ViewHolder未果后,继续尝试通过ViewCacheExtension获取:


    /**
     * ViewCacheExtension is a helper class to provide an additional layer of view caching that can
     * be controlled by the developer.
     * ViewCacheExtension提供了额外的表项缓存层,用户帮助开发者自己控制表项缓存
     * <p>
     * When {@link Recycler#getViewForPosition(int)} is called, Recycler checks attached scrap and
     * first level cache to find a matching View. If it cannot find a suitable View, Recycler will
     * call the {@link #getViewForPositionAndType(Recycler, int, int)} before checking
     * {@link RecycledViewPool}.
     * 当Recycler从attached scrap和first level cache中未能找到匹配的表项时,它会在去RecycledViewPool中查找之前,先尝试从自定义缓存中查找
     * <p>
     */
    public abstract static class ViewCacheExtension {

        /**
         * Returns a View that can be binded to the given Adapter position.
         * <p>
         * This method should <b>not</b> create a new View. Instead, it is expected to return
         * an already created View that can be re-used for the given type and position.
         * If the View is marked as ignored, it should first call
         * {@link LayoutManager#stopIgnoringView(View)} before returning the View.
         * <p>
         * RecyclerView will re-bind the returned View to the position if necessary.
         */
        public abstract View getViewForPositionAndType(Recycler recycler, int position, int type);
    }

注释揭露了很多信息:ViewCacheExtension用于开发者自定义表项缓存,且这层缓存的访问顺序位于mAttachedScrapmCachedViews之后,RecycledViewPool之前。这和Recycler. tryGetViewHolderForPositionByDeadline()中的代码逻辑一致,那接下来的第五次尝试,应该是从 RecycledViewPool 中获取 ViewHolder

第五次尝试


ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
      ...
      if (holder == null) { 
          ...
          //从回收池中获取ViewHolder对象
          holder = getRecycledViewPool().getRecycledView(type);
          if (holder != null) {
               holder.resetInternal();
               if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
               }
          }
      }
      ...
}

前四次尝试都未果,最后从RecycledViewPool中获取ViewHolder稍等片刻!相对于从mAttachedScrapmCachedViews 中获取 ViewHolder,此处并没有严格的检验逻辑。为啥要区别对待不同的缓存?大大的问号悬在头顶,但现在暂时无法解答,还是接着看RecycledViewPool的结构吧~

public final class Recycler {
    ...
    RecycledViewPool mRecyclerPool;
    //获得RecycledViewPool实例
    RecycledViewPool getRecycledViewPool() {
          if (mRecyclerPool == null) {
              mRecyclerPool = new RecycledViewPool();
          }
          return mRecyclerPool;
    }
    ...
}
public static class RecycledViewPool {
    ...
    //从回收池中获取ViewHolder对象
    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;
    }
    ...
}

函数中只要访问了类成员变量,它的复杂度就提高了,因为类成员变量的作用于超出了函数体,使得函数就和类中其他函数耦合,所以不得不进行阅读更多以帮助理解该函数:

    public static class RecycledViewPool {
        //同类ViewHolder缓存个数上限
        private static final int DEFAULT_MAX_SCRAP = 5;

        /**
         * Tracks both pooled holders, as well as create/bind timing metadata for the given type.
         * 回收池中存放单个类型ViewHolder的容器
         */
        static class ScrapData {
            //同类ViewHolder存储在ArrayList中
            ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
            //每种类型的ViewHolder最多存5个
            int mMaxScrap = DEFAULT_MAX_SCRAP;
        }
        //回收池中存放所有类型ViewHolder的容器
        SparseArray<ScrapData> mScrap = new SparseArray<>();
        ...
        //ViewHolder入池 按viewType分类入池,一个类型的ViewType存放在一个ScrapData中
        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();
            //回收时,ViewHolder从列表尾部插入
            scrapHeap.add(scrap);
        }
        //从回收池中获取ViewHolder对象
        public ViewHolder getRecycledView(int viewType) {
              final ScrapData scrapData = mScrap.get(viewType);
              if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
                  final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
                  //复用时,从列表尾部获取ViewHolder(优先复用刚入池的ViewHoler)
                  return scrapHeap.remove(scrapHeap.size() - 1);
              }
              return null;
        }
}
  • 上述代码列出了RecycledViewPool中最关键的一个成员变量和两个函数。至此可以得出结论:RecycledViewPool中的ViewHolder存储在SparseArray中,并且按viewType分类存储(即是Adapter.getItemViewType()的返回值),同一类型的ViewHolder存放在ArrayList中,且默认最多存储5个。

创建ViewHolder并绑定数据


ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
            ...
            //所有缓存都没有命中,只能创建ViewHolder
            if (holder == null) {
                ...
                holder = mAdapter.createViewHolder(RecyclerView.this, type);
                ...
            }
            ...
            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);
            }
}
  • 再进行了上述所有尝试后,如果依然没有获得ViewHolder,只能重新创建并绑定数据。沿着调用链往下,就会找到熟悉的onCreateViewHolder()onBindViewHolder()
  • 绑定数据的逻辑嵌套在一个大大的if中(原来并不是每次都要绑定数据,只有满足特定条件时才需要绑定。
  • 那什么情况下需要绑定,什么情况下不需要呢?这就要引出“缓存优先级”这个概念。

缓存优先级


  • 缓存有优先级一说,在使用图片二级缓存(内存+磁盘)时,会先尝试去优先级高的内存中获取,若未命中再去磁盘中获取。优先级越高意味着性能越好。RecyclerView的缓存机制中是否也能套用“缓存优先级”这一逻辑?

  • 虽然为了获取ViewHolder做了5次尝试(共从6个地方获取),先排除3种特殊情况,即从mChangedScrap获取、通过id获取、从自定义缓存获取,正常流程中只剩下3种获取方式,优先级从高到低依次是:

    1. mAttachedScrap获取
    2. mCachedViews获取
    3. mRecyclerPool获取
  • 这样的缓存优先级是不是意味着,对应的复用性能也是从高到低?(复用性能越好意味着所做的昂贵操作越少)

    1. 最坏情况:重新创建ViewHodler并重新绑定数据
    2. 次好情况:复用ViewHolder但重新绑定数据
    3. 最好情况:复用ViewHolder且不重新绑定数据

    毫无疑问,所有缓存都未命中的情况下会发生最坏情况。剩下的两种情况应该由3种获取方式来分摊,猜测优先级最低的 mRecyclerPool 方式应该命中次好情况,而优先级最高的 mAttachedScrap应该命中最好情况,去源码中验证一下:

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

       // Try first for an exact, non-invalid match from scrap.
       //1.从attached scrap回收集合中
       for (int i = 0; i < scrapCount; i++) {
           final ViewHolder holder = mAttachedScrap.get(i);
           //只有当holder是有效时才返回
           if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
                   && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
               holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
               return holder;
           }
       }
}

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
      ...
      if (holder == null) { 
          ...
          //从回收池中获取ViewHolder对象
          holder = getRecycledViewPool().getRecycledView(type);
          if (holder != null) {
               //重置ViewHolder
               holder.resetInternal();
               if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
               }
          }
      }
      ...
      //如果表项没有绑定过数据 或 表项需要更新 或 表项无效 且表项没有被移除时绑定表项数据
      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);
      }
      ...
}

public abstract static class ViewHolder {
        /**
         * This ViewHolder has been bound to a position; mPosition, mItemId and mItemViewType
         * are all valid.
         * 绑定标志位
         */
        static final int FLAG_BOUND = 1 << 0;
        /**
         * This ViewHolder's data is invalid. The identity implied by mPosition and mItemId
         * are not to be trusted and may no longer match the item view type.
         * This ViewHolder must be fully rebound to different data.
         * 无效标志位
         */
        static final int FLAG_INVALID = 1 << 2;
        //判断ViewHolder是否无效
        boolean isInvalid() {
            //将当前ViewHolder对象的flag和无效标志位做位与操作
            return (mFlags & FLAG_INVALID) != 0;
        }
        //判断ViewHolder是否被绑定
        boolean isBound() {
            //将当前ViewHolder对象的flag和绑定标志位做位与操作
            return (mFlags & FLAG_BOUND) != 0;
        }
        /**
         * 将ViewHolder重置
         */
        void resetInternal() {
            //将ViewHolder的flag置0
            mFlags = 0;
            mPosition = NO_POSITION;
            mOldPosition = NO_POSITION;
            mItemId = NO_ID;
            mPreLayoutPosition = NO_POSITION;
            mIsRecyclableCount = 0;
            mShadowedHolder = null;
            mShadowingHolder = null;
            clearPayload();
            mWasImportantForAccessibilityBeforeHidden = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
            mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
            clearNestedRecyclerViewIfNotNested(this);
        }
}

温故知新,回看 mRecyclerPool复用逻辑时,发现在成功获得ViewHolder对象后,立即对其重置(将flag置0)。这样就满足了绑定数据的判断条件(因为0和非0位与之后必然为0)。
同样的,在才mAttachedScrap中获取ViewHolder时,只有当其是有效的才会返回。所以猜测成立:mRecyclerPool中复用的ViewHolder需要重新绑定数据,从mAttachedScrap中复用的ViewHolder不要重新出创建也不需要重新绑定数据

总结


  1. RecyclerView中,并不是每次绘制表项,都会重新创建ViewHolder对象,也不是每次都会重新绑定ViewHolder数据。
  2. RecyclerView通过Recycler获得下一个待绘制表项。
  3. Recycler有4个层次用于缓存ViewHolder对象,优先级从高到底依次为ArrayList<ViewHolder> mAttachedScrapArrayList<ViewHolder> mCachedViewsViewCacheExtension mViewCacheExtensionRecycledViewPool mRecyclerPool。如果四层缓存都未命中,则重新创建并绑定ViewHolder对象
  4. RecycledViewPoolViewHolderviewType分类存储(通过SparseArray),同类ViewHolder存储在默认大小为5的ArrayList
  5. mRecyclerPool中复用的ViewHolder需要重新绑定数据,从mAttachedScrap中复用的ViewHolder不要重新出创建也不需要重新绑定数据

这篇文章粗略的回答了关于“复用”的4个问题,即“复用什么?”、“从哪里获得复用?”,“什么时候复用?”,“复用优先级”读到这里,可能会有很多疑问:

  1. scrap view是什么?
  2. changed scrap viewattached scrap view有什么区别?
  3. 复用的ViewHolder是在什么时候被缓存的?
  4. 为什么要4层缓存?它们的用途有什么区别?

分析完“复用”,后续文章会进一步分析“回收”,希望到时候这些问题都能迎刃而解。

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

推荐阅读更多精彩内容