RecyclerView 源码分析(三) - RecyclerView的缓存机制

3,048 阅读17分钟

  RecyclerView作为一个非常惹人爱的控件,有一部分的功劳归于它优秀的缓存机制。RecyclerView的缓存机制属于RecyclerView的核心部分,同时也是比较难的部分。尽管缓存机制那么难,但是还是不能抵挡得住我们的好奇心😂。今天我们来看看它的神奇之处。

  本文参考资料:

  1. RecyclerView缓存原理,有图有真相
  2. 【进阶】RecyclerView源码解析(二)——缓存机制
  3. 深入 RecyclerView 源码探究四:回收复用和动画
  4. 手摸手第二弹,可视化 RecyclerView 缓存机制
  5. RecyclerView 源码分析(一) - RecyclerView的三大流程

  由于本文跟本系列的前两篇文章都有关联,所以为了便于理解,可以去看作者本系列的前两篇文章。

  注意,本文所有的代码都来自于27.1.1。

1. 概述

  在正式分析源码之前,我先对缓存机制做一个概述,同时也会对一些概念进行统一解释,这些对后面的分析有很大的帮助,因为如果不理解这些概念的话,后面容易看得雨里雾里的。

(1).四级缓存

  首先,我将RecyclerView的缓存分为四级,可能有的人将它分为三级,这些看个人的理解。这里统一说明一下每级缓存的意思。

缓存级别 实际变量 含义
一级缓存 mAttachedScrapmChangedScrap 这是优先级最高的缓存,RecyclerView在获取ViewHolder时,优先会到这两个缓存来找。其中mAttachedScrap存储的是当前还在屏幕中的ViewHoldermChangedScrap存储的是数据被更新的ViewHolder,比如说调用了AdapternotifyItemChanged方法。可能有人对这两个缓存还是有点疑惑,不要急,待会会详细的解释。
二级缓存 mCachedViews 默认大小为2,通常用来存储预取的ViewHolder,同时在回收ViewHolder时,也会可能存储一部分的ViewHolder,这部分的ViewHolder通常来说,意义跟一级缓存差不多。
三级缓存 ViewCacheExtension 自定义缓存,通常用不到,在本文中先忽略
四级缓存 RecyclerViewPool 根据ViewType来缓存ViewHolder,每个ViewType的数组大小为5,可以动态的改变。

  如上表,统一的解释了每个缓存的含义和作用。在这里,我再来对其中的几个缓存做一个详细的解释。

  1. mAttachedScrap:上表中说,它表示存储的是当前还在屏幕中ViewHolder。实际上是从屏幕上分离出来的ViewHolder,但是又即将添加到屏幕上去的ViewHolder。比如说,RecyclerView上下滑动,滑出一个新的Item,此时会重新调用LayoutManageronLayoutChildren方法,从而会将屏幕上所有的ViewHolderscrap掉(含义就是废弃掉),添加到mAttachedScrap里面去,然后在重新布局每个ItemView时,会从优先mAttachedScrap里面获取,这样效率就会非常的高。这个过程不会重新onBindViewHolder
  2. mCachedViews:默认大小为2,不过通常是3,3由默认的大小2 + 预取的个数1。所以在RecyclerView在首次加载时,mCachedViewssize为3(这里以LinearLayoutManager的垂直布局为例)。通常来说,可以通过RecyclerViewsetItemViewCacheSize方法设置大小,但是这个不包括预取大小;预取大小通过LayoutManagersetItemPrefetchEnabled方法来控制。

(2).ViewHolder的几个状态值

  我们在看RecyclerView的源码时,可能到处都能看到调用ViewHolderisInvalidisRemovedisBoundisTmpDetachedisScrapisUpdated这几个方法。这里我统一的解释一下。

