RecyclerView的緩存主要體現(xiàn)在RecyclerView的內(nèi)部類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緩存默認(rèn)大小
static final int DEFAULT_CACHE_SIZE = 2;
int mViewCacheMax = DEFAULT_CACHE_SIZE;
重要的方法
設(shè)置Cache緩存大小 —— setViewCacheSize
這個方法是公有的,所以可以在自己的RecyclerView中定義Cache緩存大小,例如:
mRecyclerView.setItemViewCacheSize(5);
獲得指定位置的子View —— getViewForPosition,可能來自于緩存,也可能重新創(chuàng)建
搜索mChangedScrap列表,從對應(yīng)postion中找,找不到再從對應(yīng)id找
只有滿足mState.isPreLayout()這個條件才會搜索mChangedScrap列表,這個條件在dispatchLayoutStep1中賦值為mState.mInPreLayout = mState.mRunPredictiveAnimations;,即發(fā)生添加、刪除、修改要執(zhí)行動畫效果時,mState.mInPreLayout為true;在dispatchLayoutStep2中會賦值為false。顯然只有在dispatchLayoutStep1中要執(zhí)行動畫的時候會調(diào)用mLayout.onLayoutChildren(mRecycler, mState);方法,預(yù)布局時,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找到相應(yīng)的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緩存中找到相應(yīng)的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中獲取到了相應(yīng)的holder,要將holder的一些狀態(tài)重置,因?yàn)閺倪@取的holder只是根據(jù)type匹配的,不是position對應(yīng)的holder,所以需要重置holder的狀態(tài)
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
如果上述步驟都沒獲取到值,則通過Adapter的createViewHolder方法創(chuàng)建一個holder
holder = mAdapter.createViewHolder(RecyclerView.this, type);
調(diào)用Adapter的bindViewHolder方法,會調(diào)用Adapter的onBindViewHolder空方法
只有滿足這三個條件之一,才會調(diào)用Adapter的bindViewHolder方法
- holder還沒綁定,即還沒調(diào)用
bindViewHolder方法,這個是唯一能將holder的標(biāo)記設(shè)為綁定的方法 - holder需要更新
- holder已經(jīng)無效
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緩存的時機(jī)
每當(dāng)RecyclerView調(diào)用dispatchLayoutStep2方法,內(nèi)部都會調(diào)用onLayoutChildren方法,雖然不同的LayoutManager的實(shí)現(xiàn)不同,但是其中都會調(diào)用detachAndScrapAttachedViews方法,在這個方法中會對RecyclerView中已經(jīng)添加的子View遍歷調(diào)用scrapOrRecycleView方法,scrapOrRecycleView方法會根據(jù)holder的狀態(tài)來判斷是要添加到cache緩存中還是scrap緩存中,如果添加到Scrap緩存,最終會調(diào)用scrapView方法
大多數(shù)情況會添加到mAttachedScrap這個Scrap緩存中,什么時候會添加到mChangedScrap緩存中呢?舉個例子:

如上圖的列表,我現(xiàn)在要將字母
C修改為Z,當(dāng)調(diào)用getAdapter().notifyItemChanged(2);方法,流程如下

