最近做項目的時候突然想到一個問題,就是在項目裏面使用了多種ViewHolder,但是在onBindView這個方法中,RecyclerView是如何知道我在哪個位置需要的是哪種ViewHolder呢?就這個問題趁機看了一下源碼,終於找到了答案,原來RecyclerView的緩存機制是區分type的,也就是Recyclerview.Adapter.getItemViewType()這個方法的返回值來區分的。
那麼從哪裏開始看RecyclerView的源碼呢?
因爲View創建出來需要加到RecyclerView中,所以從view的measure、layout中去找,RecyclerView的佈局都是交給LayoutManager,這裏以LinearLayoutManager爲例,從onLayoutChildren開始,調用鏈爲
onLayoutChildren --> fill --> layoutChunk
這裏fill、layoutChunk都是佈局的核心代碼,在layoutChunk開頭有這麼一段:
View view = layoutState.next(recycler);
這個地方返回一個view,傳進去的參數爲recycler,大概可以猜到這和緩存有關了,點進去看看:
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
果然,使用到了recycler.getViewForPosition,這個就是緩存的其實方法,最終看到了tryGetViewHolderForPositionByDeadline這個方法,這裏就是緩存邏輯的核心代碼,這裏只挑重點看:
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
.......
//和動畫有關係,而且scrapList是在LayoutManager內部的,和recyclerview關係不大
if (mState.isPreLayout()) {
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) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
final int type = mAdapter.getItemViewType(offsetPosition);
......
//擴展的緩存
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 (holder == null) { // fallback to pool
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
+ position + ") fetching from shared pool");
}
//從Recycler中獲取緩存
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
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;
}
//創建ViewHolder
holder = mAdapter.createViewHolder(RecyclerView.this, type);
......
}
return holder;
}
上面的流程其實就是按照這麼四級緩存一步一步查找下去,分別是
mAttachedScrap , mCachedViews ,mViewCacheExtension, recycledViewPool
這四級緩存都是由Recycler這個類來管理的
下面分別介紹這四種緩存的作用:
- 一級緩存:mAttachedScrap
這個緩存中的複用是位置必須要相同,而且複用之後不用重新綁定數據,在代碼中可以體現:
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;
}
}
在滑動過程中一般不到這個緩存中去查找,因爲此緩存是和layout相關的,在layout過程中,會首先將recyclerview的子view全部detach下來,放到這個數組裏面,然後經過layout過程,再一個個的加回去,在LinearLayoutManager.onLayoutChildren中先調用detachAndScrapAttachedViews,然後再進行fill操作:
public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
上面很明顯就是把所有childView detach下來,具體的detach操作還是ChildViewHelper來進行的。
那爲什麼在layout之前進行detach操作呢?
我猜這是和recyclerview特性相關的,因爲recyclerview事先並不知道自己能有多少個childView,這不僅取決於childView的大小還取決於layoutmanager的具體佈局方式,也就是說measure和layout的過程是相關的,等到childview放滿了recyclerview就不再添加childView了。而一般的ViewGroup的chilgview個數都是確定的,childview個數和measure、layout完全無關,只要全部測量完,然後挨個layout就行。
由於以上的性質,recyclerview的measure和layout過程和一般的viewgroup都不一樣,recyclerview是先測量childview1,然後擺放childview1.接着測量childview2,然後擺放childview2.然後繼續,直到擺滿recyclerview。
在代碼中這樣體現:
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout.isAutoMeasureEnabled()) {
......
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
// set dimensions in 2nd step. Pre-layout should happen with old dimensions for
// consistency
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
}
可以看出在Recyclerview的onMeasure階段,就開始進行layout了。
- 二級緩存:mCachedViews
這個緩存中也是精確匹配的,也就是需要位置一樣才能複用,並且複用之後不用重新bind數據,因爲位置一樣,數據就是一樣的,不用重新bind,這個緩存的大小爲2,數據結構使用了arraylist。 - 三級緩存:mViewCacheExtension
這個屬於自定義緩存,一般不使用 - 四級緩存:mRecyclerPool
這個緩存中是根據type來複用的,每個type緩存5個holder,可以使用map+arraylist的數據結構緩存,因爲type爲int類型,所以使用SparseArray來作爲鍵值對查找,效率更高。所以是SparseArray+ArrayList結構:
static class ScrapData {
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
SparseArray<ScrapData> mScrap = new SparseArray<>();
基於上面的緩存說明,寫了一個例子來驗證:
這裏recyclerview寬高爲正方形的三倍,我們來計算在整個滑動過程中會創建幾個viewHolder,首先一屏之內最多可以出現12個item,因此先創建12個,當第四行出現,第一行消失時,先將第一行的三個viewholder回收,那麼這時緩存池中的情況是,mCachedViews=2,mRecyclerPool=1,因爲mCachedViews是精確匹配的,因此只有複用mRecyclerPool中的一個,所以第四行只能有一個複用,剩下兩個創建,因此總計創建14個ViewHolder,再滑動其實就不會創建viewholder了。上面的計算是沒有預加載的情況下得到的,默認Recyclerview是有預加載的,可以禁止,使用layoutmanager.setItemPrefetchEnabled(false)禁止預加載,預加載是爲了滑動更順滑,不過在這裏爲了簡化分析,所以禁止了預加載。
具體demo鏈接:
https://github.com/whoami-I/RecyclerViewExample