【進階】RecyclerView原始碼解析(二)——快取機制
引言
接著上一篇部落格分析完RecyclerView的繪製流程,其實對RecyclerView已經有了一個大體的瞭解,尤其是RecyclerView和LayoutManager和ItemDecoration的關係。 本篇文章將對RecyclerView的快取機制的講解,但由於快取對於RecyclerView非常重要,所以準備分幾部分進行分析,本篇部落格主要從原始碼角度進行分析快取的流程。
前言
無論是原來使用的ListView還是RecyclerView,列表型別的檢視一直是原生使用的一個重頭戲。無論是從使用功能上還是效能上,原生的列表檢視都有著巨大的優勢,而這個優勢很重要的一方面其實就是對於檢視的複用機制,也就是快取
總流程圖
放上一張Bugly的一篇部落格對RecyclerView的快取的流程圖吧(自己畫發現差不多就直接挪過來了…若侵立刪)
原始碼分析
如果看過上一篇部落格的人應該還記得我們當中提到了和快取機制有關的那個函式。
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
//next方法很重要
View view = layoutState.next(recycler);
//執行addView
//執行measureChild操作
這裡再放上這行程式碼,沒錯就是next函式。
View next(RecyclerView.Recycler recycler) {
//預設mScrapList=null,但是執行layoutForPredictiveAnimations方法的時候不會為空
if (mScrapList != null) {
return nextViewFromScrapList();
}
//重要,從recycler獲得View,mScrapList是被LayoutManager持有,recycler是被RecyclerView持有
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
而next函式這裡也放了上來,其實可以看到,除了我們平常認知的RecyclerView中Recycler的快取,這裡其實還存在一級的快取mScrapList,mScrapList是被LayoutManager持有,recycler是被RecyclerView持有。但是mScrapList其實一定程度上和動畫有關,這裡就不做分析了,所以可以看到,快取的重頭戲還是在RecyclerView中的內部類Recycler中。這裡先對Recycler這個內部類大體瞭解一下。
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<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;
...
}
類的結構也比較清楚,這裡可以清楚的看到我們後面講到的四級快取機制所用到的類都在這裡可以看到:
* 1.一級快取:mAttachedScrap
* 2.二級快取:mCacheViews
* 3.三級快取:mViewCacheExtension
* 4.四級快取:mRecyclerPool
繼續跟進getViewForPosition方法,其實可以發現最後進入的是tryGetViewHolderForPositionByDeadline方法。
/**
* Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
* cache, the RecycledViewPool, or creating it directly.
**/
/**
* 註釋寫的很清楚,從Recycler的scrap,cache,RecyclerViewPool,或者直接create建立
**/
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
if (position < 0 || position >= mState.getItemCount()) {
throw new IndexOutOfBoundsException("Invalid item position " + position
+ "(" + position + "). Item count:" + mState.getItemCount()
+ exceptionLabel());
}
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
//preLayout預設是false,只有有動畫的時候才為true
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
if (!validateViewHolderForOffsetPosition(holder)) {
//如果檢查發現這個holder不是當前position的
// recycle holder (and unscrap if relevant) since it can't be used
if (!dryRun) {
// we would like to recycle this but need to make sure it is not used by
// animation logic etc.
holder.addFlags(ViewHolder.FLAG_INVALID);
//從scrap中移除
if (holder.isScrap()) {
removeDetachedView(holder.itemView, false);
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
//放到ViewCache或者Pool中
recycleViewHolderInternal(holder);
}
//至空繼續尋找
holder = null;
} else {
fromScrapOrHiddenOrCache = true;
}
}
}
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
+ "position " + position + "(offset:" + offsetPosition + ")."
+ "state:" + mState.getItemCount() + exceptionLabel());
}
final int type = mAdapter.getItemViewType(offsetPosition);
// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
//自定義快取
if (holder == null && mViewCacheExtension != null) {
// We are NOT sending the offsetPosition because LayoutManager does not
// know it.
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
if (holder == null) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view which does not have a ViewHolder"
+ exceptionLabel());
} else if (holder.shouldIgnore()) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view that is ignored. You must call stopIgnoring before"
+ " returning this view." + exceptionLabel());
}
}
}
//pool
if (holder == null) { // fallback to pool
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
+ position + ") fetching from shared pool");
}
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
//create
if (holder == null) {
long start = getNanoTime();
if (deadlineNs != FOREVER_NS
&& !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
// abort - we have a deadline we can't meet
return null;
}
holder = mAdapter.createViewHolder(RecyclerView.this, type);
if (ALLOW_THREAD_GAP_WORK) {
// only bother finding nested RV if prefetching
RecyclerView innerView = findNestedRecyclerView(holder.itemView);
if (innerView != null) {
holder.mNestedRecyclerView = new WeakReference<>(innerView);
}
}
long end = getNanoTime();
mRecyclerPool.factorInCreateTime(type, end - start);
}
}
....
return holder;
}
刪除了得到holder後的程式碼(其實還想再刪點的…),本篇部落格主要是對快取機制原始碼的分析。對於原始碼的一個方法第一眼先看一下方法的註釋,這裡專門把方法的註釋放了上來,可以發現註釋寫的很清楚從Recycler的scrap,cache,RecyclerViewPool,或者直接create建立,這可以說是對RecyclerView快取流程的概述:四級快取(不知道為什麼官方的註釋沒有寫上自定義快取…)。接下來就一級一級分析吧。
if (mState.isPreLayout()) {
//preLayout預設是false,只有有動畫的時候才為true
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
首先可以看到這裡有個判斷,當為true的時候也可以拿到holder,但是這裡我們沒有併到常規快取裡面,首先可以看一下判斷條件是對mInPreLayout變數的判斷,mInPreLayout預設是false,只有有動畫的時候才為true。其次對於getChangedScrapViewForPosition方法,其實是從Recycler類中的mChangedScrap獲取ViewHolder,這也是為什麼我們剛才沒有將mChangedScrap放到常規快取裡面。
第一次嘗試(從mAttachedScrap和mCacheView中)
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
if (!validateViewHolderForOffsetPosition(holder)) {
//如果檢查發現這個holder不是當前position的
...
//從scrap中移除
if (holder.isScrap()) {
removeDetachedView(holder.itemView, false);
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
...
}
//放到ViewCache或者Pool中
recycleViewHolderInternal(holder);
}
//至空繼續尋找
holder = null;
} else {
fromScrapOrHiddenOrCache = true;
}
}
}
先大體看一下第一級快取,可以看到,這裡通過getScrapOrHiddenOrCachedHolderForPosition方法來獲取ViewHolder,並檢驗holder的有效性,如果無效,則從mAttachedScrap中移除,並加入到mCacheViews或者Pool中,並且將holder至null,走下一級快取判斷。
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
//---------------------------------------------------------
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();
// Try first for an exact, non-invalid match from scrap.
//先從scrap中尋找
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
return holder;
...
}
//dryRun為false
if (!dryRun) {
//從HiddenView中獲得,這裡獲得是View
View view = mChildHelper.findHiddenNonRemovedView(position);
if (view != null) {
// This View is good to be used. We just need to unhide, detach and move to the
// scrap list.
//通過View的LayoutParam獲得ViewHolder
final ViewHolder vh = getChildViewHolderInt(view);
//從HiddenView中移除
mChildHelper.unhide(view);
....
mChildHelper.detachViewFromParent(layoutIndex);
//新增到Scrap中,其實這裡既然已經拿到了ViewHolder,可以直接傳vh進去
scrapView(view);
vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
| ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
return vh;
}
}
// Search in our first-level recycled view cache.
//從CacheView中拿
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
//holder是有效的,並且position相同
if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
if (!dryRun) {
mCachedViews.remove(i);
}
return holder;
}
}
return null;
}
這裡可以看到也分了三個步驟:
* 1.從mAttachedScrap中獲取
* 2.從HiddenView中獲取
* 3.從CacheView獲取
關鍵的程式碼註釋我已經放上了,流程上可以用下面這個圖來理解:
第二次嘗試(對應hasStablelds情況)
if (holder == null) {
...
final int type = mAdapter.getItemViewType(offsetPosition);
// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
....
}
這裡首先先看一個重點:mAdapter.getItemViewType(offsetPosition);熟悉的方法有木有,可以看到這裡呼叫了我們平常使用RecyclerView進行多樣式item的方法,也就是說前面對於一級快取mAttachedScrap和mCacheViews是不區分type的,從現在開始的判斷是區分type的快取。這裡對於我們研究多type型別的RecyclerView很有幫助。
接下來的判斷可以看到很明顯這是對於我們重寫hasStableIds()方法為true的情況。
ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {
// Look in our attached views first
//
final int count = mAttachedScrap.size();
for (int i = count - 1; i >= 0; i--) {
//在attachedScrap中尋找
final ViewHolder holder = mAttachedScrap.get(i);
if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
//id相同並且不是從scrap中返回的
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.
//從scrap中移除
mAttachedScrap.remove(i);
removeDetachedView(holder.itemView, false);
//加入cacheView或者pool
quickRecycleScrapView(holder.itemView);
}
}
}
//從cacheView中找
// Search the first-level cache
final int cacheSize = mCachedViews.size();
for (int i = cacheSize - 1; i >= 0; i--) {
final ViewHolder holder = mCachedViews.get(i);
if (holder.getItemId() == id) {
if (type == holder.getItemViewType()) {
if (!dryRun) {
//從cache中移除
mCachedViews.remove(i);
}
return holder;
} else if (!dryRun) {
//從cacheView中移除,但是放到pool中
recycleCachedViewAt(i);
return null;
}
}
}
return null;
}
可以看到這裡的判斷其實和上面那一次差不多,需要注意的是多了對於id的判斷和對於type的判斷,也就是當我們將hasStableIds()設為true後需要重寫holder.getItemId() 方法,來為每一個item設定一個單獨的id。具體流程圖如下:
第三次嘗試(對應於自定義快取)
其實這種對於我們平常的使用來說已經很陌生了,甚至很多人都不知道RecyclerView的這一項特性。
//自定義快取
if (holder == null && mViewCacheExtension != null) {
// We are NOT sending the offsetPosition because LayoutManager does not
// know it.
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
if (holder == null) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view which does not have a ViewHolder"
+ exceptionLabel());
} else if (holder.shouldIgnore()) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view that is ignored. You must call stopIgnoring before"
+ " returning this view." + exceptionLabel());
}
}
}
這裡對於流程的理解沒有什麼好說的,我們可以看一下這個自定義快取的類ViewCacheExtension。
public abstract static class ViewCacheExtension {
public abstract View getViewForPositionAndType(Recycler recycler, int position, int type);
}
可以看到這裡類很恐怖,為什麼這樣說哪?
1.首先這個類基本上沒有什麼限制,也就是說無論是快取使用的資料結構還有快取演算法(LRU還是什麼)完全自定義,都由開發者自己決定,這一點可以說既給了開發者很大的便利,也給開發者帶來了很大的隱患。
2.對於平常的快取,我們的理解在怎麼說至少get-add|push-pop都是成對出現,為什麼這樣說的,也就是快取至少有進也有出。而這裡可以看到這裡的抽象類只定義了出的方法,也就是只出不進,進的時機,大小,時效等完全沒有規定。
第四次嘗試(對應於Pool)
終於到了最後一次的嘗試,這個快取是針對Pool的,可以說RecyclerView內部提供的Pool是RecyclerView的一大特性,這也是和ListView不同的地方,RecyclerView提供了這種快取形式,支援多個RecyclerView之間複用View,也就是說通過自定義Pool我們甚至可以實現整個應用內的RecyclerView的View的複用。
if (holder == null) { // fallback to pool
......
holder = getRecycledViewPool().getRecycledView(type);
......
}
同樣這裡對於流程沒有什麼好說的了,可以看一下RecyclerPool的類的結構。
public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 5;
static class ScrapData {
ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
SparseArray<ScrapData> mScrap = new SparseArray<>();
private int mAttachCount = 0;
...
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;
}
......
}
可以看到RecyclerdViewPool內部使用到了Google推薦的資料結構型別SparseArray,而SparseArray內部的key就是我們的ViewType,而value存放的是ArrayList。而預設的每個ArrayList的大小是5個。這裡還有一個要注意的點就是getRecycledView這個方法可以看到拿到viewholder其實是通過remove拿到的,也就是通過remove拿到的。
最終建立
//create
if (holder == null) {
......
holder = mAdapter.createViewHolder(RecyclerView.this, type);
......
}
終於看到了我們經常重寫的方法createViewHolder,當所有的的嘗試從快取中獲取都失敗後,只能呼叫我們自己重寫的createViewHolder方法,重新建立一個。
總結
本篇部落格主要從原始碼的角度將RecyclerView內部的快取獲取的流程梳理了一遍,對於RecyclerView的快取機制還遠遠不止如此,後面還會從別的角度學習RecyclerView的快取機制。從這篇部落格主要能看到以下幾點:
1.RecyclerView內部大體可以分為四級快取:mAttachedScrap,mCacheViews,ViewCacheExtension,RecycledViewPool.
2.mAttachedScrap,mCacheViews只是對View的複用,並且不區分type,ViewCacheExtension,RecycledViewPool是對於ViewHolder的複用,而且區分type。
3.如果快取ViewHolder時發現超過了mCachedView的限制,會將最老的ViewHolder(也就是mCachedView快取佇列的第一個ViewHolder)移到RecycledViewPool中。