前言
本篇记录笔者学习ReclerView缓存机制的心路历程
一、概述
我们都知道RecyclerView无论如何都不会导致OOM的发生,而这背后依靠是其本身强大的回收复用机制,那么其回收复用机制是如何实现的呢,下面笔者记录对其的分析过程
二、基本知识
1.复用的内容
在搞清楚RecyclerView的缓存复用机制之前,我们先要清楚缓存复用机制是对什么进行复用的呢,毫无疑问不可能是我们针对每个itemViews书写的布局里面的那些控件,这里直接给出答案,缓存复用机制针对复用的内容是ViewHolder,后面的源码分析会给出分析
2.何时调用
在我们自定义书写RecyclerView适配器的过程中,难免会接触到两个方法:onCreateViewHolder()和onBindViewHolder(),前者会调用方法创建ViewHolder(),后者会调用相关方法进行数据绑定工作。 需要注意的是,如果是新创建View并填充数据,则会调用onCreateViewHolder()和onBindViewHolder()两个方法,这通常发生RecyclerView首次创建View并填充数据的过程中; 如果是屏幕滑动过程中不断出现新的ItemView显示,这一过程中通常会复用已经存在的ViewHolder,并调用onBindViewHolder() 对数据进行绑定
3.每个Item对应的状态
下面介绍ViewHolder的各个状态,相关方法如下
三、RecyclerView的四级缓存
在了解RecyclerView的几级缓存之前,我们先通过RecyclerView的官方源码构造函数了解其几级缓存的数据结构
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
private final List<ViewHolder>
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
int mViewCacheMax = DEFAULT_CACHE_SIZE;
RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
static final int DEFAULT_CACHE_SIZE = 2;
Scrap
Scrap是RecyclerView最轻量的缓存,包括mAttachedScrap和mChangedScrap,它不参与列表滚动时的回收复用,作为重新布局时的临时缓存,它的作用是,缓存当界面重新布局前和界面重新布局后都出现的ViewHolder,这些ViewHolder是无效、未移除、未标记的。在这些无效、未移除、未标记的ViewHolder之中,mAttachedScrap负责保存其中没有改变的ViewHolder;剩下的由mChangedScrap负责保存。mAttachedScrap和mChangedScrap也只是分工合作保存不同ViewHolder而已。
注意:Scrap只是作为布局的临时缓存,它和滑动时的缓存没有任何关系,它的detach和atach只是临时存在于布局过程中。布局结束时,Scrap列表应该是空的,缓存的数据要么重新布局出来,要么被清空;总之在布局结束后Scrap列表不应该存在任何东西。
上述的描述难免抽象,下面我们通过一个具体的例子进行阐述和解读
在这个案例中,我们对一个RecyclerView的数据itemA、itemB进行删除,然后itemC、itemD、itemE依次移动上来,毫无疑问这一过程会对后者itemView的布局参数产生影响,也就是对onLayout()需要的参数产生影响,在这一过程中,itemA、itemB前后的相关参数没有发生变化,因而itemA、itemB存放于mAttachedScrap中,itemC、itemD存放于mChangedScrap。
需要注意这一过程中仅仅是对屏幕上出现的item进行操作,如itemE没有出现在屏幕上,是被扔到任意一个列表中的
细致分析如下
在一个手机屏幕中,将itemB删除,并且调用notifyItemRemoved()方法,如果item是无效并且被移除的就会回收到其他的缓存,否则都是缓存到Scrap中,那么mAttachedScrap和mChangedScrap会分别存储itemView,itemA没有任何的变化,存储到mAttachedScrap中,itemB虽然被移出了,但是还有效,也被存储到mAttachedScrap中(但是会被标记REMOVED,之后会移除);itemC和itemD发生了变化,位置往上移动了,会被存储到mChangedScrap中。删除时,ABCD都会进入Scrap中;删除后,ACD都会回来,A没有任何变化,CD只是位置发生了变化,内容没有发生变化。 RecyclerView的局部刷新就是依赖Scrap的临时缓存,当我们通过notifyItemRemoved(),notifyItemChanged()通知item发生变化的时候,通过mAttachedScrap缓存没有发生变化的ViewHolder,其他的则由mChangedScrap缓存,添加itemView的时候快速从里面取出,完成局部刷新。注意,如果我们使用notifyDataSetChanged()来通知RecyclerView刷新,屏幕上的itemView被标记为FLAG_INVALID并且未被移除,所以不会使用Scrap缓存,而是直接扔到CacheView或者RecycledViewPool池中,回来的时候重新走一次绑定数据。
CacheView
CacheView用于RecyclerView列表位置产生变动时,对刚刚移出屏幕的view进行回收复用。根据position/id来精准匹配是不是原来的item,如果是则直接返回使用,不需要重新绑定数据;如果不是则去RecycledViewPool中找holder实例返回,并且重新绑定数据。 CacheView的最大容量为2,缓存一个新的ViewHolder时,如果超出了最大限制,那么会将CacheView缓存的第一个数据添加到RecycledViewPool后再移除掉,最后才会将新的ViewHolder添加进来。我们在滑动RecyclerView的时候,Recycler会不断地缓存刚刚移出屏幕不可见的View到CacheView中,CacheView到达上限时又会不断替换CacheView中旧的ViewHolder,将它们扔到RecycledViewPool中。如果一直朝一个方向滚动,CacheView并没有在效率上产生帮助,它只是把后面滑过的ViewHolder缓存起来,缓存到RecycledViewPool中,如果经常来回滑动,那么从CacheView根据对应位置的item直接复用,不需要重新绑定数据,将会得到很好的利用。
下面来看一个CacheView的应用,如图,itemA先移动出屏幕,然后移入到CacheView中,向下滑动或向上滑动的过程中对移入item进行判定,根据position/id确定是刚才的itemA,则直接从CacheView中进行移入
ViewCacheExtension
ViewCacheExtension是缓存拓展的帮助类,额外提供了一层缓存池给开发者。开发者视情况而定是否使用ViewCacheExtension增加一层缓存池,Recycler首先去scrap和CacheView中寻找复用view,如果没有就去ViewCacheExtension中寻找View,如果还是没有找到,那么最后去RecycledViewPool寻找复用的View。
在日常的开发中,一般我们使用不到ViewCacheExtension,所以这里就简略带过了
RecycledViewPool
在Scrap、CacheView、ViewCacheExtension都不愿意回收的时候,都会丢到RecycledViewPool中回收,所以RecycledViewPool是Recycler的终极回收站。 RecycledViewPool实际上是以SparseArray嵌套一个ArraryList的形式保存ViewHolder的,因为RecycledViewPool保存的ViewHolder是以itemType来区分的。这样方便不同的itemType保存不同的ViewHolder。它在回收的时候只是回收该viewType的ViewHolder对象,并没有保存原来的数据信息,在复用的时候需要重新走onBindViewHolder()方法重新绑定数据。
RecycledViewPool源码如下
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;
}
SparseArray<ScrapData> mScrap = new SparseArray<>();
}
可以看出,RecycledViewPool中定义了SparseArray mScrap,它是一个根据不同itemType来保存静态类ScrapData对象的SparseArray,ScrapData中包含了ArrayList mScrapHeap ,mScrapHeap是保存该itemType类型下ViewHolder的ArrayList。 缓存池定义了默认的缓存大小DEFAULT_MAX_SCRAP = 5,这个数量不是说整个缓存池只能缓存这多个ViewHolder,而是不同itemType的ViewHolder的list的缓存数量,即mScrap的数量,说明最多只有5组不同类型的mScrapHeap。mMaxScrap = DEFAULT_MAX_SCRAP说明每种不同类型的ViewHolder默认保存5个,当然mMaxScrap的值是可以设置的。这样RecycledViewPool就把不同ViewType的ViewHolder按类型分类缓存起来。
四、源码解析部分
RecyclerView的绘制部分
设置布局管理器
RecyclerView在正式进行绘制之前需要进行布局管理器的设置,不然RecyclerView也不知道如何去进行绘制,我们先从此部分源码进行下手理解
recyclerView.setLayoutManager(manager);
public void setLayoutManager(@Nullable LayoutManager layout) {
if (layout == mLayout) {
return;
}
stopScroll();
if (mLayout != null) {
if (mItemAnimator != null) {
mItemAnimator.endAnimations();
}
mLayout.removeAndRecycleAllViews(mRecycler);
mLayout.removeAndRecycleScrapInt(mRecycler);
mRecycler.clear();
if (mIsAttached) {
mLayout.dispatchDetachedFromWindow(this, mRecycler);
}
mLayout.setRecyclerView(null);
mLayout = null;
} else {
mRecycler.clear();
}
mChildHelper.removeAllViewsUnfiltered();
mLayout = layout;
if (layout != null) {
if (layout.mRecyclerView != null) {
throw new IllegalArgumentException("LayoutManager " + layout
+ " is already attached to a RecyclerView:"
+ layout.mRecyclerView.exceptionLabel());
}
mLayout.setRecyclerView(this);
if (mIsAttached) {
mLayout.dispatchAttachedToWindow(this);
}
}
mRecycler.updateViewCacheSize();
requestLayout();
}
这里看到RecyclerView在设置布局管理器之前,先是进行了相关的重置回收工作,而后将LayoutManaer和RecyclerView关联起来,最后进行请求重绘,调用请求重回的==requestLayout()==方法,该方法会调用RecyclerView的onMeasure()、onLayout()、onDraw()方法绘制三部曲
RecyclerView的回收部分
这里由LinearLayoutManager进行分析,观察其对子View进行布局的==onLayoutChilren()==方法
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
if (state.getItemCount() == 0) {
removeAndRecycleAllViews(recycler);
return;
}
}
ensureLayoutState();
mLayoutState.mRecycle = false;
resolveShouldLayoutReverse();
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
detachAndScrapAttachedViews(recycler);
}
在==onLayoutChildren()布局的时候,先根据实际情况是否需要removeAndRecycleAllViews()移除所有的子View,那些ViewHolder不可用;然后通过detachAndScrapAttachedViews()==暂时分离已经附加的ItemView,缓存到List中。
detachAndScrapAttachedViews()的作用就是把当前屏幕所有的item与屏幕分离,将他们从RecyclerView的布局中拿下来,保存到list中,在重新布局时,再将ViewHolder重新一个个放到新的位置上去。将屏幕上的ViewHolder从RecyclerView的布局中拿下来后,存放在Scrap中,Scrap包括mAttachedScrap和mChangedScrap,它们是一个list,用来保存从RecyclerView布局中拿下来ViewHolder列表,==detachAndScrapAttachedViews()只会在onLayoutChildren()==中调用,只有在布局的时候,才会把ViewHolder detach掉,然后再add进来重新布局,但是大家需要注意,Scrap只是保存从RecyclerView布局中当前屏幕显示的item的ViewHolder,不参与回收复用,单纯是为了现从RecyclerView中拿下来再重新布局上去。对于没有保存到的item,会放到mCachedViews或者RecycledViewPool缓存中参与回收复用。
public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}
在else分支中,先detachViewAt()分离视图,然后再通过scrapView()缓存到scrap中
在scrapView()方法中,进入if()分支的ViewHolder保存到mAttachedScrap中,else分支的保存到mChangedScrap中。
可以看到,mAttachedScrap为已移除的(isInvalid())或是参数未发生改变的, mChangedScrap则为其他情况
回到==scrapOrRecycleView()中,进入if()分支如果viewHolder是无效、未被移除、未被标记的则放到recycleViewHolderInternal()缓存起来,同时removeViewAt()==移除了viewHolder
void recycleViewHolderInternal(ViewHolder holder) {
·····
if (forceRecycle || holder.isRecyclable()) {
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize--;
}
·····
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
}
如果符合条件,会优先缓存到mCachedViews中时,如果超出了mCachedViews的最大限制,通过recycleCachedViewAt()将CacheView缓存的第一个数据添加到终极回收池RecycledViewPool后再移除掉,最后才会add()新的ViewHolder添加到mCachedViews中。
剩下不符合条件的则通过==addViewHolderToRecycledViewPool()==缓存到RecycledViewPool中。
void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
clearNestedRecyclerViewIfNotNested(holder);
View itemView = holder.itemView;
······
holder.mOwnerRecyclerView = null;
getRecycledViewPool().putRecycledView(holder);
}
还有一个就是在填充布局fill()的时候,它会回收移出屏幕的view到mCachedViews或者RecycledViewPool中:
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
recycleByLayoutState(recycler, layoutState);
}
}
在recycleByLayoutState()层层追查下去,会来到recycler.recycleView(view)Recycler的公共回收方法中,:
public void recycleView(@NonNull View view) {
ViewHolder holder = getChildViewHolderInt(view);
if (holder.isTmpDetached()) {
removeDetachedView(view, false);
}
recycleViewHolderInternal(holder);
}
回收分离的视图到缓存池中,方便以后重新绑定和复用,这里又来到了recycleViewHolderInternal(holder),和上面的一样,按照优先级缓存 mCachedViews> RecycledViewPool。
RecyclerView的复用流程
来看LinearLayoutManager的布局入口的方法==onLayoutChildren()==观看
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
if (state.getItemCount() == 0) {
removeAndRecycleAllViews(recycler);
return;
}
}
detachAndScrapAttachedViews(recycler);
if (mAnchorInfo.mLayoutFromEnd) {
updateLayoutStateToFillStart(mAnchorInfo);
fill(recycler, mLayoutState, state, false);
updateLayoutStateToFillEnd(mAnchorInfo);
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
}else {
updateLayoutStateToFillEnd(mAnchorInfo);
fill(recycler, mLayoutState, state, false);
updateLayoutStateToFillStart(mAnchorInfo);
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
}
}
这里有两个方法,分别对应从不同方向对RecyclerView进行滑动导致的; 但无论是哪个方向,最终都是调用==fill()方法填充由layoutState()==定义的给定布局
int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
recycleByLayoutState(recycler, layoutState);
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
layoutChunk(recycler, state, layoutState, layoutChunkResult);
······
if (layoutChunkResult.mFinished) {
break;
}
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
}
······
return start - layoutState.mAvailable;
}
核心方法是==while()循环,并通过判断可见区域是否有剩余空间,如果有则填充view上去,核心是通过while()循环执行layoutChunk()==填充一个itemView到屏幕, ==layoutChunk()==完成布局工作:
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
······
}
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
@NonNull
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
tryGetViewHolderForPositionByDeadline()才是获取view的方法,它会根据给出的position/id去scrap、cache、RecycledViewPool、或者创建获取一个ViewHolder
总结
RecyclerVIew的回收原理
在RecyclerView重新布局onLayoutChildren()或者填充布局fill()的时候,会先把必要的item与屏幕分离或者移除,并做好标记,保存到list中,在重新布局时,再将ViewHolde拿出来重新一个个放到新的位置上去。
- 如果是RecyclerView不滚动情况下缓存(比如删除item),重新布局时,把屏幕上的ViewHolder与屏幕分离下来,存放到Scrap中,即发生改变的ViewHolder缓存到mChangedScrap中,不发生改变的ViewHolder存放到mAttachedScrap中;剩下ViewHolder的会按照mCachedViews>RecycledViewPool的优先级缓存到mCachedViews或者RecycledViewPool中。
- 如果是RecyclerVIew滚动情况下缓存(比如滑动列表),在滑动时填充布局,先移除滑出屏幕的item,第一级缓存mCachedViews优先缓存这些ViewHolder,但是mCachedViews最大容量为2,当mCachedViews满了以后,会利用先进先出原则,把旧的ViewHolder存放到RecycledViewPool中后移除掉,腾出空间,再将新的ViewHolder添加到mCachedViews中,最后剩下的ViewHolder都会缓存到终极回收池RecycledViewPool中,它是根据itemType来缓存不同类型的ArrayList,最大容量为5。
RecycleView的缓存复用过程
- 当RecyclerView要拿一个复用的ViewHolder时,如果是预加载,则会先去mChangedScrap中精准查找(分别根据position和id)对应的ViewHolder,如果有就返回;
- 如果没有就再去mAttachedScrap和mCachedViews中精确查找(先position后id)是不是原来的ViewHolder,如果是说明ViewHolder是刚刚被移除的;
如果不是,则最终去mRecyclerPool找,如果itemType类型匹配对应的ViewHolder,那么返回实例,让它重新绑定数据; (4)如果mRecyclerPool也没有返回ViewHolder才会调用createViewHolder()重新去创建一个。
参考博客 深入理解RecyclerView的回收复用机制
深入理解RecyclerView的绘制流程和滑动原理
RecyclerView的缓存机制
|