由于本文跟本系列的前两篇文章都有关联,所以为了便于理解,可以去看作者本系列的前两篇文章。
注意,本文所有的代码都来自于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