RecyclerView的缓存主要体现在RecyclerView的内部类Recycler
重要的成员变量
四级缓存 —— Scrap、Cache、ViewCacheExtension 、RecycledViewPool
- mAttachedScrap
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
- mChangedScrap
ArrayList<ViewHolder> mChangedScrap = null;
- mCachedViews
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
- ViewCacheExtension
private ViewCacheExtension mViewCacheExtension;
- RecycledViewPool
RecycledViewPool mRecyclerPool;
Cache缓存默认大小
static final int DEFAULT_CACHE_SIZE = 2;
int mViewCacheMax = DEFAULT_CACHE_SIZE;
重要的方法
设置Cache缓存大小 —— setViewCacheSize
这个方法是公有的,所以可以在自己的RecyclerView中定义Cache缓存大小,例如:
mRecyclerView.setItemViewCacheSize(5);
获得指定位置的子View —— getViewForPosition,可能来自于缓存,也可能重新创建
搜索mChangedScrap列表,从对应postion中找,找不到再从对应id找
只有满足mState.isPreLayout()
这个条件才会搜索mChangedScrap列表,这个条件在dispatchLayoutStep1
中赋值为mState.mInPreLayout = mState.mRunPredictiveAnimations;
,即发生添加、删除、修改要执行动画效果时,mState.mInPreLayout
为true;在dispatchLayoutStep2
中会赋值为false。显然只有在dispatchLayoutStep1
中要执行动画的时候会调用mLayout.onLayoutChildren(mRecycler, mState);
方法,预布局时,getViewForPosition才会走这第一步。
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
- 找是否有和postion相同的holder
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
- 找是否有和id相同的holder
final ViewHolder holder = mChangedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
通过postion按顺序搜索mAttachedScrap、ChildHelper中存的mHiddenViews、mCachedViews列表
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}
- 搜索mAttachedScrap
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
- 搜索mHiddenViews,从mHiddenViews找到相应的holder后,立即将其从mHiddenViews中移除,然后添加到Scrap缓存中
View view = mChildHelper.findHiddenNonRemovedView(position);
if (view != null) {
// This View is good to be used. We just need to unhide, detach and
// scrap list.
final ViewHolder vh = getChildViewHolderInt(view);
mChildHelper.unhide(view);
int layoutIndex = mChildHelper.indexOfChild(view);
mChildHelper.detachViewFromParent(layoutIndex);
scrapView(view);
vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
| ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
return vh;
}
- 搜索mCachedViews,从Cache缓存中找到相应的holder后,立即将其从Cache缓存中移除
if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
if (!dryRun) {
mCachedViews.remove(i);
}
return holder;
}
通过id按顺序搜索mAttachedScrap、mCachedViews列表
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
}
- 搜索mAttachedScrap
if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
if (type == holder.getItemViewType()) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
if (holder.isRemoved()) {
// this might be valid in two cases:
// > item is removed but we are in pre-layout pass
// >> do nothing. return as is. make sure we don't rebind
// > item is removed then added to another position and we are in
// post layout.
// >> remove removed and invalid flags, add update flag to rebind
// because item was invisible to us and we don't know what happened in
// between.
if (!mState.isPreLayout()) {
holder.setFlags(ViewHolder.FLAG_UPDATE, ViewHolder.FLAG_UPDATE |
ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED);
}
}
return holder;
} else if (!dryRun) {
// if we are running animations, it is actually better to keep it in scrap
// but this would force layout manager to lay it out which would be bad.
// Recycle this scrap. Type mismatch.
mAttachedScrap.remove(i);
removeDetachedView(holder.itemView, false);
quickRecycleScrapView(holder.itemView);
}
}
- 搜索mCachedViews
if (holder.getItemId() == id) {
if (type == holder.getItemViewType()) {
if (!dryRun) {
mCachedViews.remove(i);
}
return holder;
} else if (!dryRun) {
recycleCachedViewAt(i);
return null;
}
}
用户通过ViewCacheExtension可以自定义缓存策略
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
通过RecycledViewPool获取缓存
需要注意的是,如果从RecycledViewPool中获取到了相应的holder,要将holder的一些状态重置,因为从这取的holder只是根据type匹配的,不是position对应的holder,所以需要重置holder的状态
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
如果上述步骤都没获取到值,则通过Adapter的createViewHolder方法创建一个holder
holder = mAdapter.createViewHolder(RecyclerView.this, type);
调用Adapter的bindViewHolder方法,会调用Adapter的onBindViewHolder空方法
只有满足这三个条件之一,才会调用Adapter的bindViewHolder方法
- holder还没绑定,即还没调用
bindViewHolder
方法,这个是唯一能将holder的标记设为绑定的方法 - holder需要更新
- holder已经无效
if (mState.isPreLayout() && holder.isBound()) {
// do not update unless we absolutely have to.
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
if (DEBUG && holder.isRemoved()) {
throw new IllegalStateException("Removed holder should be bound and it should"
+ " come here only in pre-layout. Holder: " + holder);
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
总的来说分为这四类缓存:
Scrap缓存
scrap缓存主要用在布局前后,主要包括mAttachedScrap和mChangedScrap这两个缓存列表
添加缓存 —— scrapView
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);
}
添加Scrap缓存的时机
每当RecyclerView调用dispatchLayoutStep2
方法,内部都会调用onLayoutChildren
方法,虽然不同的LayoutManager的实现不同,但是其中都会调用detachAndScrapAttachedViews
方法,在这个方法中会对RecyclerView中已经添加的子View遍历调用scrapOrRecycleView方法,scrapOrRecycleView方法会根据holder的状态来判断是要添加到cache缓存中还是scrap缓存中,如果添加到Scrap缓存,最终会调用scrapView
方法
大多数情况会添加到mAttachedScrap这个Scrap缓存中,什么时候会添加到mChangedScrap缓存中呢?举个例子:
如上图的列表,我现在要将字母
C
修改为Z
,当调用getAdapter().notifyItemChanged(2);
方法,流程如下
Recycler_Update_Flow.png
也就是说要修改的item会添加到mChangedScrap缓存中去,其余的会添加到mAttachedScrap缓存中
移除缓存 —— unscrapView
根据添加缓存方法中holder.setScrapContainer(this, boolean);
这行代码设置的boolean值来判断,true则移除mChangedScrap中的holder,false则移除mAttachedScrap中的holder
移除Scrap缓存的时机:
- RecyclerView的addView方法,内部会根据
holder.wasReturnedFromScrap() || holder.isScrap()
此条件判断是否需要移除scrap缓存,相应的会attach之前添加scrap缓存时detach的viewmChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);
- onLayout中最终会调用
dispatchLayoutStep3
方法,内部调用了removeAndRecycleScrapInt
方法回收所有的scrap缓存
显然,Scrap缓存只是用在布局期间,布局后就清空了Scrap缓存
Cache缓存
添加缓存 —— recycleViewHolderInternal
在 RecyclerView中通过recycleViewHolderInternal
方法添加缓存
- 满足以下两个条件,才能添加到Cache缓存中:
a.mViewCacheMax > 0
,即Cache缓存设置的大小要大于0
b.!holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)
,一般指的是该item不会执行动画,例如滑动中等 - 如果超过Cache缓存的最大,则移除第0个缓存
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize--;
}
- 添加到Cache缓存的最后
int targetCacheIndex = cachedViewSize;
mCachedViews.add(targetCacheIndex, holder);
cached = true;
显然,Cache缓存的数据结构是后入先出的队列结构
添加Cache缓存时机
dispatchLayoutStep2
- 在通过fill方法填充布局时,会遍历每一个ChildHelper中的子类,如果满足
viewHolder.isInvalid() && !viewHolder.isRemoved() &&!mRecyclerView.mAdapter.hasStableIds()
这个条件,则会添加到cache缓存中去 - 在通过
getViewForPosition
方法获得给定位置的itemview时,假如通过第二步getScrapOrHiddenOrCachedHolderForPosition
方法获得了一个holder,满足!validateViewHolderForOffsetPosition(holder)
条件,则会添加到cache缓存中去 - 调用
recycleView
方法,将holder回收到cache缓存中
dispatchLayoutStep3
-
removeAnimatingView
中,如果是从ChildHelper的mHiddenViews中找到并移除了这个View,则将这个View添加到cache缓存中去,举个例子,当调用notifyItemRangeRemoved
方法删除item,则被删除的item在执行完删除动画,会将这个item的holder添加到cache缓存中 -
removeAndRecycleScrapInt
中,清空Scrap缓存,并将其添加到Cache缓存中
除了上述几种情况,对于不同的LayoutManager还有不同的区别,例如LinearLayoutManager
调用fill
方法时,在方法开头会调用recycleByLayoutState
方法,该方法会回收看不到的item
注意:子View回收之前必须已经从父布局中detached或removed
移除缓存 —— mCachedViews.remove
- 在getViewForPosition步骤2时,通过
getScrapOrHiddenOrCachedHolderForPosition
方法,从cache缓存中获取到了holder,则移除 - 在getViewForPosition步骤3时,通过
getScrapOrCachedViewForId
方法,从cache缓存中获取到了holder,则移除 - 需要通过
recycleCachedViewAt
方法移除cache缓存时
ViewCacheExtension
用户可以自定义的缓存
RecyclerViewPool
添加到RecyclerViewPool中 —— putRecycledView
scrapHeap.add(scrap);
从RecyclerViewPool中获取 —— getRecycledView
return scrapHeap.remove(scrapHeap.size() - 1);
从添加和获取缓存来看,RecyclerViewPool的数据结构是后进先出的栈结构,这能保证每次获取到的holder都是池中最新的
添加RecyclerViewPool缓存 —— recycleViewHolderInternal
在recycleViewHolderInternal
内部,如果没有将item添加到Cache缓存中,则会添加到RecyclerViewPool缓存中
// cache的值在添加Cache缓存的步骤中赋值
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
添加RecyclerViewPool缓存时机
由于在RecyclerView中添加Cache缓存和RecyclerViewPool缓存用的是同一个方法recycleViewHolderInternal
,所以两个缓存的添加时机是一样的
移除缓存 —— getViewForPosition
return scrapHeap.remove(scrapHeap.size() - 1);
只在getViewForPosition
时,从RecyclerViewPool缓存中获取到holder,同时从RecyclerViewPool中移除
RecyclerView各种状态下的缓存分析
加载RecyclerView显示到屏幕上
在dispatchLayoutStep2中的缓存变动
所在方法 | 缓存类型 | 列表中的数据 |
---|---|---|
ChildHelper,即ReclyerView中的子View | ChildHelper_List.png | |
detachAndScrapAttachedViews | Scrap缓存的mAttachedScrap列表 | mAttachedScrap.png |
getViewForPosition | 从Scrap缓存的mAttachedScrap列表中取 | |
addView | 从Scrap缓存的mAttachedScrap列表中移除缓存 |
滑动RecyclerView
可以打印Cache缓存列表和RecyclerViewPool缓存列表来看滑动RecyclerView时的缓存变化,如下:
mRvTest.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
mRvTest.printAllValue();
}
});
public void printAllValue() {
try {
Field field = rvClz.getDeclaredField("mRecycler");
field.setAccessible(true);
recycler = (RecyclerView.Recycler) field.get(this);
recycler.getScrapList();
getCacheField("mCachedViews");
Field poolField = recyclerPoolClz.getDeclaredField("mScrap");
poolField.setAccessible(true);
SparseArray<Object> sa = (SparseArray<Object>) poolField.get(this.getRecycledViewPool());
for (int i = 0; i < sa.size(); i++) {
Field fd = scrapDataClz.getDeclaredField("mScrapHeap");
fd.setAccessible(true);
ArrayList<ViewHolder> mScrapHeap = (ArrayList<RecyclerView.ViewHolder>) fd.get(sa.get(sa.keyAt(i)));
for (int j = 0; j < mScrapHeap.size(); j++) {
Log.d(TAG, "RecycledViewPool position: " + ((TextView) mScrapHeap.get(i).itemView.findViewById(R.id.tv)).getText());
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void getCacheField(String fieldName) throws Exception {
Field field = recyclerClz.getDeclaredField(fieldName);
field.setAccessible(true);
List<ViewHolder> viewHolders = (List<RecyclerView.ViewHolder>) field.get(recycler);
if (viewHolders == null)
return;
for (int i = 0; i < viewHolders.size(); i++) {
Log.d(TAG, fieldName + " position : " + ((TextView) viewHolders.get(i).itemView.findViewById(R.id.tv)).getText());
}
}
如果RecyclerView滑动到F开始有一部分显示到屏幕中,则会
所在方法 | 缓存类型 | 列表中的数据 |
---|---|---|
GapWorker.prefetchPositionWithDeadline | Cache缓存 | Cache缓存列表1.png |
getViewForPosition | 从Cache缓存的列表中取 |
如果RecyclerView滑动到A开始消失在屏幕中,则会
所在方法 | 缓存类型 | 列表中的数据 |
---|---|---|
GapWorker.prefetchPositionWithDeadline | Cache缓存 | Cache缓存列表2.png |
getViewForPosition | 从Cache缓存的列表中取 |
总结
-
Scrap缓存用在RecyclerView布局时,布局完成之后就会清空
-
添加到Cache缓存和RecyclerViewPool缓存的item,他们的View必须已经从RecyclerView中detached或removed
网友评论