這是RecyclerView
快取機制系列文章的第一篇,系列文章的目錄如下:
如果想直接看結論可以移步到第四篇末尾(你會後悔的,過程更加精彩)。
引子
- 如果列表中每個移出螢幕的表項都直接銷燬,移入時重新建立,很不經濟。所以RecyclerView引入了快取機制。
- 回收是為了複用,複用的好處是有可能免去兩個昂貴的操作:
- 為表項檢視繫結資料
- 建立表項檢視
- 下面幾個問題對於理解“回收複用機制”很關鍵:
- what:回收什麼?複用什麼?
- where:回收到哪裡去?從哪裡獲得複用?
- when:什麼時候回收?什麼時候複用?
這一篇試著從已知的知識出發在原始碼中尋覓未知的“RecyclerView複用機制”。
(ps: 下文中的 粗斜體字 表示引導原始碼閱讀的內心戲)
尋覓
觸發複用的眾多時機中必然包含下面這種:“當移出螢幕的表項重新回到介面”。表項本質上是一個View,螢幕上的表項必然需要依附於一棵View樹,即必然有一個父容器呼叫了addView()
。而 RecyclerView
繼承自 ViewGroup
,遂以RecyclerView.addView()
為切入點向上搜尋複用的程式碼。
在RecyclerView.java
中全域性搜尋“addView”,發現RecyclerView()
並沒有對addView()
函式過載,但找到一處addView()
的呼叫:
//RecyclerView是ViewGroup的子類
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
...
private void initChildrenHelper() {
mChildHelper = new ChildHelper(new ChildHelper.Callback() {
...
@Override
public void addView(View child, int index) {
if (VERBOSE_TRACING) {
TraceCompat.beginSection("RV addView");
}
//直接呼叫ViewGroup.addView()
RecyclerView.this.addView(child, index);
if (VERBOSE_TRACING) {
TraceCompat.endSection();
}
dispatchChildAttached(child);
}
}
}
...
}
複製程式碼
以ChildHelper.Callback.addView()
為起點沿著呼叫鏈繼續向上搜尋,經歷ChildHelper.addView()
---LayoutManager.addViewInt()
---LayoutManager.addView()
最終到達LayoutManager.layoutChunk()
:
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
//獲得下一個表項
View view = layoutState.next(recycler);
if (view == null) {
if (DEBUG && layoutState.mScrapList == null) {
throw new RuntimeException("received null view when unexpected");
}
// if we are laying out views in scrap, this may return null which means there is
// no more items to layout.
result.mFinished = true;
return;
}
LayoutParams params = (LayoutParams) view.getLayoutParams();
if (layoutState.mScrapList == null) {
//將表項插入到列表中
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
} else {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addDisappearingView(view);
} else {
addDisappearingView(view, 0);
}
}
...
}
複製程式碼
addView(view)
中傳入的view
是函式layoutState.next()
的返回值。猜測該函式是用來獲得下一個表項的。表項不止一個,應該有一個迴圈不斷的獲得下一個表項才對。 沿著剛才的呼叫鏈繼續往上搜尋,就會發現:的確有一個迴圈!
public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
...
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
...
//recyclerview 剩餘空間
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
//不斷填充,直到空間消耗完畢
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
if (VERBOSE_TRACING) {
TraceCompat.beginSection("LLM LayoutChunk");
}
//填充一個表項
layoutChunk(recycler, state, layoutState, layoutChunkResult);
...
}
...
}
}
複製程式碼
而fill()
是在onLayoutChildren()
中被呼叫:
/**
* Lay out all relevant child views from the given adapter.
* 佈局所有給定adapter中相關孩子檢視
* 註釋太長了,省略了不相關資訊
* @param recycler Recycler to use for fetching potentially cached views for a
* position
* @param state Transient state of RecyclerView
*/
public void onLayoutChildren(Recycler recycler, State state) {
Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
}
複製程式碼
看完註釋,感覺前面猜測應該是正確的。onLayoutChildren()
是用來佈局RecyclerView
中所有的表項的。回頭去看一下layoutState.next()
,表項複用邏輯應該就在其中。
/**
* Helper class that keeps temporary state while {LayoutManager} is filling out the empty
* space.
*/
static class LayoutState {
/**
* Gets the view for the next element that we should layout.
* 獲得下一個元素的檢視用於佈局
* Also updates current item index to the next item, based on {@link #mItemDirection}
*
* @return The next element that we should layout.
*/
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
//呼叫了Recycler.getViewForPosition()
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
}
複製程式碼
最終呼叫了Recycler.getViewForPosition()
,Recycler是回收器的意思,感覺離想要找的“複用”邏輯越來越近了。 Recycler
到底是做什麼用的? :
/**
* A Recycler is responsible for managing scrapped or detached item views for reuse.
* Recycler負責管理scrapped和detached表項的複用
* <p>A "scrapped" view is a view that is still attached to its parent RecyclerView but
* that has been marked for removal or reuse.</p>
*/
public final class Recycler {
...
}
複製程式碼
終於找到你~~ ,Recycler
用於表項的複用!沿著Recycler.getViewForPosition()
的呼叫鏈繼續向下搜尋,找到了一個關鍵函式(函式太長了,為了防止頭暈,只列出了關鍵節點):
/**
* Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
* cache, the RecycledViewPool, or creating it directly.
* 嘗試獲得指定位置的ViewHolder,要麼從scrap,cache,RecycledViewPool中獲取,要麼直接重新建立
* @return ViewHolder for requested position
*/
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
...
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
//0 從changed scrap集合中獲取ViewHolder
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
//1. 通過position從attach scrap或一級回收快取中獲取ViewHolder
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
...
}
if (holder == null) {
...
final int type = mAdapter.getItemViewType(offsetPosition);
//2. 通過id在attach scrap集合和一級回收快取中查詢viewHolder
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
...
}
//3. 從自定義快取中獲取ViewHolder
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);
...
}
//4.從快取池中拿ViewHolder
if (holder == null) { // fallback to pool
...
holder = getRecycledViewPool().getRecycledView(type);
...
}
//所有快取都沒有命中,只能建立ViewHolder
if (holder == null) {
...
holder = mAdapter.createViewHolder(RecyclerView.this, type);
...
}
}
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
// do not update unless we absolutely have to.
holder.mPreLayoutPosition = position;
}
//只有invalid的viewHolder才能繫結檢視資料
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
+ exceptionLabel());
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
//獲得ViewHolder後,繫結檢視資料
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
...
return holder;
}
複製程式碼
- 函式的名字以“tryGet”開頭,“嘗試獲得”表示可能獲得失敗,再結合註釋中說的:“嘗試獲得指定位置的ViewHolder,要麼從scrap,cache,RecycledViewPool中,要麼直接重新建立。”猜測scrap,cache,RecycledViewPool是回收表項的容器,相當於表項快取,如果快取未命中則只能重新建立。
- 函式的返回值是
ViewHolder
,難道回收和複用的是ViewHolder
? 函式開頭宣告瞭區域性變數ViewHolder holder = null;
最終返回的也是這個區域性變數,並且有4處holder == null
的判斷,這樣的程式碼結構是不是有點像快取?每次判空意味著上一級快取未命中並繼續嘗試新的獲取方法?快取是不是有不止一種儲存形式? 讓我們一次一次地看:
第一次嘗試
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
...
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
...
}
複製程式碼
只有在mState.isPreLayout()
為true
時才會做這次嘗試,這應該是一種特殊情況,先忽略。
第二次嘗試
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
...
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
//下面一段程式碼蘊含著一個線索,買個伏筆,先把他略去
...
}
...
}
複製程式碼
- 當第一次嘗試失敗後,嘗試通過
getScrapOrHiddenOrCachedHolderForPosition()
獲得ViewHolder
。 - 這裡故意省略了一段程式碼,先埋個伏筆,待會分析。先沿著獲取
ViewHolder
的呼叫鏈繼續往下:
//省略非關鍵程式碼
/**
* Returns a view for the position either from attach scrap, hidden children, or cache.
* 從attach scrap,hidden children或者cache中獲得指定位置上的一個ViewHolder
*/
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();
// Try first for an exact, non-invalid match from scrap.
//1.在attached scrap中搜尋ViewHolder
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())) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
//2.從移除螢幕的檢視中搜尋ViewHolder,找到了之後將他存入scrap回收集合中
if (!dryRun) {
View view = mChildHelper.findHiddenNonRemovedView(position);
if (view != null) {
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;
}
}
// Search in our first-level recycled view cache.
//3.在快取中搜尋ViewHolder
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
//若找到ViewHolder,還需要對ViewHolder的索引進行匹配判斷
if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
...
return holder;
}
}
return null;
}
複製程式碼
依次從三個地方搜尋ViewHolder
:1. mAttachedScrap
2. 隱藏表項 3. mCachedViews
,找到立即返回。
其中mAttachedScrap
和mCachedViews
作為Recycler
的成員變數,用來儲存一組ViewHolder
:
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
...
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
...
RecycledViewPool mRecyclerPool;
}
複製程式碼
- 看到這裡應該可以初步得出結論:RecyclerView回收機制中,回收複用的物件是
ViewHolder
,且以ArrayList
為結構儲存在Recycler
物件中。 RecycledViewPool mRecyclerPool;
看著也像是回收容器,那待會是不是也會到這裡拿ViewHolder
?- 值得注意的是,當成功從
mCachedViews
中獲取ViewHolder
物件後,還需要對其索引進行判斷,這就意味著mCachedViews
中快取的ViewHolder
只能複用於指定位置 ,打個比方:手指向上滑動,列表向下滾動,第2個表項移出螢幕,第4個表項移入螢幕,此時再滑回去,第2個表項再次出現,這個過程中第4個表項不能複用被回收的第2個表項的ViewHolder
,因為他們的位置不同,而再次進入螢幕的第2個表項就可以成功複用。 待會可以對比一下其他複用是否也需要索引判斷 - 回到剛才埋下的伏筆,把第二次嘗試獲取
ViewHolder
的程式碼補全:
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
...
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
//下面一段程式碼蘊含這一個線索,買個伏筆,先把他略去
if (holder != null) {
//檢驗ViewHolder有效性
if (!validateViewHolderForOffsetPosition(holder)) {
// recycle holder (and unscrap if relevant) since it can not 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);
if (holder.isScrap()) {
removeDetachedView(holder.itemView, false);
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
//若不滿足有效性檢驗,則回收ViewHolder
recycleViewHolderInternal(holder);
}
holder = null;
} else {
fromScrapOrHiddenOrCache = true;
}
}
}
...
}
複製程式碼
如果成功獲得ViewHolder
則檢驗其有效性,若檢驗失敗則將其回收。好不容易獲取了ViewHoler
物件,一言不合就把他回收?難道對所有複用的 ViewHolder
都有這麼嚴格的檢驗嗎? 暫時無法回答這些疑問,還是先把複用邏輯看完吧:
第三次嘗試
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
...
//只有當Adapter設定了id,才會進行這次查詢
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
...
}
複製程式碼
這一次嘗試呼叫的函式名(“byId”)和上一次(“byPosition”)只是字尾不一樣。上一次是通過表項位置,這一次是通過表項id。內部實現也幾乎一樣,判斷的依據從表項位置變成表項id。為表項設定id屬於特殊情況,先忽略。
第四次嘗試
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
...
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) {
//獲得view對應的ViewHolder
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());
}
}
}
...
}
複製程式碼
經過從mAttachedScrap
和mCachedViews
獲取ViewHolder
未果後,繼續嘗試通過ViewCacheExtension
獲取:
/**
* ViewCacheExtension is a helper class to provide an additional layer of view caching that can
* be controlled by the developer.
* ViewCacheExtension提供了額外的表項快取層,使用者幫助開發者自己控制表項快取
* <p>
* When {@link Recycler#getViewForPosition(int)} is called, Recycler checks attached scrap and
* first level cache to find a matching View. If it cannot find a suitable View, Recycler will
* call the {@link #getViewForPositionAndType(Recycler, int, int)} before checking
* {@link RecycledViewPool}.
* 當Recycler從attached scrap和first level cache中未能找到匹配的表項時,它會在去RecycledViewPool中查詢之前,先嚐試從自定義快取中查詢
* <p>
*/
public abstract static class ViewCacheExtension {
/**
* Returns a View that can be binded to the given Adapter position.
* <p>
* This method should <b>not</b> create a new View. Instead, it is expected to return
* an already created View that can be re-used for the given type and position.
* If the View is marked as ignored, it should first call
* {@link LayoutManager#stopIgnoringView(View)} before returning the View.
* <p>
* RecyclerView will re-bind the returned View to the position if necessary.
*/
public abstract View getViewForPositionAndType(Recycler recycler, int position, int type);
}
複製程式碼
註釋揭露了很多資訊:ViewCacheExtension用於開發者自定義表項快取,且這層快取的訪問順序位於mAttachedScrap
和mCachedViews
之後,RecycledViewPool
之前。這和Recycler. tryGetViewHolderForPositionByDeadline()
中的程式碼邏輯一致,那接下來的第五次嘗試,應該是從 RecycledViewPool
中獲取 ViewHolder
第五次嘗試
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
...
if (holder == null) {
...
//從回收池中獲取ViewHolder物件
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
...
}
複製程式碼
前四次嘗試都未果,最後從RecycledViewPool
中獲取ViewHolder
。稍等片刻!相對於從mAttachedScrap
和 mCachedViews
中獲取 ViewHolder
,此處並沒有嚴格的檢驗邏輯。為啥要區別對待不同的快取? 大大的問號懸在頭頂,但現在暫時無法解答,還是接著看RecycledViewPool
的結構吧~
public final class Recycler {
...
RecycledViewPool mRecyclerPool;
//獲得RecycledViewPool例項
RecycledViewPool getRecycledViewPool() {
if (mRecyclerPool == null) {
mRecyclerPool = new RecycledViewPool();
}
return mRecyclerPool;
}
...
}
public static class RecycledViewPool {
...
//從回收池中獲取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;
}
...
}
複製程式碼
函式中只要訪問了類成員變數,它的複雜度就提高了,因為類成員變數的作用於超出了函式體,使得函式就和類中其他函式耦合,所以不得不進行閱讀更多以幫助理解該函式:
public static class RecycledViewPool {
//同類ViewHolder快取個數上限
private static final int DEFAULT_MAX_SCRAP = 5;
/**
* Tracks both pooled holders, as well as create/bind timing metadata for the given type.
* 回收池中存放單個型別ViewHolder的容器
*/
static class ScrapData {
//同類ViewHolder儲存在ArrayList中
ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
//每種型別的ViewHolder最多存5個
int mMaxScrap = DEFAULT_MAX_SCRAP;
}
//回收池中存放所有型別ViewHolder的容器
SparseArray<ScrapData> mScrap = new SparseArray<>();
...
//ViewHolder入池 按viewType分類入池,一個型別的ViewType存放在一個ScrapData中
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;
}
if (DEBUG && scrapHeap.contains(scrap)) {
throw new IllegalArgumentException("this scrap item already exists");
}
scrap.resetInternal();
//回收時,ViewHolder從列表尾部插入
scrapHeap.add(scrap);
}
//從回收池中獲取ViewHolder物件
public ViewHolder getRecycledView(int viewType) {
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
//複用時,從列表尾部獲取ViewHolder(優先複用剛入池的ViewHoler)
return scrapHeap.remove(scrapHeap.size() - 1);
}
return null;
}
}
複製程式碼
- 上述程式碼列出了
RecycledViewPool
中最關鍵的一個成員變數和兩個函式。至此可以得出結論:RecycledViewPool
中的ViewHolder
儲存在SparseArray
中,並且按viewType
分類儲存(即是Adapter.getItemViewType()的返回值),同一型別的ViewHolder
存放在ArrayList
中,且預設最多儲存5個。 - 相比較於
mCachedViews
,從mRecyclerPool
中成功獲取ViewHolder
物件後並沒有做合法性和表項位置校驗,只檢驗viewType
是否一致。所以 從mRecyclerPool
中取出的ViewHolder
只能複用於相同viewType
的表項。
建立ViewHolder並繫結資料
ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
...
//所有快取都沒有命中,只能建立ViewHolder
if (holder == null) {
...
holder = mAdapter.createViewHolder(RecyclerView.this, type);
...
}
...
boolean bound = false;
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
+ exceptionLabel());
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
//為表項繫結資料
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
}
複製程式碼
- 再進行了上述所有嘗試後,如果依然沒有獲得
ViewHolder
,只能重新建立並繫結資料。沿著呼叫鏈往下,就會找到熟悉的onCreateViewHolder()
和onBindViewHolder()
。 - 繫結資料的邏輯巢狀在一個大大的if中(原來並不是每次都要繫結資料,只有滿足特定條件時才需要繫結。 )
- 那什麼情況下需要繫結,什麼情況下不需要呢?這就要引出“快取優先順序”這個概念。
快取優先順序
-
快取有優先順序一說,在使用圖片二級快取(記憶體+磁碟)時,會先嚐試去優先順序高的記憶體中獲取,若未命中再去磁碟中獲取。優先順序越高意味著效能越好。
RecyclerView
的快取機制中是否也能套用“快取優先順序”這一邏輯? -
雖然為了獲取
ViewHolder
做了5次嘗試(共從6個地方獲取),先排除3種特殊情況,即從mChangedScrap
獲取、通過id獲取、從自定義快取獲取,正常流程中只剩下3種獲取方式,優先順序從高到低依次是:- 從
mAttachedScrap
獲取 - 從
mCachedViews
獲取 - 從
mRecyclerPool
獲取
- 從
-
這樣的快取優先順序是不是意味著,對應的複用效能也是從高到低?(複用效能越好意味著所做的昂貴操作越少)
- 最壞情況:重新建立
ViewHodler
並重新繫結資料 - 次好情況:複用
ViewHolder
但重新繫結資料 - 最好情況:複用
ViewHolder
且不重新繫結資料
毫無疑問,所有快取都未命中的情況下會發生最壞情況。剩下的兩種情況應該由3種獲取方式來分攤,猜測優先順序最低的
mRecyclerPool
方式應該命中次好情況,而優先順序最高的mAttachedScrap
應該命中最好情況,去原始碼中驗證一下: - 最壞情況:重新建立
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();
// Try first for an exact, non-invalid match from scrap.
//1.從attached scrap回收集合中
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
//只有當holder是有效時才返回
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
}
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
...
if (holder == null) {
...
//從回收池中獲取ViewHolder物件
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
//重置ViewHolder
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
...
//如果表項沒有繫結過資料 或 表項需要更新 或 表項無效 且表項沒有被移除時繫結表項資料
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
+ exceptionLabel());
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
//為表項繫結資料
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
...
}
public abstract static class ViewHolder {
/**
* This ViewHolder has been bound to a position; mPosition, mItemId and mItemViewType
* are all valid.
* 繫結標誌位
*/
static final int FLAG_BOUND = 1 << 0;
/**
* This ViewHolder’s data is invalid. The identity implied by mPosition and mItemId
* are not to be trusted and may no longer match the item view type.
* This ViewHolder must be fully rebound to different data.
* 無效標誌位
*/
static final int FLAG_INVALID = 1 << 2;
//判斷ViewHolder是否無效
boolean isInvalid() {
//將當前ViewHolder物件的flag和無效標誌位做位與操作
return (mFlags & FLAG_INVALID) != 0;
}
//判斷ViewHolder是否被繫結
boolean isBound() {
//將當前ViewHolder物件的flag和繫結標誌位做位與操作
return (mFlags & FLAG_BOUND) != 0;
}
/**
* 將ViewHolder重置
*/
void resetInternal() {
//將ViewHolder的flag置0
mFlags = 0;
mPosition = NO_POSITION;
mOldPosition = NO_POSITION;
mItemId = NO_ID;
mPreLayoutPosition = NO_POSITION;
mIsRecyclableCount = 0;
mShadowedHolder = null;
mShadowingHolder = null;
clearPayload();
mWasImportantForAccessibilityBeforeHidden = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
clearNestedRecyclerViewIfNotNested(this);
}
}
複製程式碼
溫故知新,回看 mRecyclerPool
複用邏輯時,發現在成功獲得ViewHolder
物件後,立即對其重置(將flag置0)。這樣就滿足了繫結資料的判斷條件(因為0和非0位與之後必然為0)。
同樣的,在才mAttachedScrap
中獲取ViewHolder
時,只有當其是有效的才會返回。所以猜測成立:從mRecyclerPool
中複用的ViewHolder
需要重新繫結資料,從mAttachedScrap
中複用的ViewHolder
不要重新出建立也不需要重新繫結資料。
總結
- 在
RecyclerView
中,並不是每次繪製表項,都會重新建立ViewHolder
物件,也不是每次都會重新繫結ViewHolder
資料。 RecyclerView
通過Recycler
獲得下一個待繪製表項。Recycler
有4個層次用於快取ViewHolder
物件,優先順序從高到底依次為ArrayList<ViewHolder> mAttachedScrap
、ArrayList<ViewHolder> mCachedViews
、ViewCacheExtension mViewCacheExtension
、RecycledViewPool mRecyclerPool
。如果四層快取都未命中,則重新建立並繫結ViewHolder
物件RecycledViewPool
對ViewHolder
按viewType
分類儲存(通過SparseArray
),同類ViewHolder
儲存在預設大小為5的ArrayList
中- 從
mRecyclerPool
中複用的ViewHolder
需要重新繫結資料,從mAttachedScrap
中複用的ViewHolder
不需要重新建立也不需要重新繫結資料。 - 從
mRecyclerPool
中複用的ViewHolder
,只能複用於viewType
相同的表項,從mCachedViews
中複用的ViewHolder
,只能複用於指定位置的表項。
這篇文章粗略的回答了關於“複用”的4個問題,即“複用什麼?”、“從哪裡獲得複用?”、“什麼時候複用?”、“複用優先順序”。讀到這裡,可能會有很多疑問:
scrap view
是什麼?changed scrap view
和attached scrap view
有什麼區別?- 複用的
ViewHolder
是在什麼時候被快取的? - 為什麼要4層快取?它們的用途有什麼區別?
分析完“複用”,後續文章會進一步分析“回收”,希望到時候這些問題都能迎刃而解。