方法名 对应的Flag 含义或者状态设置的时机
isInvalid FLAG_INVALID 表示当前ViewHolder是否已经失效。通常来说,在3种情况下会出现这种情况:1.调用了AdapternotifyDataSetChanged方法;2. 手动调用RecyclerViewinvalidateItemDecorations方法;3. 调用RecyclerViewsetAdapter方法或者swapAdapter方法。
isRemoved FLAG_REMOVED 表示当前的ViewHolder是否被移除。通常来说,数据源被移除了部分数据,然后调用AdapternotifyItemRemoved方法。
isBound FLAG_BOUND 表示当前ViewHolder是否已经调用了onBindViewHolder
isTmpDetached FLAG_TMP_DETACHED 表示当前的ItemView是否从RecyclerView(即父View)detach掉。通常来说有两种情况下会出现这种情况:1.手动了RecyclerViewdetachView相关方法;2. 在从mHideViews里面获取ViewHolder,会先detach掉这个ViewHolder关联的ItemView。这里又多出来一个mHideViews,待会我会详细的解释它是什么。
isScrap 无Flag来表示该状态,用mScrapContainer是否为null来判断 表示是否在mAttachedScrap或者mChangedScrap数组里面,进而表示当前ViewHolder是否被废弃。
isUpdated FLAG_UPDATE 表示当前ViewHolder是否已经更新。通常来说,在3种情况下会出现情况:1.isInvalid方法存在的三种情况;2.调用了AdapteronBindViewHolder方法;3. 调用了AdapternotifyItemChanged方法

(3). ChildHelper的mHiddenViews

  在四级缓存中,我们并没有将mHiddenViews算入其中。因为mHiddenViews只在动画期间才会有元素,当动画结束了,自然就清空了。所以mHiddenViews并不算入4级缓存中。

  这里还有一个问题,就是上面在解释mChangedScrap时,也在说,当调用AdapternotifyItemChanged方法,会将更新了的ViewHolder反放入mChangedScrap数组里面。那到底是放入mChangedScrap还是mHiddenViews呢?同时可能有人对mChangedScrapmAttachedScrap有疑问,这里我做一个统一的解释:

首先,如果调用了AdapternotifyItemChanged方法,会重新回调到LayoutManageronLayoutChildren方法里面,而在onLayoutChildren方法里面,会将屏幕上所有的ViewHolder回收到mAttachedScrapmChangedScrap。这个过程就是将ViewHolder分别放到mAttachedScrapmChangedScrap,而什么条件下放在mAttachedScrap,什么条件放在mChangedScrap,这个就是他们俩的区别。

  接下来我们来看一段代码,就能分清mAttachedScrapmChangedScrap的区别了

        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);
                mAttachedScrap.add(holder);
            } else {
                if (mChangedScrap == null) {
                    mChangedScrap = new ArrayList<ViewHolder>();
                }
                holder.setScrapContainer(this, true);
                mChangedScrap.add(holder);
            }
        }

  可能很多人初次看到这方法时,会非常的懵逼,我也是如此。今天我们就来看看这个方法。这个根本的目的就是,判断ViewHolder的flag状态,从而来决定是放入mAttachedScrap还是mChangedScrap。从上面的代码,我们得出:

  1. mAttachedScrap里面放的是两种状态的ViewHolder:1.被同时标记为removeinvalid;2.完全没有改变的ViewHolder。这里还有第三个判断,这个跟RecyclerViewItemAnimator有关,如果ItemAnimator为空或者ItemAnimatorcanReuseUpdatedViewHolder方法为true,也会放入到mAttachedScrap。那正常情况下,什么情况返回为true呢?从SimpleItemAnimator的源码可以看出来,当ViewHolderisInvalid方法返回为true时,会放入到 mAttachedScrap里面。也就是说,如果ViewHolder失效了,也会放到mAttachedScrap里面。
  2. 那么mChangedScrap里面放什么类型flag的ViewHolder呢?当然是ViewHolderisUpdated方法返回为true时,会放入到mChangedScrap里面去。所以,调用AdapternotifyItemChanged方法时,并且RecyclerViewItemAnimator不为空,会放入到mChangedScrap里面。

  了解了mAttachedScrapmChangedScrap的区别之后,接下我们来看Scrap数组和mHiddenViews的区别。

mHiddenViews只存放动画的ViewHolder,动画结束了自然就清空了。之所以存在 mHiddenViews这个数组,我猜测是存在动画期间,进行复用的可能性,此时就可以在mHiddenViews进行复用了。而Scrap数组跟mHiddenViews两者完全不冲突,所以存在一个ViewHolder同时在Scrap数组和mHiddenViews的可能性。但是这并不影响,因为在动画结束时,会从mHiddenViews里面移除。

  本文在分析RecyclerView的换出机制时,打算从两个大方面入手:1.复用;2.回收。

  我们先来看看复用的部分逻辑,因为只有理解了RecyclerView究竟是如何复用的,对回收才能更加明白。

