ViewPager2系列:

  1. 图解RecyclerView缓存复用机制
  2. 图解RecyclerView预拉取机制
  3. 图解ViewPager2离屏加载机制(上)

ViewPager2是在RecyclerView的基础上构建而成的,意味着其能够复用RecyclerView目标的绝大部分特性,比方缓存复用机制等。

作为ViewPager2系列的第一篇,本篇的首要意图是快速遍及必要的前置知识,而内容的核心,正是前面所提到的RecyclerView的缓存复用机制。


RecyclerView,顾名思义,它会收回其列表项视图以供重用

详细而言,当一个列表项被移出屏暗地,RecyclerView并不会毁掉其视图,而是会缓存起来,以供给给新进入屏幕的列表项重用,这种重用能够:

  • 防止重复创立不必要的视图

  • 防止重复履行贵重的findViewById

从而达到的改进功能、提高运用呼应能力、下降功耗的作用。而要了解其间的作业原理,咱们还得回到RecyclerView是怎么构建动态列表的这一步。

RecyclerView是怎么构建动态列表的?

与RecyclerView构建动态列表相相关的几个重要类中,Adapter与ViewHolder负责配合运用,一起定义RecyclerView列表项数据的展现方式,其间:

  • ViewHolder是一个包含列表项视图(itemView)的封装容器,一起也是RecyclerView缓存复用的首要目标

  • Adapter则供给了数据<->视图 的“绑定”联系,其包含以下几个关键办法:

    • onCreateViewHolder:负责创立并初始化ViewHolder及其相关的视图,但不会填充视图内容。
    • onBindViewHolder:负责提取适当的数据,填充ViewHolder的视图内容。

然而,这2个办法并非每一个进入屏幕的列表项都会回调,相反,因为视图创立及findViewById履行等动作都首要集中在这2个办法,每次都要回调的话反而功率不佳。因而,咱们应该经过对ViewHolder目标积极地缓存复用,来尽量削减对这2个办法的回调频次。

  1. 最优状况是——取得的缓存目标正好是原先的ViewHolder目标,这种状况下既不需要从头创立该目标,也不需要从头绑定数据,即拿即用。

  2. 次优状况是——取得的缓存目标虽然不是原先的ViewHolder目标,但因为二者的列表项类型(itemType)相同,其相关的视图能够复用,因而只需要从头绑定数据即可。

  3. 最后真实没办法了,才需要履行这2个办法的回调,即创立新的ViewHolder目标并绑定数据。

实际上,这也是RecyclerView从缓存中查找最佳匹配ViewHolder目标时所遵循的优先级次序。而真实负责履行这项查找作业的,则是RecyclerView类中一个被称为收回者的内部类——Recycler

Recycler是怎么查找ViewHolder目标的?


    /**
     * ...
     * 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}.
     * 
     * 当调用getViewForPosition(int)办法时,Recycler会查看attached scrap和一级缓存(指的是mCachedViews)以找到匹配的View。 
     * 假如找不到适宜的View,Recycler会先调用ViewCacheExtension的getViewForPositionAndType(RecyclerView.Recycler, int, int)办法,再查看RecycledViewPool目标。
     * ...
     */
    public abstract static class ViewCacheExtension {
        ...
    }
    public final class Recycler {
        ...
        /**
         * Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
         * cache, the RecycledViewPool, or creating it directly.
         * 
         * 测验经过从Recycler scrap缓存、RecycledViewPool查找或直接创立的方式来获取指定方位的ViewHolder。
         * ...
         */
        @Nullable
        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
            if (mState.isPreLayout()) {
                // 0 测验从mChangedScrap中获取ViewHolder目标
                holder = getChangedScrapViewForPosition(position);
                ...
            }
            if (holder == null) {
                // 1.1 测验依据position从mAttachedScrap或mCachedViews中获取ViewHolder目标
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                ...
            }
            if (holder == null) {
                ...
                final int type = mAdapter.getItemViewType(offsetPosition);
                if (mAdapter.hasStableIds()) {
                    // 1.2 测验依据id从mAttachedScrap或mCachedViews中获取ViewHolder目标
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    ...
                }
                if (holder == null && mViewCacheExtension != null) {
                    // 2 测验从mViewCacheExtension中获取ViewHolder目标
                    final View view = mViewCacheExtension
                            .getViewForPositionAndType(this, position, type);
                    if (view != null) {
                        holder = getChildViewHolder(view);
                        ...
                    }
                }
                if (holder == null) { // fallback to pool
                    // 3 测验从mRecycledViewPool中获取ViewHolder目标
                    holder = getRecycledViewPool().getRecycledView(type);
                    ...
                }
                if (holder == null) {
                    // 4.1 回调createViewHolder办法创立ViewHolder目标及其相关的视图
                    holder = mAdapter.createViewHolder(RecyclerView.this, type);
                    ...
                }
            }
            if (mState.isPreLayout() && holder.isBound()) {
                ...
            } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
                ...
                // 4.1 回调bindViewHolder办法提取数据填充ViewHolder的视图内容
                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            }
            ...
            return holder;
        }
        ...
    }    

