由于本文跟本系列的前两篇文章都有关联,所以为了便于理解,可以去看作者本系列的前两篇文章。
注意,本文所有的代码都来自于27.1.1。
1. 概述
在正式分析源码之前,我先对缓存机制做一个概述,同时也会对一些概念进行统一解释,这些对后面的分析有很大的帮助,因为如果不理解这些概念的话,后面容易看得雨里雾里的。
(1).四级缓存
首先,我将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方法来控制。
(2).ViewHolder的几个状态值
我们在看RecyclerView的源码时,可能到处都能看到调用ViewHolder的isInvalid、isRemoved、isBound、isTmpDetached、isScrap和isUpdated这几个方法。这里我统一的解释一下。
| 方法名 | 对应的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方法 |
(3). ChildHelper的mHiddenViews
在四级缓存中,我们并没有将mHiddenViews算入其中。因为mHiddenViews只在动画期间才会有元素,当动画结束了,自然就清空了。所以mHiddenViews并不算入4级缓存中。
这里还有一个问题,就是上面在解释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究竟是如何复用的,对回收才能更加明白。
2. 复用
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真正复用的核心就在这个方法,我们今天来详细的分析一下这个方法。
(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;
}
}
}如上的代码分为两步:
从
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里面去取呢?
首先,我们得理解在什么情况下才算是开启了预布局。从代码上来说,只有当前RecyclerView的ItemAnimator不为null,并且当前的操作支持运行预布局的动画。如此,才算开了预布局。而在mChangedScrap只有在预布局的情况才会存储ViewHolder,这一点我们可以从之前的总结中可以得到,比如调用了Adapter的notifyItemChanged方法,如果当前RecyclerView的ItemAnimator不为空的话,就会添加到mChangedScrap数组里面。在这种情况下,肯定满足预布局的条件,所以复用时,会从mChangedScrap数组里面取。
这里说到预布局的动画,详细的可以参考:RecyclerView animations - AndroidDevSummit write-up。这是动画的知识点,本文就不分析了,后续会有相关的文章来详细的解释RecyclerView的动画。
然后,我们再来看看第二步。
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的方法,后面再分析回收相关的逻辑会重点分析这个方法,这里就不进行追究了。
(2). 通过viewType方式来获取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。接下来,我通过代码简单的分析一下每一步。
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方法本身没有什么分析的必要,就是分别从mAttachedScrap和mCachedViews数组寻找合适的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");
}
} 上面的代码主要的目的就是调用Adapter的createViewHolder方法来创建一个ViewHolder,在这个过程就是简单计算了创建一个ViewHolder的时间。
关于复用机制的理解,我们就到此为止。其实RecyclerView的复用机制一点都不复杂,我觉得让大家望而却步的原因,是因为我们不知道为什么在这么做,如果了解这么做的原因,一切都显得那么理所当然。
分析RecyclerView的复用部分,接下来,我们来分析一下回收部分。
3. 回收
回收是RecyclerView复用机制内部非常重要。首先,有复用的过程,肯定就有回收的过程;其次,同时理解了复用和回收两个过程,这可以帮助我们在宏观上理解RecyclerView的工作原理;最后,理解RecyclerView在何时会回收ViewHolder,这对使用RecyclerView有很大的帮助。
其实回收的机制也没有想象中那么的难,本文打算从几个方面来分析RecyclerView的回收过程。
scrap数组
mCacheViews数组
mHiddenViews数组
RecyclerViewPool数组
接下来,我们将一一的分析。
(1). scrap数组
关于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方法)
(2). mCacheViews数组
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已经做完了。
(3). mHiddenViews数组
一个ViewHolder回收到mHiddenView数组里面的条件比较简单,如果当前操作支持动画,就会调用到RecyclerView的addAnimatingView方法,在这个方法里面会将做动画的那个View添加到mHiddenView数组里面去。通常就是动画期间可以会进行复用,因为mHiddenViews只在动画期间才会有元素。
(4). RecyclerViewPool
RecyclerViewPool跟mCacheViews,都是通过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失败之后,如果Adapter的hasStableIds方法返回为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回收和复用机制到这里分析的差不多了。这里做一个小小的总结。
在
RecyclerView内部有4级缓存,每一级的缓存所代表的意思都不一样,同时复用的优先也是从上到下,各自的回收也是不一样。
mHideenViews的存在是为了解决在动画期间进行复用的问题。
ViewHolder内部有很多的flag,在理解回收和复用机制之前,最好是将ViewHolder的flag梳理清楚。
最后用一张图片来结束本文的介绍。

作者:琼珶和予
链接:https://www.jianshu.com/p/efe81969f69d
随时随地看视频