2. 复用

  RecyclerViewViewHolder的复用,我们得从LayoutStatenext方法开始。LayoutManager在布局itemView时,需要获取一个ViewHolder对象,就是通过这个方法来获取,具体的复用逻辑也是在这个方面开始调用的。我们来看看:

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

  next方法里面其实也没做什么事,就是调用RecyclerViewgetViewForPosition方法来获取一个View的。而getViewForPosition方法最终会调用到RecyclerViewtryGetViewHolderForPositionByDeadline方法。所以,RecyclerView真正复用的核心就在这个方法,我们今天来详细的分析一下这个方法。

(1). 通过Position方式来获取ViewHolder

  通过这种方式来获取优先级比较高,因为每个ViewHolder还没被改变,通常在这种情况下,都是某一个ItemView对应的ViewHolder被更新导致的,所以在屏幕上其他的ViewHolder,可以快速对应原来的ItemView。我们来看看相关的源码。

            if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }
            // 1) Find by position from scrap/hidden list/cache
            if (holder == null) {
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                if (holder != null) {
                    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();
                            }
                            recycleViewHolderInternal(holder);
                        }
                        holder = null;
                    } else {
                        fromScrapOrHiddenOrCache = true;
                    }
                }
            }

  如上的代码分为两步:

  1. mChangedScrap里面去获取ViewHolder,这里面存储的是更新的ViewHolder
  2. 分别mAttachedScrapmHiddenViewsmCachedViews获取ViewHolder

  我们来简单的分析一下这两步。先来看看第一步。

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

  如果当前是预布局阶段,那么就从mChangedScrap里面去获取ViewHolder。那什么阶段是预布局阶段呢?这里我对预布局这个概念简单的解释。

预布局又可以称之为preLayout,当当前的RecyclerView处于dispatchLayoutStep1阶段时,称之为预布局;dispatchLayoutStep2称为真正布局的阶段;dispatchLayoutStep3称为postLayout阶段。同时要想真正开启预布局,必须有ItemAnimator,并且每个RecyclerView对应的LayoutManager必须开启预处理动画

  是不是感觉听了解释之后更加的懵逼了?为了解释一个概念,反而引出了更多的概念了?关于动画的问题,不出意外,我会在下一篇文章分析,本文就不对动画做过多的解释了。在这里,为了简单,只要RecyclerView处于dispatchLayoutStep1,我们就当做它处于预布局阶段。

  为什么只在预布局的时候才从mChangedScrap里面去取呢?   首先,我们得知道mChangedScrap数组里面放的是什么类型的 ViewHolder。从前面的分析中,我们知道,只有当ItemAnimator不为空,被changed的ViewHolder会放在mChangedScrap数组里面。因为chang动画前后相同位置上的ViewHolder是不同的,所以当预布局时,从mChangedScrap缓存里面去,而正式布局时,不会从mChangedScrap缓存里面去,这就保证了动画前后相同位置上是不同的ViewHolder。为什么要保证动画前后是不同的ViewHolder呢?这是RecyclerView动画机制相关的知识,这里就不详细的解释,后续有专门的文章来分析它,在这里,我们只需要记住,chang动画执行的有一个前提就是动画前后是不同的ViewHolder

  然后,我们再来看看第二步。

            if (holder == null) {
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                if (holder != null) {
                    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();
                            }
                            recycleViewHolderInternal(holder);
                        }
                        holder = null;
                    } else {
                        fromScrapOrHiddenOrCache = true;
                    }
                }
            }

  这一步理解起来比较容易,分别从mAttachedScrapmHiddenViewsmCachedViews获取ViewHolder。但是我们需要的是,如果获取的ViewHolder是无效的,得做一些清理操作,然后重新放入到缓存里面,具体对应的缓存就是mCacheViewsRecyclerViewPoolrecycleViewHolderInternal方法就是回收ViewHolder的方法,后面再分析回收相关的逻辑会重点分析这个方法,这里就不进行追究了。