结合RecyclerView类中的源码及注释可知,Recycler会顺次从mChangedScrap/mAttachedScrap、mCachedViews、mViewCacheExtension、mRecyclerPool中测验获取指定方位或ID的ViewHolder目标以供重用,假如全都获取不到则直接从头创立。这其间涉及的几层缓存结构别离是:

mChangedScrap/mAttachedScrap

mChangedScrap/mAttachedScrap首要用于暂时寄存仍在当时屏幕可见、但被标记为「移除」或「重用」的列表项,其均以ArrayList的方式持有着每个列表项的ViewHolder目标,巨细无清晰约束,但一般来讲,其最大数便是屏幕内总的可见列表项数。

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

但问题来了,既然是当时屏幕可见的列表项,为什么还需要缓存呢?又是什么时候列表项会被标记为「移除」或「重用」的呢?

这2个缓存结构实际上更多是为了防止出现像部分刷新这一类的操作,导致一切的列表项都需要重绘的情形。

区别在于,mChangedScrap首要的运用场景是:

  1. 敞开了列表项动画(itemAnimator),并且列表项动画的canReuseUpdatedViewHolder(ViewHolder viewHolder)办法回来false的前提下;
  2. 调用了notifyItemChanged、notifyItemRangeChanged这一类办法,告诉列表项数据发生变化;
    boolean canReuseUpdatedViewHolder(ViewHolder viewHolder) {
        return mItemAnimator == null || mItemAnimator.canReuseUpdatedViewHolder(viewHolder,
                viewHolder.getUnmodifiedPayloads());
    }
    public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
            @NonNull List<Object> payloads) {
        return canReuseUpdatedViewHolder(viewHolder);
    }
    public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder) {
        return true;
    }

canReuseUpdatedViewHolder办法的回来值表明的不同含义如下:

  • true,表明能够重用原先的ViewHolder目标
  • false,表明应该创立该ViewHolder的副本,以便itemAnimator利用两者来实现动画作用(例如交叉淡入淡出作用)。

简单讲便是,mChangedScrap首要是为列表项数据发生变化时的动画作用服务的

mAttachedScrap应对的则是剩余的绝大部分场景,比方:

  • 像notifyItemMoved、notifyItemRemoved这种列表项发生移动,但列表项数据本身没有发生变化的场景。
  • 封闭了列表项动画,或者列表项动画的canReuseUpdatedViewHolder办法回来true,即允许重用原先的ViewHolder目标的场景。

下面以一个简单的notifyItemRemoved(int position)操作为例来演示:

notifyItemRemoved(int position)办法用于告诉观察者,从前坐落position的列表项已被移除, 其往后的列表项position都将往前移动1位。

为了简化问题、便利演示,咱们的典范将会居于以下约束:

  • 列表项总个数没有铺满整个屏幕——意味着不会触发mCachedViews、mRecyclerPool等结构的缓存操作
  • 去除列表项动画——意味着调用notifyItemRemoved后RecyclerView只会从头布局子视图一次
  recyclerView.itemAnimator = null

