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

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

  本文参考资料:缓存

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

  因为本文跟本系列的前两篇文章都有关联,因此为了便于理解,能够去看做者本系列的前两篇文章。bash

  注意,本文全部的代码都来自于27.1.1。数据结构

1. 概述

  在正式分析源码以前,我先对缓存机制作一个概述,同时也会对一些概念进行统一解释,这些对后面的分析有很大的帮助,由于若是不理解这些概念的话,后面容易看得雨里雾里的。ide

(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这几个方法。这里我统一的解释一下。post

方法名 对应的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级缓存中。fetch

  这里还有一个问题,就是上面在解释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的动画机制。
相关文章
相关标签/搜索