(2). 通过viewType方式来获取ViewHolder

  前面分析了通过Position的方式来获取ViewHolder,这里我们来分析一下第二种方式--ViewType。不过在这里,我先对前面的方式做一个简单的总结,RecyclerView通过Position来获取ViewHolder,并不需要判断ViewType是否合法,因为如果能够通过Position来获取ViewHolderViewType本身就是正确对应的。

  而这里通过ViewType来获取ViewHolder表示,此时ViewHolder缓存的Position已经失效了。ViewType方式来获取ViewHolder的过程,我将它分为3步:

  1. 如果AdapterhasStableIds方法返回为true,优先通过ViewTypeid两个条件来寻找。如果没有找到,那么就进行第2步。
  2. 如果AdapterhasStableIds方法返回为false,在这种情况下,首先会在ViewCacheExtension里面找,如果还没有找到的话,最后会在RecyclerViewPool里面来获取ViewHolder。
  3. 如果以上的复用步骤都没有找到合适的ViewHolder,最后就会调用AdapteronCreateViewHolder方法来创建一个新的ViewHolder

  在这里,我们需要注意的是,上面的第1步 和 第2步有前提条件,就是两个都必须比较ViewType。接下来,我通过代码简单的分析一下每一步。

A. 通过id来寻找ViewHolder

  通过id寻找合适的ViewHolder主要是通过调用getScrapOrCachedViewForId方法来实现的,我们简单的看一下代码:

                // 2) Find from scrap/cache via stable ids, if exists
                if (mAdapter.hasStableIds()) {
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    if (holder != null) {
                        // update position
                        holder.mPosition = offsetPosition;
                        fromScrapOrHiddenOrCache = true;
                    }
                }

  而getScrapOrCachedViewForId方法本身没有什么分析的必要,就是分别从mAttachedScrapmCachedViews数组寻找合适的ViewHolder

B. 从RecyclerViewPool里面获取ViewHolder

  ViewCacheExtension存在的情况是非常的少见,这里为了简单,就不展开了(实际上我也不懂!),所以这里,我们直接来看RecyclerViewPool方式。

  在这里,我们需要了解RecyclerViewPool的数组结构。我们简单的分析一下RecyclerViewPool这个类。

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

  在RecyclerViewPool的内部,使用SparseArray来存储每个ViewType对应的ViewHolder数组,其中每个数组的最大size为5。这个数据结构是不是非常简单呢?

  简单的了解了RecyclerViewPool的数据结构,接下来我们来看看复用的相关的代码:

                if (holder == null) { // fallback to pool
                    if (DEBUG) {
                        Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
                                + position + ") fetching from shared pool");
                    }
                    holder = getRecycledViewPool().getRecycledView(type);
                    if (holder != null) {
                        holder.resetInternal();
                        if (FORCE_INVALIDATE_DISPLAY_LIST) {
                            invalidateDisplayListInt(holder);
                        }
                    }
                }

  相信这段代码不用我来分析吧,表达的意思非常简单。

C. 调用Adapter的onCreateViewHolder方法创建一个新的ViewHolder
                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;
                    }
                    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");
                    }
                }

  上面的代码主要的目的就是调用AdaptercreateViewHolder方法来创建一个ViewHolder,在这个过程就是简单计算了创建一个ViewHolder的时间。

  关于复用机制的理解,我们就到此为止。其实RecyclerView的复用机制一点都不复杂,我觉得让大家望而却步的原因,是因为我们不知道为什么在这么做,如果了解这么做的原因,一切都显得那么理所当然。

  分析RecyclerView的复用部分,接下来,我们来分析一下回收部分。

3. 回收

  回收是RecyclerView复用机制内部非常重要。首先,有复用的过程,肯定就有回收的过程;其次,同时理解了复用和回收两个过程,这可以帮助我们在宏观上理解RecyclerView的工作原理;最后,理解RecyclerView在何时会回收ViewHolder,这对使用RecyclerView有很大的帮助。

  其实回收的机制也没有想象中那么的难,本文打算从几个方面来分析RecyclerView的回收过程。

  1. scrap数组
  2. mCacheViews数组
  3. mHiddenViews数组
  4. RecyclerViewPool数组

  接下来,我们将一一的分析。

