RecyclerView
做为一个很是惹人爱的控件,有一部分的功劳归于它优秀的缓存机制。RecyclerView
的缓存机制属于RecyclerView
的核心部分,同时也是比较难的部分。尽管缓存机制那么难,可是仍是不能抵挡得住咱们的好奇心😂。今天咱们来看看它的神奇之处。数组
本文参考资料:缓存
因为本文跟本系列的前两篇文章都有关联,因此为了便于理解,能够去看做者本系列的前两篇文章。bash
注意,本文全部的代码都来自于27.1.1。数据结构
在正式分析源码以前,我先对缓存机制作一个概述,同时也会对一些概念进行统一解释,这些对后面的分析有很大的帮助,由于若是不理解这些概念的话,后面容易看得雨里雾里的。ide
首先,我将RecyclerView
的缓存分为四级,可能有的人将它分为三级,这些看我的的理解。这里统一说明一下每级缓存的意思。源码分析
缓存级别 | 实际变量 | 含义 |
---|---|---|
一级缓存 | mAttachedScrap 和mChangedScrap |
这是优先级最高的缓存,RecyclerView 在获取ViewHolder 时,优先会到这两个缓存来找。其中mAttachedScrap 存储的是当前还在屏幕中的ViewHolder ,mChangedScrap 存储的是数据被更新的ViewHolder ,好比说调用了Adapter 的notifyItemChanged 方法。可能有人对这两个缓存仍是有点疑惑,不要急,待会会详细的解释。 |
二级缓存 | mCachedViews |
默认大小为2,一般用来存储预取的ViewHolder ,同时在回收ViewHolder 时,也会可能存储一部分的ViewHolder ,这部分的ViewHolder 一般来讲,意义跟一级缓存差很少。 |
三级缓存 | ViewCacheExtension |
自定义缓存,一般用不到,在本文中先忽略 |
四级缓存 | RecyclerViewPool |
根据ViewType 来缓存ViewHolder ,每一个ViewType 的数组大小为5,能够动态的改变。 |
如上表,统一的解释了每一个缓存的含义和做用。在这里,我再来对其中的几个缓存作一个详细的解释。布局
mAttachedScrap
:上表中说,它表示存储的是当前还在屏幕中ViewHolder
。其实是从屏幕上分离出来的ViewHolder
,可是又即将添加到屏幕上去的ViewHolder
。好比说,RecyclerView
上下滑动,滑出一个新的Item
,此时会从新调用LayoutManager
的onLayoutChildren
方法,从而会将屏幕上全部的ViewHolder
先scrap
掉(含义就是废弃掉),添加到mAttachedScrap
里面去,而后在从新布局每一个ItemView
时,会从优先mAttachedScrap
里面获取,这样效率就会很是的高。这个过程不会从新onBindViewHolder
。mCachedViews
:默认大小为2,不过一般是3,3由默认的大小2 + 预取的个数1。因此在RecyclerView
在首次加载时,mCachedViews
的size
为3(这里以LinearLayoutManager
的垂直布局为例)。一般来讲,能够经过RecyclerView
的setItemViewCacheSize
方法设置大小,可是这个不包括预取大小;预取大小经过LayoutManager
的setItemPrefetchEnabled
方法来控制。
咱们在看RecyclerView
的源码时,可能处处都能看到调用ViewHolder
的isInvalid
、isRemoved
、isBound
、isTmpDetached
、isScrap
和isUpdated
这几个方法。这里我统一的解释一下。post
方法名 | 对应的Flag | 含义或者状态设置的时机 |
---|---|---|
isInvalid |
FLAG_INVALID |
表示当前ViewHolder 是否已经失效。一般来讲,在3种状况下会出现这种状况:1.调用了Adapter 的notifyDataSetChanged 方法;2. 手动调用RecyclerView 的invalidateItemDecorations 方法;3. 调用RecyclerView 的setAdapter 方法或者swapAdapter 方法。 |
isRemoved |
FLAG_REMOVED |
表示当前的ViewHolder 是否被移除。一般来讲,数据源被移除了部分数据,而后调用Adapter 的notifyItemRemoved 方法。 |
isBound |
FLAG_BOUND |
表示当前ViewHolder 是否已经调用了onBindViewHolder 。 |
isTmpDetached |
FLAG_TMP_DETACHED |
表示当前的ItemView 是否从RecyclerView (即父View )detach 掉。一般来讲有两种状况下会出现这种状况:1.手动了RecyclerView 的detachView 相关方法;2. 在从mHideViews 里面获取ViewHolder ,会先detach 掉这个ViewHolder 关联的ItemView 。这里又多出来一个mHideViews ,待会我会详细的解释它是什么。 |
isScrap |
无Flag来表示该状态,用mScrapContainer 是否为null来判断 |
表示是否在mAttachedScrap 或者mChangedScrap 数组里面,进而表示当前ViewHolder 是否被废弃。 |
isUpdated |
FLAG_UPDATE |
表示当前ViewHolder 是否已经更新。一般来讲,在3种状况下会出现状况:1.isInvalid 方法存在的三种状况;2.调用了Adapter 的onBindViewHolder 方法;3. 调用了Adapter 的notifyItemChanged 方法 |
在四级缓存中,咱们并无将mHiddenViews
算入其中。由于mHiddenViews
只在动画期间才会有元素,当动画结束了,天然就清空了。因此mHiddenViews
并不算入4级缓存中。fetch
这里还有一个问题,就是上面在解释mChangedScrap
时,也在说,当调用Adapter
的notifyItemChanged
方法,会将更新了的ViewHolder
反放入mChangedScrap
数组里面。那究竟是放入mChangedScrap
仍是mHiddenViews
呢?同时可能有人对mChangedScrap
和mAttachedScrap
有疑问,这里我作一个统一的解释:动画
首先,若是调用了
Adapter
的notifyItemChanged
方法,会从新回调到LayoutManager
的onLayoutChildren
方法里面,而在onLayoutChildren
方法里面,会将屏幕上全部的ViewHolder
回收到mAttachedScrap
和mChangedScrap
。这个过程就是将ViewHolder
分别放到mAttachedScrap
和mChangedScrap
,而什么条件下放在mAttachedScrap
,什么条件放在mChangedScrap
,这个就是他们俩的区别。
接下来咱们来看一段代码,就能分清mAttachedScrap
和mChangedScrap
的区别了
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
。从上面的代码,咱们得出:
mAttachedScrap
里面放的是两种状态的ViewHolder
:1.被同时标记为remove
和invalid
;2.彻底没有改变的ViewHolder
。这里还有第三个判断,这个跟RecyclerView
的ItemAnimator
有关,若是ItemAnimator
为空或者ItemAnimator
的canReuseUpdatedViewHolder
方法为true,也会放入到mAttachedScrap
。那正常状况下,什么状况返回为true呢?从SimpleItemAnimator
的源码能够看出来,当ViewHolder
的isInvalid
方法返回为true时,会放入到mAttachedScrap
里面。也就是说,若是ViewHolder
失效了,也会放到mAttachedScrap
里面。- 那么
mChangedScrap
里面放什么类型flag的ViewHolder
呢?固然是ViewHolder
的isUpdated
方法返回为true时,会放入到mChangedScrap
里面去。因此,调用Adapter
的notifyItemChanged
方法时,而且RecyclerView
的ItemAnimator
不为空,会放入到mChangedScrap
里面。
了解了mAttachedScrap
和mChangedScrap
的区别以后,接下咱们来看Scrap
数组和mHiddenViews
的区别。
mHiddenViews
只存放动画的ViewHolder
,动画结束了天然就清空了。之因此存在mHiddenViews
这个数组,我猜想是存在动画期间,进行复用的可能性,此时就能够在mHiddenViews
进行复用了。而Scrap
数组跟mHiddenViews
二者彻底不冲突,因此存在一个ViewHolder
同时在Scrap
数组和mHiddenViews
的可能性。可是这并不影响,由于在动画结束时,会从mHiddenViews
里面移除。
本文在分析RecyclerView
的换出机制时,打算从两个大方面入手:1.复用;2.回收。
咱们先来看看复用的部分逻辑,由于只有理解了RecyclerView
到底是如何复用的,对回收才能更加明白。
RecyclerView
对ViewHolder
的复用,咱们得从LayoutState
的next
方法开始。LayoutManager
在布局itemView
时,须要获取一个ViewHolder
对象,就是经过这个方法来获取,具体的复用逻辑也是在这个方面开始调用的。咱们来看看:
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
方法。因此,RecyclerView
真正复用的核心就在这个方法,咱们今天来详细的分析一下这个方法。
经过这种方式来获取优先级比较高,由于每一个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; } } } 复制代码
如上的代码分为两步:
- 从
mChangedScrap
里面去获取ViewHolder
,这里面存储的是更新的ViewHolder
。- 分别
mAttachedScrap
、mHiddenViews
、mCachedViews
获取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; } } } 复制代码
这一步理解起来比较容易,分别从mAttachedScrap
、 mHiddenViews
、mCachedViews
获取ViewHolder
。可是咱们须要的是,若是获取的ViewHolder
是无效的,得作一些清理操做,而后从新放入到缓存里面,具体对应的缓存就是mCacheViews
和RecyclerViewPool
。recycleViewHolderInternal
方法就是回收ViewHolder
的方法,后面再分析回收相关的逻辑会重点分析这个方法,这里就不进行追究了。
前面分析了经过Position的方式来获取ViewHolder
,这里咱们来分析一下第二种方式--ViewType
。不过在这里,我先对前面的方式作一个简单的总结,RecyclerView
经过Position
来获取ViewHolder
,并不须要判断ViewType
是否合法,由于若是可以经过Position
来获取ViewHolder
,ViewType
自己就是正确对应的。
而这里经过ViewType
来获取ViewHolder
表示,此时ViewHolder
缓存的Position
已经失效了。ViewType
方式来获取ViewHolder
的过程,我将它分为3步:
- 若是
Adapter
的hasStableIds
方法返回为true,优先经过ViewType
和id
两个条件来寻找。若是没有找到,那么就进行第2步。- 若是
Adapter
的hasStableIds
方法返回为false,在这种状况下,首先会在ViewCacheExtension
里面找,若是尚未找到的话,最后会在RecyclerViewPool
里面来获取ViewHolder。- 若是以上的复用步骤都没有找到合适的
ViewHolder
,最后就会调用Adapter
的onCreateViewHolder
方法来建立一个新的ViewHolder
。
在这里,咱们须要注意的是,上面的第1步 和 第2步有前提条件,就是两个都必须比较ViewType
。接下来,我经过代码简单的分析一下每一步。
经过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
方法自己没有什么分析的必要,就是分别从mAttachedScrap
和mCachedViews
数组寻找合适的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);
}
}
}
复制代码
相信这段代码不用我来分析吧,表达的意思很是简单。
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"); } } 复制代码
上面的代码主要的目的就是调用Adapter
的createViewHolder
方法来建立一个ViewHolder
,在这个过程就是简单计算了建立一个ViewHolder
的时间。
关于复用机制的理解,咱们就到此为止。其实RecyclerView
的复用机制一点都不复杂,我以为让你们望而却步的缘由,是由于咱们不知道为何在这么作,若是了解这么作的缘由,一切都显得那么理所固然。
分析RecyclerView
的复用部分,接下来,咱们来分析一下回收部分。
回收是RecyclerView
复用机制内部很是重要。首先,有复用的过程,确定就有回收的过程;其次,同时理解了复用和回收两个过程,这能够帮助咱们在宏观上理解RecyclerView
的工做原理;最后,理解RecyclerView
在什么时候会回收ViewHolder
,这对使用RecyclerView
有很大的帮助。
其实回收的机制也没有想象中那么的难,本文打算从几个方面来分析RecyclerView
的回收过程。
- scrap数组
- mCacheViews数组
- mHiddenViews数组
- RecyclerViewPool数组
接下来,咱们将一一的分析。
关于ViewHolder
回收到scrap
数组里面,其实我在前面已经简单的分析了,重点就在于Recycler
的scrapView
方法里面。咱们来看看scrapView
在哪里被调用了。有以下两个地方:
- 在
getScrapOrHiddenOrCachedHolderForPosition
方法里面,若是从mHiddenViews
得到一个ViewHolder
的话,会先将这个ViewHolder
从mHiddenViews
数组里面移除,而后调用Recycler
的scrapView
方法将这个ViewHolder
放入到scrap
数组里面,而且标记FLAG_RETURNED_FROM_SCRAP
和FLAG_BOUNCED_FROM_HIDDEN_LIST
两个flag。- 在
LayoutManager
里面的scrapOrRecycleView
方法也会调用Recycler
的scrapView
方法。而有两种情形下会出现如此状况:1. 手动调用了LayoutManager
相关的方法;2.RecyclerView
进行了一次布局(调用了requestLayout
方法)
mCacheViews
数组做为二级缓存,回收的路径相较于一级缓存要多。关于mCacheViews数组,重点在于Recycler
的recycleViewHolderInternal
方法里面。我将mCacheViews
数组的回收路径大概分为三类,咱们来看看:
- 在从新布局回收了。这种状况主要出如今调用了
Adapter
的notifyDataSetChange
方法,而且此时Adapter
的hasStableIds
方法返回为false。从这里看出来,为何notifyDataSetChange
方法效率为何那么低,同时也知道了为何重写hasStableIds
方法能够提升效率。由于notifyDataSetChange
方法使得RecyclerView
将回收的ViewHolder
放在二级缓存,效率天然比较低。- 在复用时,从一级缓存里面获取到
ViewHolder
,可是此时这个ViewHolder
已经不符合一级缓存的特色了(好比Position失效了,跟ViewType对不齐),就会从一级缓存里面移除这个ViewHolder
,从添加到mCacheViews
里面- 当调用
removeAnimatingView
方法时,若是当前ViewHolder
被标记为remove,会调用recycleViewHolderInternal
方法来回收对应的ViewHolder
。调用removeAnimatingView
方法的时机表示当前的ItemAnimator
已经作完了。
一个ViewHolder
回收到mHiddenView
数组里面的条件比较简单,若是当前操做支持动画,就会调用到RecyclerView
的addAnimatingView
方法,在这个方法里面会将作动画的那个View
添加到mHiddenView
数组里面去。一般就是动画期间能够会进行复用,由于mHiddenViews
只在动画期间才会有元素。
RecyclerViewPool
跟mCacheViews
,都是经过recycleViewHolderInternal
方法来进行回收,因此情景与mCacheViews
差很少,只不过当不知足放入mCacheViews
时,才会放入到RecyclerViewPool
里面去。
了解了RecyclerView
的复用和回收机制以后,这个问题就变得很简单了。我从两个方面来解释缘由。
咱们先来看看复用怎么能体现hasStableIds
能提升效率呢?来看看代码:
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
复制代码
在前面经过Position
方式来获取一个ViewHolder
失败以后,若是Adapter
的hasStableIds
方法返回为true,在进行经过ViewType
方式来获取ViewHolder
时,会优先到1级或者二级缓存里面去寻找,而不是直接去RecyclerViewPool
里面去寻找。从这里,咱们能够看到,在复用方面,hasStableIds
方法提升了效率。
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会提升效率。
RecyclerView
回收和复用机制到这里分析的差很少了。这里作一个小小的总结。
- 在
RecyclerView
内部有4级缓存,每一级的缓存所表明的意思都不同,同时复用的优先也是从上到下,各自的回收也是不同。mHideenViews
的存在是为了解决在动画期间进行复用的问题。ViewHolder
内部有不少的flag,在理解回收和复用机制以前,最好是将ViewHolder
的flag梳理清楚。
最后用一张图片来结束本文的介绍。
RecyclerView
的动画机制。