本篇记录笔者学习ReclerView缓存机制的心路历程
我们都知道RecyclerView无论如何都不会导致OOM的发生,而这背后依靠是其本身强大的回收复用机制,那么其回收复用机制是如何实现的呢,下面笔者记录对其的分析过程
在搞清楚RecyclerView的缓存复用机制之前,我们先要清楚缓存复用机制是对什么进行复用的呢,毫无疑问不可能是我们针对每个itemViews书写的布局里面的那些控件,这里直接给出答案,缓存复用机制针对复用的内容是ViewHolder,后面的源码分析会给出分析
在我们自定义书写RecyclerView适配器的过程中,难免会接触到两个方法:onCreateViewHolder()和onBindViewHolder(),前者会调用方法创建ViewHolder(),后者会调用相关方法进行数据绑定工作。
需要注意的是,如果是新创建View并填充数据,则会调用onCreateViewHolder()和onBindViewHolder()两个方法,这通常发生RecyclerView首次创建View并填充数据的过程中;
如果是屏幕滑动过程中不断出现新的ItemView显示,这一过程中通常会复用已经存在的ViewHolder,并调用onBindViewHolder() 对数据进行绑定
在了解RecyclerView的几级缓存之前,我们先通过RecyclerView的官方源码构造函数了解其几级缓存的数据结构
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();//保存重新布局时从RecyclerView分离的item的无效、未移除、未更新的holder
ArrayList<ViewHolder> mChangedScrap = null;//保存重新布局时无效的item,未移除的Holder
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();//保存最新被移除的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;//默认滚动回收复用的item数量为2
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用于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增加一层缓存池,Recycler首先去scrap和CacheView中寻找复用view,如果没有就去ViewCacheExtension中寻找View,如果还是没有找到,那么最后去RecycledViewPool寻找复用的View。
在日常的开发中,一般我们使用不到ViewCacheExtension,所以这里就简略带过了
在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.setLayoutManager(manager);//设置布局管理器
public void setLayoutManager(@Nullable LayoutManager layout) {
if (layout == mLayout) {
return;
}
stopScroll();//先停止滚动,防止缓存View发生影响
// TODO We should do this switch a dispatchLayout pass and animate children. There is a good
// chance that LayoutManagers will re-use views.
if (mLayout != null) {//重新所有RecyclerView的参数
// end all running animations
if (mItemAnimator != null) {
mItemAnimator.endAnimations();//结束动画
}
mLayout.removeAndRecycleAllViews(mRecycler);//移除所有回收的itemView
mLayout.removeAndRecycleScrapInt(mRecycler);//溢出所有被废弃的View
mRecycler.clear();//清除缓存
if (mIsAttached) {
mLayout.dispatchDetachedFromWindow(this, mRecycler);
}
mLayout.setRecyclerView(null);
mLayout = null;
} else {
mRecycler.clear();
}
// this is just a defensive measure for faulty item animators.
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);//进行关联,这一步可以看到,一个LayoutManager只可以和一个RecyclerView进行管理
if (mIsAttached) {
mLayout.dispatchAttachedToWindow(this);
}
}
mRecycler.updateViewCacheSize();//更新缓存大小
requestLayout();
}
这里看到RecyclerView在设置布局管理器之前,先是进行了相关的重置回收工作,而后将LayoutManaer和RecyclerView关联起来,最后进行请求重绘,调用请求重回的==requestLayout()==方法,该方法会调用RecyclerView的onMeasure()、onLayout()、onDraw()方法绘制三部曲
这里由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);//移除所有子View
return;
}
}
ensureLayoutState();
mLayoutState.mRecycle = false;//禁止回收
//颠倒绘制布局
resolveShouldLayoutReverse();
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
//暂时分离已经附加的view,即将所有child detach并通过Scrap回收
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);//移除VIew
recycler.recycleViewHolderInternal(viewHolder);//缓存到CacheView或者RecycledViewPool中
} else {
detachViewAt(index);//分离View
recycler.scrapView(view);//scrap缓存
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);//保存到mAttachedScrap中
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);//保存到mChangedScrap中
}
}
在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);//mCachedViews回收
cached = true;
}
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);//放到RecycledViewPool回收
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);//将holder添加到RecycledViewPool中
}
还有一个就是在填充布局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);//回收移出屏幕的view
}
}
在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。
来看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);//移除所有子View
return;
}
}
//暂时分离已经附加的view,即将所有child detach并通过Scrap回收
detachAndScrapAttachedViews(recycler);
if (mAnchorInfo.mLayoutFromEnd) {
//描点位置从start位置开始填充ItemView布局
updateLayoutStateToFillStart(mAnchorInfo);
fill(recycler, mLayoutState, state, false);//填充所有itemView
//描点位置从end位置开始填充ItemView布局
updateLayoutStateToFillEnd(mAnchorInfo);
fill(recycler, mLayoutState, state, false);//填充所有itemView
endOffset = mLayoutState.mOffset;
}else {
//描点位置从end位置开始填充ItemView布局
updateLayoutStateToFillEnd(mAnchorInfo);
fill(recycler, mLayoutState, state, false);
//描点位置从start位置开始填充ItemView布局
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);//回收滑出屏幕的view
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {//一直循环,知道没有数据
layoutChunkResult.resetInternal();
layoutChunk(recycler, state, layoutState, layoutChunkResult);//添加一个child
······
if (layoutChunkResult.mFinished) {//布局结束,退出循环
break;
}
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;//根据添加的child高度偏移计算
}
······
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
······
}
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重新布局onLayoutChildren()或者填充布局fill()的时候,会先把必要的item与屏幕分离或者移除,并做好标记,保存到list中,在重新布局时,再将ViewHolde拿出来重新一个个放到新的位置上去。
参考博客
深入理解RecyclerView的回收复用机制
深入理解RecyclerView的绘制流程和滑动原理
RecyclerView的缓存机制