理想状况下,调用notifyItemRemoved(int position)办法后,应只要坐落position的列表项会被移除,其他的列表项,无论是坐落position之前或之后,都最多只会调整position值,而不该发生视图的从头创立或数据的从头绑定,即不该该回调onCreateViewHolder与onBindViewHolder这2个办法。

为此,咱们就需要将当时屏幕内的可见列表项暂时从当时屏幕剥离,暂时缓存到mAttachedScrap这个结构中去。

这10张图拿去,别再说学不会RecyclerView的缓存复用机制了!

等到RecyclerView从头开始布局显现其子视图后,再遍历mAttachedScrap找到对应position的ViewHolder目标进行复用。

这10张图拿去,别再说学不会RecyclerView的缓存复用机制了!

mCachedViews

mCachedViews首要用于寄存已被移出屏幕、但有或许很快从头进入屏幕的列表项。其相同是以ArrayList的方式持有着每个列表项的ViewHolder目标,默许巨细约束为2。

    final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
    int mViewCacheMax = DEFAULT_CACHE_SIZE;
    static final int DEFAULT_CACHE_SIZE = 2;

比方像朋友圈这种按更新时刻的先后次序展现的Feed流,咱们常常会在快速滑动中确定是否有自己感兴趣的内容,当意识到方才滑走的内容或许比较风趣时,咱们往往就会将上一条内容从头滑回来查看。

这种场景下咱们追求的天然是上一条内容展现的实时性与完整性,而不该让用户发生“才滑走那么一瞬间又要从头加载”的诉苦,也即相同不该发生视图的从头创立或数据的从头绑定。

咱们用几张流程示意图来演示这种状况:

相同为了简化问题、便利描绘,咱们的典范将会居于以下约束:

  • 封闭预拉取——意味着之后向上滑动时,都不会再预拉取「待进入屏幕区域」的一个列表项放入mCachedView了
recyclerView.layoutManager?.isItemPrefetchEnabled = false
  • 只存在一种类型的列表项,即一切列表项的itemType相同,默许都为0。

咱们将图中的列表项分成了3块区域,别离是被滑出屏幕之外的区域、屏幕内的可见区域、跟着滑动手势待进入屏幕的区域。

这10张图拿去,别再说学不会RecyclerView的缓存复用机制了!

  1. 当position=0的列表项跟着向上滑动的手势被移出屏暗地,因为mCachedViews初始容量为0,因而可直接放入;

这10张图拿去,别再说学不会RecyclerView的缓存复用机制了!

  1. 当position=1的列表项相同被移出屏暗地,因为未达到mCachedViews的默许容量巨细约束,因而也可持续放入;

这10张图拿去,别再说学不会RecyclerView的缓存复用机制了!

  1. 此刻改为向下滑动,position=1的列表项从头进入屏幕,Recycler就会顺次从mAttachedScrap、mCachedViews查找可重用于此方位的ViewHolder目标;

  2. mAttachedScrap不是应对这种状况的,天然找不到。而mCachedViews会遍历自身持有的ViewHolder目标,对比ViewHolder目标的position值与待复用方位的position值是否共同,是的话就会将ViewHolder目标从mCachedViews中移除并回来;

  3. 此处拿到的ViewHolder目标即可直接复用,即契合前面所述的最优状况

这10张图拿去,别再说学不会RecyclerView的缓存复用机制了!

  1. 另外,跟着position=1的列表项从头进入屏幕,position=7的列表项也会被移出屏幕,该方位的列表项相同会进入mCachedViews,即RecyclerView是双向缓存的。

这10张图拿去,别再说学不会RecyclerView的缓存复用机制了!

mViewCacheExtension

mViewCacheExtension首要用于供给额定的、可由开发人员自在操控的缓存层级,属于非常规运用的状况,因而这里暂不展开讲。

mRecyclerPool

mRecyclerPool首要用于按不同的itemType别离寄存超出mCachedViews约束的、被移出屏幕的列表项,其会先以SparseArray区别不同的itemType,然后每种itemType对应的值又以ArrayList的方式持有着每个列表项的ViewHolder目标,每种itemType的ArrayList巨细约束默许为5。

    public static class RecycledViewPool {
        private static final int DEFAULT_MAX_SCRAP = 5;
        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<>();
        ...
    }