(1). scrap数组

  关于ViewHolder回收到scrap数组里面,其实我在前面已经简单的分析了,重点就在于RecyclerscrapView方法里面。我们来看看scrapView在哪里被调用了。有如下两个地方:

  1. getScrapOrHiddenOrCachedHolderForPosition方法里面,如果从mHiddenViews获得一个ViewHolder的话,会先将这个ViewHoldermHiddenViews数组里面移除,然后调用RecyclerscrapView方法将这个ViewHolder放入到scrap数组里面,并且标记FLAG_RETURNED_FROM_SCRAPFLAG_BOUNCED_FROM_HIDDEN_LIST两个flag。
  2. LayoutManager里面的scrapOrRecycleView方法也会调用RecyclerscrapView方法。而有两种情形下会出现如此情况:1. 手动调用了LayoutManager相关的方法;2. RecyclerView进行了一次布局(调用了requestLayout方法)

(2). mCacheViews数组

  mCacheViews数组作为二级缓存,回收的路径相较于一级缓存要多。关于mCacheViews数组,重点在于RecyclerrecycleViewHolderInternal方法里面。我将mCacheViews数组的回收路径大概分为三类,我们来看看:

  1. 在重新布局回收了。这种情况主要出现在调用了AdapternotifyDataSetChange方法,并且此时AdapterhasStableIds方法返回为false。从这里看出来,为什么notifyDataSetChange方法效率为什么那么低,同时也知道了为什么重写hasStableIds方法可以提高效率。因为notifyDataSetChange方法使得RecyclerView将回收的ViewHolder放在二级缓存,效率自然比较低。
  2. 在复用时,从一级缓存里面获取到ViewHolder,但是此时这个ViewHolder已经不符合一级缓存的特点了(比如Position失效了,跟ViewType对不齐),就会从一级缓存里面移除这个ViewHolder,从添加到mCacheViews里面
  3. 当调用removeAnimatingView方法时,如果当前ViewHolder被标记为remove,会调用recycleViewHolderInternal方法来回收对应的ViewHolder。调用removeAnimatingView方法的时机表示当前的ItemAnimator已经做完了。

(3). mHiddenViews数组

  一个ViewHolder回收到mHiddenView数组里面的条件比较简单,如果当前操作支持动画,就会调用到RecyclerViewaddAnimatingView方法,在这个方法里面会将做动画的那个View添加到mHiddenView数组里面去。通常就是动画期间可以会进行复用,因为mHiddenViews只在动画期间才会有元素。

(4). RecyclerViewPool

  RecyclerViewPoolmCacheViews,都是通过recycleViewHolderInternal方法来进行回收,所以情景与mCacheViews差不多,只不过当不满足放入mCacheViews时,才会放入到RecyclerViewPool里面去。

(5). 为什么hasStableIds方法返回true会提高效率呢?

  了解了RecyclerView的复用和回收机制之后,这个问题就变得很简单了。我从两个方面来解释原因。

A. 复用方面

  我们先来看看复用怎么能体现hasStableIds能提高效率呢?来看看代码:

                if (mAdapter.hasStableIds()) {
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    if (holder != null) {
                        // update position
                        holder.mPosition = offsetPosition;
                        fromScrapOrHiddenOrCache = true;
                    }
                }

  在前面通过Position方式来获取一个ViewHolder失败之后,如果AdapterhasStableIds方法返回为true,在进行通过ViewType方式来获取ViewHolder时,会优先到1级或者二级缓存里面去寻找,而不是直接去RecyclerViewPool里面去寻找。从这里,我们可以看到,在复用方面,hasStableIds方法提高了效率。

B. 回收方面
        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;
            }
            if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                    && !mRecyclerView.mAdapter.hasStableIds()) {
                removeViewAt(index);
                recycler.recycleViewHolderInternal(viewHolder);
            } else {
                detachViewAt(index);
                recycler.scrapView(view);
                mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
            }
        }

  从上面的代码中,我们可以看出,如果hasStableIds方法返回为true的话,这里所有的回收都进入scrap数组里面。这刚好与前面对应了。

  通过如上两点,我们就能很好的理解为什么hasStableIds方法返回true会提高效率。

4. 总结

  RecyclerView回收和复用机制到这里分析的差不多了。这里做一个小小的总结。

  1. RecyclerView内部有4级缓存,每一级的缓存所代表的意思都不一样,同时复用的优先也是从上到下,各自的回收也是不一样。
  2. mHideenViews的存在是为了解决在动画期间进行复用的问题。
  3. ViewHolder内部有很多的flag,在理解回收和复用机制之前,最好是将ViewHolder的flag梳理清楚。

  最后用一张图片来结束本文的介绍。

  如果不出意外的话,下一篇文章应该是分析RecyclerView的动画机制。