也就是說要修改的item會添加到mChangedScrap緩存中去,其余的會添加到mAttachedScrap緩存中
移除緩存 —— unscrapView
根據(jù)添加緩存方法中holder.setScrapContainer(this, boolean);這行代碼設(shè)置的boolean值來判斷,true則移除mChangedScrap中的holder,false則移除mAttachedScrap中的holder
移除Scrap緩存的時機(jī):
- RecyclerView的addView方法,內(nèi)部會根據(jù)
holder.wasReturnedFromScrap() || holder.isScrap()此條件判斷是否需要移除scrap緩存,相應(yīng)的會attach之前添加scrap緩存時detach的viewmChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false); - onLayout中最終會調(diào)用
dispatchLayoutStep3方法,內(nèi)部調(diào)用了removeAndRecycleScrapInt方法回收所有的scrap緩存
顯然,Scrap緩存只是用在布局期間,布局后就清空了Scrap緩存
Cache緩存
添加緩存 —— recycleViewHolderInternal
在 RecyclerView中通過recycleViewHolderInternal方法添加緩存
- 滿足以下兩個條件,才能添加到Cache緩存中:
a.mViewCacheMax > 0,即Cache緩存設(shè)置的大小要大于0
b.!holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN),一般指的是該item不會執(zhí)行動畫,例如滑動中等 - 如果超過Cache緩存的最大,則移除第0個緩存
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize--;
}
- 添加到Cache緩存的最后
int targetCacheIndex = cachedViewSize;
mCachedViews.add(targetCacheIndex, holder);
cached = true;
顯然,Cache緩存的數(shù)據(jù)結(jié)構(gòu)是后入先出的隊(duì)列結(jié)構(gòu)
添加Cache緩存時機(jī)
dispatchLayoutStep2
- 在通過fill方法填充布局時,會遍歷每一個ChildHelper中的子類,如果滿足
viewHolder.isInvalid() && !viewHolder.isRemoved() &&!mRecyclerView.mAdapter.hasStableIds()這個條件,則會添加到cache緩存中去 - 在通過
getViewForPosition方法獲得給定位置的itemview時,假如通過第二步getScrapOrHiddenOrCachedHolderForPosition方法獲得了一個holder,滿足!validateViewHolderForOffsetPosition(holder)條件,則會添加到cache緩存中去 - 調(diào)用
recycleView方法,將holder回收到cache緩存中
dispatchLayoutStep3
-
removeAnimatingView中,如果是從ChildHelper的mHiddenViews中找到并移除了這個View,則將這個View添加到cache緩存中去,舉個例子,當(dāng)調(diào)用notifyItemRangeRemoved方法刪除item,則被刪除的item在執(zhí)行完刪除動畫,會將這個item的holder添加到cache緩存中 -
removeAndRecycleScrapInt中,清空Scrap緩存,并將其添加到Cache緩存中
除了上述幾種情況,對于不同的LayoutManager還有不同的區(qū)別,例如LinearLayoutManager調(diào)用fill方法時,在方法開頭會調(diào)用recycleByLayoutState方法,該方法會回收看不到的item
注意:子View回收之前必須已經(jīng)從父布局中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的數(shù)據(jù)結(jié)構(gòu)是后進(jìn)先出的棧結(jié)構(gòu),這能保證每次獲取到的holder都是池中最新的
添加RecyclerViewPool緩存 —— recycleViewHolderInternal
在recycleViewHolderInternal內(nèi)部,如果沒有將item添加到Cache緩存中,則會添加到RecyclerViewPool緩存中
// cache的值在添加Cache緩存的步驟中賦值
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
添加RecyclerViewPool緩存時機(jī)
由于在RecyclerView中添加Cache緩存和RecyclerViewPool緩存用的是同一個方法recycleViewHolderInternal,所以兩個緩存的添加時機(jī)是一樣的
移除緩存 —— getViewForPosition
return scrapHeap.remove(scrapHeap.size() - 1);
只在getViewForPosition時,從RecyclerViewPool緩存中獲取到holder,同時從RecyclerViewPool中移除
RecyclerView各種狀態(tài)下的緩存分析
加載RecyclerView顯示到屏幕上
在dispatchLayoutStep2中的緩存變動
| 所在方法 | 緩存類型 | 列表中的數(shù)據(jù) |
|---|---|---|
| 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開始有一部分顯示到屏幕中,則會
| 所在方法 | 緩存類型 | 列表中的數(shù)據(jù) |
|---|---|---|
| GapWorker.prefetchPositionWithDeadline | Cache緩存 | ![]() Cache緩存列表1.png
|
| getViewForPosition | 從Cache緩存的列表中取 |
如果RecyclerView滑動到A開始消失在屏幕中,則會
| 所在方法 | 緩存類型 | 列表中的數(shù)據(jù) |
|---|---|---|
| GapWorker.prefetchPositionWithDeadline | Cache緩存 | ![]() Cache緩存列表2.png
|
| getViewForPosition | 從Cache緩存的列表中取 |
總結(jié)
Scrap緩存用在RecyclerView布局時,布局完成之后就會清空
添加到Cache緩存和RecyclerViewPool緩存的item,他們的View必須已經(jīng)從RecyclerView中detached或removed



