赞
踩
本篇记录笔者学习ReclerView缓存机制的心路历程
我们都知道RecyclerView无论如何都不会导致OOM的发生,而这背后依靠是其本身强大的回收复用机制,那么其回收复用机制是如何实现的呢,下面笔者记录对其的分析过程
在搞清楚RecyclerView的缓存复用机制之前,我们先要清楚缓存复用机制是对什么进行复用的呢,毫无疑问不可能是我们针对每个itemViews书写的布局里面的那些控件,这里直接给出答案,缓存复用机制针对复用的内容是ViewHolder,后面的源码分析会给出分析
在我们自定义书写RecyclerView适配器的过程中,难免会接触到两个方法:onCreateViewHolder()和onBindViewHolder(),前者会调用方法创建ViewHolder(),后者会调用相关方法进行数据绑定工作。
需要注意的是,如果是新创建View并填充数据,则会调用onCreateViewHolder()和onBindViewHolder()两个方法,这通常发生RecyclerView首次创建View并填充数据的过程中;
如果是屏幕滑动过程中不断出现新的ItemView显示,这一过程中通常会复用已经存在的ViewHolder,并调用onBindViewHolder() 对数据进行绑定
下面介绍ViewHolder的各个状态,相关方法如下
在了解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拿出来重新一个个放到新的位置上去。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。