之前写过一片关于RecyclerView的博客,主要是从源码的角度捋了一遍。最近看了看,觉得写的比较粗糙,比较重点的四级缓存没有说的很清楚,所以再写一篇博客来专门说一下RecyclerView的四级缓存
目录
一.Scrap
Scrap是RecyclerView的第一级缓存,包括mAttachedScrap和mChangedScrap。之前对这两个缓存很不理解,打点调试的时候,发现无论怎么滚动RecyclerView,都没有走到这两个缓存的地方。这是因为这两个缓存和滚动没有关系,只有当界面重新绘制的时候,这两个缓存才会有作用。这两个缓存的区别稍后再说,我们先从源码中分析一下mAttachedScrap和mChangedScrap的缓存和复用过程
1.Scrap的缓存过程
我们先说一下缓存过程的源码:
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);
}
}
这个方法是LinearLayoutManager中的方法,顺序为:
onLayoutChildren->detachAndScrapAttachedViews->scrapOrRecycleView
onLayoutChildren这个方法我就不多说了,我们直接进入正题
第9~10行:这里的if有三个条件,如果条件不符合,则执行else。而else中的recycler.scrapView方法(第15行)就是向mAttachedScrap或mChangedScrap中存储。我们先看一下这三个条件:
viewHolder.isInvalid():从翻译上来看,是判断ViewHolder是否为无效状态,说实话,我不太明白这个无效状态时什么,但是我们知道它在哪里设置的这个状态就可以了。我们调用setAdapter的时候会有如下的调用链:setAdapter->processDataSetCompletelyChanged->markKnownViewsInvalide,看一下markKnownViewsInvalide方法:
void markKnownViewsInvalid() {
final int childCount = mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
if (holder != null && !holder.shouldIgnore()) {
holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
}
}
markItemDecorInsetsDirty();
mRecycler.markKnownViewsInvalid();
}
第6行中设置了这个无效的状态。所以第一次数据设置的时候,viewHolder.isInvalid()返回值为true。
viewHolder.isRemoved():这个比较容易理解,viewHolder是否移除,而代码中有一个!,也就是说viewHolder没有移除。调用Adapter的notifyItemRemoved会使这个标记为移除状态,源码就不翻了,有点费劲,而且不那么重要,知道就行
mRecyclerView.mAdapter.hasStableIds():这个值不设置的话,默认为false,而代码中有!,所以条件也成立
综上所述,得出一个结论:
- 如果是正常的设置数据,比如第一次数据的设置或者滚动的时候,mAttachedScrap和mChangedScrap的缓存不会触发的;
- 如何调用了适配器的notifyItemRemoved方法,会触发mAttachedScrap和mChangedScrap缓存
- 如果设置了适配器的stableId为true,会触发mAttachedScrap和mChangedScrap缓存
我们继续看scrapOrRecycleView方法,查看第15行的源代码:
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);
}
}
向mAttachedScrap中添加,还是向mChangeScrap中添加,主要是if中的3个条件:
holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID):
这个条件表示ViewHolder执行了Remove动作(上面说了)或者为无效状态
!holder.isUpdated():ViewHolder为非更新状态,而调用了adapter的notifyItemChanged方法就会打上更新的标记
canReuseUpdatedViewHolder():一直都为true,忽略,因为基本上前两个条件就有结果了,到不了这里
经过断点调试得出结论:
- RecyclerView发生了移除的操作,所有的数据依然会存储到mAttachedScrap中,而不会进入到mChangedScrap里(而所借鉴的文章中说,发生移除的时候,移除条目的上方ViewHolder会存入到mAttachedScrap,而下方的ViewHolder会存入到mChangedScrap中,我的结论和他的不太一样)
- 如果数据发生了改变,调用了适配器的notifyItemChanged方法,那么被修改数据的ViewHolder会存储到mChangedScrap中
2.Scrap的复用
其实mAttachedScrap和mChangedScrap的复用过程还是比较简单的,我就简单贴一张图来说明一下(如果源码不太熟悉的可以看我这篇文章:Android RecyclerView源码完全解析)
如果了解RecyclerView源码的话,tryGetViewHolderForPositionByDeadline方法应该会比较熟悉。这个方法里,第一个红框的方法是mChangedScrap的复用过程,而第二个红框的方法中有mAttachedScrap的复用过程,都比较好找,就懒得贴相关方法的源码了。
但是有一个点要特别注意:看过我的博客Android RecyclerView源码完全解析都知道,tryGetViewHolderForPositionByDeadline是在dispatchLayoutStep2中,也就是说,缓存的复用是在dispatchLayout2中。dispatchLayout2调用之后,会调用dispatcheLayoutStep3,而这个方法中有这样一句代码:
mLayout.removeAndRecycleScrapInt(mRecycler);
我们看一下这个方法源码:
void removeAndRecycleScrapInt(Recycler recycler) {
final int scrapCount = recycler.getScrapCount();
// Loop backward, recycler might be changed by removeDetachedView()
for (int i = scrapCount - 1; i >= 0; i--) {
final View scrap = recycler.getScrapViewAt(i);
final ViewHolder vh = getChildViewHolderInt(scrap);
if (vh.shouldIgnore()) {
continue;
}
vh.setIsRecyclable(false);
if (vh.isTmpDetached()) {
mRecyclerView.removeDetachedView(scrap, false);
}
if (mRecyclerView.mItemAnimator != null) {
mRecyclerView.mItemAnimator.endAnimation(vh);
}
vh.setIsRecyclable(true);
recycler.quickRecycleScrapView(scrap);
}
recycler.clearScrap();
if (scrapCount > 0) {
mRecyclerView.invalidate();
}
}
第20行:这行代码是关键,这里清空了mAttachedScrap和mChangedScrap,我们看一下
void clearScrap() {
mAttachedScrap.clear();
if (mChangedScrap != null) {
mChangedScrap.clear();
}
}
所以说,RecyclerView中的两个Scrap缓存,都是用完即可清空的。说了半天,可能有同学明白了Scrap的过程,但是并不太清楚Scrap缓存的数据对RecyclerView有什么好处?其实RecyclerView的两个Scrap缓存主要是应对比较轻量级的刷新,比如移除一个条目,刷新一个条目等,页面中大部分的ViewHolder都没有变化,除了变化的那一个条目外,其他的没必要重新设置数据,完全可以哪里复用;如果是全部刷新(适配器的notifyDataSetChanged方法),那么不会走Scrap缓存。
二、CachedViews
RecyclerView的CacheView是和滚动相关的缓存,缓存被移出屏幕的View,CacheView的容量为2。CacheView的缓存过程没什么说的,我们看一下复用过程:
依然是在getScrapOrHiddenOrCachedHolderForPosition方法中:
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
// invalid view holders may be in cache if adapter has stable ids as they can be
// retrieved via getScrapOrCachedViewForId
if (!holder.isInvalid() && holder.getLayoutPosition() == position
&& !holder.isAttachedToTransitionOverlay()) {
if (!dryRun) {
mCachedViews.remove(i);
}
if (DEBUG) {
Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
+ ") found match in cache: " + holder);
}
return holder;
}
}
第6行:if的第二个条件,判断了position是否相同,也就是缓存的position和现在要展示的position是否相同。我们假设我们一直朝一个方向滑动,将要展示的position和CachedViews中缓存的View的position是不可能相同的。
那CachedView这个缓存又有什么用呢?其实CachedView的缓存主要是应对来回滑动的情况,这时候CachedView才会真正的起作用,其缓存的ViewHolder不需要重新赋值,就可以直接拿来用了。当然,如果你认为CachedView的默认容量有点小,可以调用setItemViewCacheSize方法来手动设置。
三、ViewCacheExtension
这个缓存需要开发者进行实现,我没用过这个缓存,也没探究过这个缓存,总觉得其他的缓存已经够用了,先简单引用一下其他博客的内容吧,有时间的话,我再补充它的用法:
ViewCacheExtension是缓存拓展的帮助类,额外提供了一层缓存池给开发者。开发者视情况而定是否使用ViewCacheExtension增加一层缓存池,Recycler首先去scrap和CacheView中寻找复用view,如果没有就去ViewCacheExtension中寻找View,如果还是没有找到,那么最后去RecycledViewPool寻找复用的View。
————————————————
原文链接:https://blog.csdn.net/qjyws/article/details/123237071
四、RecycledViewPool
觉得别人说的挺好的,也很好理解
在Scrap、CacheView、ViewCacheExtension都不愿意回收的时候,都会丢到RecycledViewPool中回收,所以RecycledViewPool是Recycler的终极回收站。
RecycledViewPool实际上是以SparseArray嵌套一个ArraryList的形式保存ViewHolder的,因为RecycledViewPool保存的ViewHolder是以itemType来区分的。这样方便不同的itemType保存不同的ViewHolder。它在回收的时候只是回收该viewType的ViewHolder对象,并没有保存原来的数据信息,在复用的时候需要重新走onBindViewHolder()方法重新绑定数据。
可以看出,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按类型分类缓存起来。
————————————————
原文链接:https://blog.csdn.net/qjyws/article/details/123237071
鸣谢:Android:深入理解RecyclerView的缓存机制_VoldemortQian的博客-CSDN博客_android recyclerview缓存机制