因为mCachedViews默许的巨细约束仅为2,因而,当滑出屏幕的列表项超越2个后,就会依照先进先出的次序,顺次将ViewHolder目标从mCachedViews移出,并按itemType放入RecycledViewPool中的不同ArrayList。

这种缓存结构首要考虑的是跟着被滑出屏幕列表项的增多,以及被滑出距离的越来越远,从头进入屏幕内的或许性也随之下降。所以Recycler就在时刻与空间上做了一个权衡,允许相同itemType的ViewHolder被提取复用,只需要从头绑定数据即可。

这样一来,既能够防止无限增加的ViewHolder目标缓存挤占了原本就严重的内存空间,又能够削减回调相比较之下履行代价愈加贵重的onCreateViewHolder办法。

相同咱们用几张流程示意图来演示这种状况,这些示意图将在前面的mCachedViews示意图基础上持续操作:

  1. 假定现在存在于mCachedViews中的仍是position=0及position=1这两个列表项。

  2. 当咱们持续向上滑动时,position=2的列表项会测验进入mCachedViews,因为超出了mCachedViews的容量约束,position=0的列表项会从mCachedViews中被移出,并放入RecycledViewPool中itemType为0的ArrayList,即图中的状况①;

  3. 一起,底部的一个新的列表项也将跟着滑动手势进入到屏幕内,但因为此刻mAttachedScrap、mCachedViews、mRecyclerPool均没有适宜的ViewHolder目标能够供给给其复用,因而该列表项只能履行onCreateViewHolder与onBindViewHolder这2个办法的回调,即图中的状况②;

这10张图拿去,别再说学不会RecyclerView的缓存复用机制了!

  1. 等到position=2的列表项被完全移出了屏暗地,也就顺畅进入了mCachedViews中。

这10张图拿去,别再说学不会RecyclerView的缓存复用机制了!

  1. 咱们持续保持向上滑动的手势,此刻,因为下一个待进入屏幕的列表项与position=0的列表项的itemType相同,因而咱们能够在走到从mRecyclerPool查找适宜的ViewHolder目标这一步时,依据itemType找到对应的ArrayList,再取出其间的1个ViewHolder目标进行复用,即图中的状况①。

  2. 因为itemType类型共同,其相关的视图能够复用,因而只需要从头绑定数据即可,即契合前面所述的次优状况

这10张图拿去,别再说学不会RecyclerView的缓存复用机制了!

  1. ②③ 状况与前面的共同,此处不再赘余。

最后总结一下,

RecyclerView缓存复用机制
目标 ViewHolder(包含列表项视图(itemView)的封装容器)
意图 削减对onCreateViewHolder、onBindViewHolder这2个办法的回调
好处 1.防止重复创立不必要的视图 2.防止重复履行贵重的findViewById
作用 改进功能、提高运用呼应能力、下降功耗
核心类 Recycler、RecyclerViewPool
缓存结构 mChangedScrap/mAttachedScrap、mCachedViews、mViewCacheExtension、mRecyclerPool
缓存结构 容器类型 容量约束 缓存用途 优先级次序(数值越小,优先级越高)
mChangedScrap/mAttachedScrap ArrayList 无,一般为屏幕内总的可见列表项数 暂时寄存仍在当时屏幕可见、但被标记为「移除」或「重用」的列表项 0
mCachedViews ArrayList 默许为2 寄存已被移出屏幕、但有或许很快从头进入屏幕的列表项 1
mViewCacheExtension 开发者自己定义 供给额定的可由开发人员自在操控的缓存层级 2
mRecyclerPool SparseArray<ArrayList> 每种itemType默许为5 按不同的itemType别离寄存超出mCachedViews约束的、被移出屏幕的列表项 3

以上的便是RecyclerView缓存复用机制的核心内容了。

ViewPager2系列的第二篇已产出,喜爱此系列的可移步阅览:/post/718197…

少侠,请留步!若本文对你有所协助或启发,还请:

  1. 点赞,让更多的人能看到!
  2. 保藏⭐️,好文值得反复品尝!
  3. 重视➕,不错过每一次更文!

===> 技能号:「星际码仔」

你的支撑是我持续创造的动力,感谢!