前言
- 上一篇文章RecyclerView之佈局設計
RecyclerView,見名知義,這個View代表了可循環使用的視圖集合控件,封裝了View的緩存邏輯判斷,RecyclerView的基本單元是ViewHolder,裏面有一個itemView代表了視圖上的子View,所以RecyclerView的緩存基本單元也是ViewHolder。本文將從源碼的角度來講解RecyclerView的緩存設計。
本文相關源碼基於Android8.0,相關源碼位置如下:
frameworks/support/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
frameworks/support/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
Recycler介紹
這裏首先介紹一下Recycler,它定義在RecyclerView中,如下:
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();//緩存着在屏幕中顯示的ViewHolder
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();//緩存着已經滾動出屏幕的ViewHolder,即屏幕外的ViewHolder
RecycledViewPool mRecyclerPool;//ViewHolder的緩存池,屏幕外緩存的mCachedViews已滿時,會將ViewHolder緩存到RecycledViewPool中。
private ViewCacheExtension mViewCacheExtension;//自定義緩存,自己實現ViewCacheExtension類來實現緩存。
ArrayList<ViewHolder> mChangedScrap = null;//屏幕內緩存,緩存着數據已經改變的ViewHolder
int mViewCacheMax = DEFAULT_CACHE_SIZE;//mCachedViews默認緩存數量
static final int DEFAULT_CACHE_SIZE = 2;//默認緩存數量爲2
private int mRequestedCacheMax = DEFAULT_CACHE_SIZE; //可以設置mCachedViews的最大緩存數量,默認爲2
//...
}
Recycler是RecyclerView的核心類,是RecyclerView的緩存實現類,它有着四級緩存:
-
1、mAttachedScrap
屏幕內緩存, 當我們調用notifiXX函數重新佈局時,在佈局之前,LayoutManager會調用detachAndScrapAttachedViews(recycler)把在RecyclerView中顯示的ViewHolder一個個的剝離下來,然後緩存在mAttachedScrap中,等佈局時會先從mAttachedScrap查找,再把ViewHolder一個個的放回RecyclerView原位中去,mAttachedScrap只是單純的保存從RecyclerView中剝離的ViewHolder,再重新放回RecyclerView中去,如果放回後還有剩餘的ViewHolder沒有參加新佈局,會從mAttachedScrap移到mCachedViews中。 -
2、mCachedViews
屏幕外緩存,在RecyclerView滾動時,對於那些不在RecyclerView中顯示的ViewHolder,LayoutManager會調用removeAndRecycleAllViews(recycler)把這些已經移除的ViewHolder緩存在mCacheViews中,它的默認大小是2,當它滿了的時候,就會利用先進先出原則,把老的ViewHolder移到mRecyclerPool中,mCachedViews它只是緩存最新被移除出屏幕的ViewHolder。 -
3、mViewCacheExtension
自定義緩存實現,一般而言,我們不會自定義緩存實現,使用Recycler提供的3級緩存足夠。 -
4、mRecyclerPool
緩存池,通過前面1、2可以知道,真正廢棄的ViewHolder最終移到mRecyclerPool,當我們向RecyclerView申請一個HolderView來使用的時,如果在mAttachedScrap、mCachedViews匹配不到,即使他們中有ViewHolder也不會返回給我們使用,而是會到mRecyclerPool中去拿一個廢棄的ViewHolder返回。mRecyclerPool內部維護了一個SparseArray,在mRecyclerPool中會根據每個ViewType把ViewHolder分別存儲在不同的列表中,每個ViewType默認緩存5個ViewHolder,而且RecyclerViewPool也可以是多個RecyclerView之間的ViewHolder的緩存池,只要通過RecyclerView.setRecycledViewPool(RecycledViewPool)設置同一個RecycledViewPool,設置時,不需要自己去new 一個 RecyclerViewPool,每個RecyclerView默認都有一個RecyclerViewPool,只需要通過mRecyclerView.getRecycledViewPool()獲取。RecyclerViewPool大概結構如下:
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的key爲type,value爲ScrapData,ScrapData中包含ViewHolder列表
SparseArray<ScrapData> mScrap = new SparseArray<>();
//...
//根據type從緩存池中獲取一個ViewHolder
public ViewHolder getRecycledView(int viewType) {
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
return scrapHeap.remove(scrapHeap.size() - 1);
}
return null;
}
//把一個ViewHolder放入緩存池中緩存
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return;
}
//...
scrap.resetInternal();
scrapHeap.add(scrap);
}
}
所以我們從Recycler中獲取一個ViewHolder時,是這樣的順序:mAttachedScrap -> mCachedViews -> mViewCacheExtension -> mRecyclerPool,當上述步驟都找不到了,就會調用Adapter的creat函數創建一個ViewHolder。那這裏爲什麼省略mChangedScrap不講呢?因爲mChangedScrap是跟RecyclerView的預佈局有關,緩存着RecyclerView中數據改變過的ViewHolder,而預佈局默認爲false,一般是RecyclerView執行動畫時纔會爲true,我們上一篇文章也沒有討論執行動畫的時候的佈局過程,所以這裏就不分析mChangedScrap。
Recycler.getViewForPosition()
在上篇文章中,提到在layoutChunk函數中,首先會調用LayoutState對象的next函數獲取到一個itemView,然後佈局這個itemView,我們來看LayoutState的next函數相關實現:
View next(RecyclerView.Recycler recycler) {
//省略了一個mScrapList,屬於LayoutManager,跟執行動畫時的緩存有關,這裏不分析
//...
//這裏纔是核心,調用Recycler中的getViewForPosition獲取itemView
final View view = recycler.getViewForPosition(mCurrentPosition);
//把itemView索引移到下一個位置
mCurrentPosition += mItemDirection;
return view;
}
上述代碼實際是調用RecyclerView.Recycler對象的getViewForPosition方法獲取itemView,而該函數最終會獲取一個ViewHolder,從而返回ViewHolder中的itemView,我們來看該函數相關調用和實現:
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
//可以看到最終返回的是ViewHolder中的itemView
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
//獲取一個ViewHolder
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
//...
}
Recycler的getViewForPosition方法最終會調用到tryGetViewHolderForPositionByDeadline方法,tryGetViewHolderForPositionByDeadline方法的意圖是通過給定的position從Recycler的scrap, cache,RecycledViewPool獲取一個ViewHolder或者通過Adapter直接創建一個ViewHolder。我們來看tryGetViewHolderForPositionByDeadline方法相關源碼:
//參數解釋:
//position:要獲得哪個位置的ViewHolder
//dryRun: 代表position的ViewHolder是否已經從scrap或cache列表中移除,這裏爲false,表示沒有,因爲佈局函數layoutChildren中一定會調用detachAndScrapAttachedViews(recycler)函數,表示把ViewHolder放入scrap列表中
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
//省略了跟預佈局有關的mChangedScrap獲取ViewHolder,mChangedScrap不屬於常規緩存
//...
ViewHolder holder = null;
if (holder == null) {
//1、第一次查找,通過position從scrap或hidden或cache中找ViewHolder
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
//如果找到ViewHolder,檢查ViewHolder的合法性
if (holder != null) {
//檢查ViewHolder的是否被移除,position是否越界等,如果檢查通過返回true,失敗返回false
if (!validateViewHolderForOffsetPosition(holder)) {
//檢查不通過
//上述講過dryRun爲false
if (!dryRun) {
//設置這個ViewHolder爲無效標誌
holder.addFlags(ViewHolder.FLAG_INVALID);
//把這個ViewHolder從scrap列表中移除
if (holder.isScrap()) {
removeDetachedView(holder.itemView, false);
holder.unScrap();
}
//...
//把這個ViewHolder放入cache列表中或mRecyclerPool中
recycleViewHolderInternal(holder);
}
//置空不匹配的ViewHolder,進入下一步查找
holder = null;
} else {
//檢查通過了
fromScrapOrHiddenOrCache = true;
}
}
}
if (holder == null) {
//...
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
//這裏可以看到我們熟悉的Adapter中的getItemViewType方法,重寫此方法可以讓RecyclerView顯示多種type的itemView
final int type = mAdapter.getItemViewType(offsetPosition);
//如果mAdapter.hasStableIds()爲true,就進入第2次查找,默認返回false
if (mAdapter.hasStableIds()) {
//2、第2次查找,根據ViewHolder的type和id從scrap或cached列表查找
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) { //找到了
//更新ViewHolder的位置
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
if (holder == null && mViewCacheExtension != null) {
//3、第3次查找,從自定義緩存中查找,一般我們不會重寫ViewCacheExtension
final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
//...
}
if (holder == null) {
//4、第4次查找,從RecycledViewPool中查找,可以看到這裏會根據type返回一個使用過的ViewHolder給你
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {//找到了
//重置ViewHolder中的信息
holder.resetInternal();
//...
}
}
//前面的4次還找不到合適的ViewHolder,就重新創建一個
if (holder == null) {
//...
//5、這裏會調用Adapter中的OnCreateViewHolder方法
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
}
//...
boolean bound = false;
//6、只要滿足以下3個情況:
//1、ViewHolder沒有被綁定過,即沒有設置FLAG_BOUND標誌位
//2、ViewHolder需要更新,即設置了FLAG_UPDATE標誌位
//3、ViewHolder是無效的,即設置了FLAG_INVALID標誌位
//就會調用Adapter中的OnBindViewHolder方法
if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
//這裏最終調用Adapter中的OnBindViewHolder方法
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
}
看起來函數很長但是步驟還是很清晰的,我們把它分爲註釋1、2、3、4、5、6來看:
1、調用getScrapOrHiddenOrCachedHolderForPosition()
註釋1中通過position從scrap或hidden或cache中找ViewHolder,我們來看getScrapOrHiddenOrCachedHolderForPosition方法的關鍵源碼:
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
//1.1、第一次嘗試,從mAttachedScrap找到一個精確,沒有失效的ViewHolder並返回
final int scrapCount = mAttachedScrap.size();
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())){
//標誌這個ViewHolder是從mAttachedScrap取出並返回的
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
//1.2、第二次嘗試,dryRun爲false,從RecyclerView中隱藏的itemView中找,如果找到合適的View,就讓它顯示並把它從RecyclerView中剝離,然後根據這個View的LayoutParam獲取ViewHolder,最後把這個ViewHolder放入mAttachedScrap並返回
if (!dryRun) {
View view = mChildHelper.findHiddenNonRemovedView(position);
if (view != null) {
//獲取ViewHolder
final ViewHolder vh = getChildViewHolderInt(view);
//顯示這個View
mChildHelper.unhide(view);
//從RecyclerView剝離這個View
mChildHelper.detachViewFromParent(layoutIndex);
//把這個ViewHolder放入mAttachedScrap
scrapView(view);
vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
| ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
//返回
return vh;
}
}
//1.3、第三次嘗試,從mCachedViews找到沒有失效的ViewHolder並返回
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
if (!dryRun) {
mCachedViews.remove(i);
}
return holder;
}
}
return null;
}
可以看到註釋1的第一次查找,裏面分爲3步:
- 1.1、從mAttachedScrap找。
- 1.2、如果上一步沒有得到合適的緩存,從HiddenViews找。
- 1.3、如果上一步沒有得到合適的緩存,從mCachedViews找。
從上面3個步驟之一找到,就返回ViewHolder,然後檢查ViewHolder的有效性,如果無效,則從mAttachedScrap中移除,並加入到mCacheViews或者mRecyclerPool中,並且將ViewHolder置爲null,走到下一步。
2、調用getScrapOrCachedViewForId()
下一步就是註釋2,如果我們通過Adapter.setHasStableIds(boolean)設置爲true,就會進入,裏面根據ViewHolder的type和id從scrap或cached列表查找ViewHolder,我們來看一下相關源碼該方法的相關源碼:
ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {
//2.1、第一次嘗試,從mAttachedScrap找到一個id相同並且沒有從mAttachedScrap取出並返回過的ViewHolder,還要type相同的ViewHolder返回
final int count = mAttachedScrap.size();
for (int i = count - 1; i >= 0; i--) {
final ViewHolder holder = mAttachedScrap.get(i);
if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
//id相同type相同
if (type == holder.getItemViewType()) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
//...
}
return holder;
} else if (!dryRun) {
//id相同但type不同
//從mAttachedScrap移除這個ViewHolder
mAttachedScrap.remove(i);
removeDetachedView(holder.itemView, false);
//把這個ViewHolder放入caches或RecyclerViewPool
quickRecycleScrapView(holder.itemView);
}
}
}
//2.2、第2次嘗試,從mCachedViews中找到一個id相同並且type相同的ViewHolder返回
final int cacheSize = mCachedViews.size();
for (int i = cacheSize - 1; i >= 0; i--) {
final ViewHolder holder = mCachedViews.get(i);
if (holder.getItemId() == id) {
//id相同並且type相同
if (type == holder.getItemViewType()) {
if (!dryRun) {
//從cache中移除
mCachedViews.remove(i);
}
return holder;
} else if (!dryRun) {
//id相同type不相同
//把這個ViewHolder從cache中移除並放入RecyclerViewPool中
recycleCachedViewAt(i);
return null;
}
}
}
return null;
}
可以看到註釋2的第二次查找,裏面分爲2步:
- 2.1、從mAttachedScrap找。
- 2.2、如果上一步沒有得到合適的緩存,從mCachedViews找。
第二次查找跟第一次不同的是,它是通過Adapter.getItemId(position)獲得該位置ViewHolder的id,來查找ViewHolder,我們可以重寫Adapter.getItemId(position)返回每個position的ViewHolder的id,默認返回RecyclerView.NO_ID。從上面2個步驟之一找到,就返回ViewHolder,如果找不到就進入下一步。
3、從ViewCacheExtension中找
註釋3的第三次查找是從自定義緩存中查找,這個沒什麼好說,可以直接到下一步。
4、從RecyclerViewRool中找
下一步就是第4次查找,從RecyclerdViewPool中查找,可以看到這裏先使用getRecyclerViewPool獲得Recycler中的RecyclerViewPool,然後調用RecyclerViewPool的getRecycledView(type)根據type獲取一個ViewHolder,我們來看該方法的源碼:
public ViewHolder getRecycledView(int viewType) {
//根據type取出ScrapData
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
//取出ScrapData中的ViewHolder列表
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
//返回一個ViewHolder並從pool中刪除
return scrapHeap.remove(scrapHeap.size() - 1);
}
return null;
}
mScrap是SparseArray類型,它會根據type把ViewHolder存放在不同ScrapData中,ScrapData中有一個mScrapHeap,是ArrayList類型,它會存放RecyclerViewPool中放進來的ViewHolder。所以上面這個方法首先會根據type取出ScrapData,然後取出mScrapHeap,如果mScrapHeap有元素,就返回並刪除,然後重置這個ViewHolder讓它複用,如果沒有就進入下一步。
5、調用Adapter的createViewHolder()
既然緩存中沒有就創建一個,該方法的相關源碼如下:
public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) {
//...
final VH holder = onCreateViewHolder(parent, viewType);
}
可以看到,調用了我們熟悉的onCreateViewHolder方法,該方法就是用來創建ViewHolder。
到這裏,經過tryGetViewHolderForPositionByDeadline方法中的註釋1、2、3、4、5步驟之一拿到了ViewHolder,接下來就是看是否需要調用Adapter的OnBindViewHolder方法綁定ViewHolder。
6、根據情況調用Adapter的OnBindViewHolder()
從上面知道當緩存中不能提供ViewHolder就會調用adapter的onCreateViewHolder創建一個,那麼我們同樣熟悉的OnBindViewHolder方法是什麼時候執行的呢?如下:
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
//...
ViewHolder holder = null;
//...
//走到這裏表示holder已經經過各種手段賦值了
//6、只要滿足以下3個情況:
//1、ViewHolder沒有被綁定過,即沒有設置FLAG_BOUND標誌位
//2、ViewHolder需要更新,即設置了FLAG_UPDATE標誌位
//3、ViewHolder是無效的,即設置了FLAG_INVALID標誌位
//就會調用Adapter中的OnBindViewHolder方法
if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
//這裏最終調用Adapter中的OnBindViewHolder方法
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
}
private boolean tryBindViewHolderByDeadline(ViewHolder holder, int offsetPosition, int position, long deadlineNs) {
//...
//調用Adapter的OnBindViewHolder方法
mAdapter.bindViewHolder(holder, offsetPosition);
return true;
}
public abstract static class ViewHolder {
//...
boolean isBound() {
return (mFlags & FLAG_BOUND) != 0;
}
boolean needsUpdate() {
return (mFlags & FLAG_UPDATE) != 0;
}
boolean isInvalid() {
return (mFlags & FLAG_INVALID) != 0;
}
}
holder.isBound()、holder.needsUpdate() 、holder.isInvalid()方法中會分別判斷ViewHolder中有沒有設置FLAG_BOUND標誌位、FLAG_UPDATE標誌位、FLAG_INVALID標誌位,只要滿足3種情況之一,就會調用Adapter的OnBindViewHolder方法綁定數據,這3種情況的解釋如下:
- 1、沒有設置FLAG_BOUND標誌位:它表示ViewHolder沒有調用過OnBindViewHolder方法,一般是調用Adapter的OnCreateViewHolder方法創建的ViewHolder會出現這種情況;
- 2、設置了FLAG_UPDATE標誌位:它表示ViewHolder需要更新,一般是調用了Adapter的定向更新的相關方法或者ViewHolder是從RecycledViewPool中取出的就會出現這種情況;
- 3、設置了FLAG_INVALID標誌位:它表示ViewHolder是無效的,一般是從mAttachedScrap或mCacheViews中取出ViewHolder後,發現它滿足被移除或者position越界了等不合法的條件,就會把取出ViewHolder設置FLAG_INVALID標誌位,標誌無效,然後調用recycleViewHolderInternal方法把它放入mCacheViews或RecycledViewPool中,在recycleViewHolderInternal方法中ViewHolder首先會被嘗試放入mCacheViews(默認大小爲2)中,如果滿了,就會利用先進先出原則,把老的ViewHolder移到mRecyclerPool中。
bind方法是用來綁定數據,對於從mAttachedScrap中拿出來的ViewHolder是不用重新bind的,而對於從mRecyclerPool拿出和通過Create方法創建的ViewHolder是需要重新bind的,而對於從mCacheViews中拿出的ViewHolder有可能會被bind,當調用getScrapOrHiddenOrCachedHolderForPosition方法根據position獲取ViewHolder時,如果這個ViewHoler是從mCacheViews中取出的,說明滿足有效的、positioin匹配這兩種情況,如果這個ViewHolder同時是合法的,那麼這個ViewHolder不需要重新bind,而如果是不合法的,就會標誌無效,再次放入mCacheViews中(有可能會移動到mRecyclerPool),等待調用getScrapOrCachedViewForId方法根據type和id從mCacheViews再次獲取這個已經被標記爲無效的ViewHolder,如果這個無效的ViewHolder的type和id都匹配的話,就會獲取這個無效的ViewHolder,而此時這個ViewHolder是需要重新bind的。
從前面的分析來看,mAttachedScrap和mCacheViews都是position匹配或者type和id匹配纔會命中返回ViewHolder,而mRecyclerPool則沒有這些限制,只要mRecyclerPool中相應type類型的ViewHolder緩存有,就會命中返回ViewHolder,且優先級mAttachedScrap > mCacheViews > mRecyclerPool,通過以下3個場景,加深大家理解mAttachedScrap、mCacheViews、mRecyclerPool的作用:
1、當RecyclerView列表上下滑動時,屏幕內的ViewHolder會被緩存到mAttachedScrap中,在屏幕內改變位置的ViewHolder復位後,很快會從mAttachedScrap複用到原位置上;
2、當RecyclerView列表向上滑動,列表頂部有ViewHolder滑出屏幕,滑出屏幕的ViewHolder會被緩存到mCacheViews中,當列表向下滑動復位時,滑出屏幕的ViewHolder很快從mCacheViews複用到原位置上;
3、當RecyclerView列表向上滑動,列表頂部有ViewHolder滑出屏幕,滑出屏幕的ViewHolder會被緩存到mCacheViews中,多餘的會移動到mRecyclerPool中,列表底部有空餘的ViewHolder位置,這時會從mRecyclerPool取出ViewHolder複用,填充底部空餘的ViewHolder位置。
總結
本文中源碼角度簡單的分析RecyclerView佈局一個itemView時是怎樣通過Recycler來獲取一個ViewHolder,從而獲取itemView,如圖:
準確的來說,Recycler是RecyclerView的itemView的提供者和管理者,它在內部封裝了RecyclerView的緩存設計實現,在RecyclerView中有着四級緩存:AttachedScrap,mCacheViews,ViewCacheExtension,RecycledViewPool,正因爲這樣RecyclerView在使用的時候效率更好。
參考文章: