在之前的兩篇文章介紹了RV的繪制和滑動,留下了兩個方法沒有具體看,scrollByInternal()和
tryGetViewHolderForPositionByDeadline(),本文會補充這兩個方法的分析;
RV的四級緩存
Recycler類是RV復用機制的核心實現(xiàn)類,設計了四級緩存,優(yōu)先級順序是:Scrap、CacheView、ViewCacheExtension、RecycledViewPool
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;
}
-
mAttachedScrap:不參與滑動時的回收復用,只保存重新布局時從RecyclerView分離的item的無效、未移除、未更新的holder。因為RecyclerView在onLayout的時候,會先把children全部移除掉,再重新添加進入,mAttachedScrap臨時保存這些holder復用。 -
mChangedScrap:mChangedScrap和mAttachedScrap類似,不參與滑動時的回收復用,只是用作臨時保存的變量,它只會負責保存重新布局時發(fā)生變化的item的無效、未移除的holder,那么會重走adapter綁定數(shù)據(jù)的方法。 -
mCachedViews :用于保存最新被移除(remove)的ViewHolder,已經(jīng)和RecyclerView分離的視圖;它的作用是滾動的回收復用時如果需要新的ViewHolder時,精準匹配(根據(jù)position/id判斷)是不是原來被移除的那個item;如果是,則直接返回ViewHolder使用,不需要重新綁定數(shù)據(jù);如果不是則不返回,再去mRecyclerPool中找holder實例返回,并重新綁定數(shù)據(jù)。這一級的緩存是有容量限制的,最大數(shù)量為2。 -
mViewCacheExtension:RecyclerView給開發(fā)者預留的緩存池,開發(fā)者可以自己拓展回收池,一般不會用到,用RecyclerView系統(tǒng)自帶的已經(jīng)足夠了。 -
mRecyclerPool:是一個終極回收站,真正存放著被標識廢棄(其他池都不愿意回收)的ViewHolder的緩存池,如果上述mAttachedScrap、mChangedScrap、mCachedViews、mViewCacheExtension都找不到ViewHolder的情況下,就會從mRecyclerPool返回一個廢棄的ViewHolder實例,但是這里的ViewHolder是已經(jīng)被抹除數(shù)據(jù)的,沒有任何綁定的痕跡,需要重新綁定數(shù)據(jù)。它是根據(jù)itemType來存儲的,是以SparseArray嵌套一個ArraryList的形式保存ViewHolder的。
四級緩存:
Scrap:最輕量級的復用,包含
mAttachedScrap和mAttachedScrap兩個部分,Scrap不會參與滑動時的回收復用,作為重新布局的臨時緩存,當RV重新布局時,mAttachedScrap負責保存其中沒有改變的ViewHolder;剩下的由mChangedScrap負責保存,布局結束時,Scrap列表應該是空的,緩存的數(shù)據(jù)要么重新布局出來,要么被清空;CacheView:用于RV列表位置產(chǎn)生變動時,對剛剛移出屏幕的view進行回收復用。CacheView的最大容量是2,如果一直向一個方向滑動,緩存的Item超過兩個,就將CacheView中第一個item放入
RecycledViewPool中,再將新的放入CacheView,如果一直朝一個方向滾動,CacheView并沒有在效率上產(chǎn)生幫助,它只是把后面滑過的ViewHolder緩存起來,如果經(jīng)常來回滑動,那么從CacheView根據(jù)對應位置的item直接復用,不需要重新綁定數(shù)據(jù),將會得到很好的利用。ViewCacheExtension:自定義緩存,額外提供了一層緩存池給開發(fā)者,開發(fā)者視情況而定是否使用ViewCacheExtension增加一層緩存池;
RecycledViewPool:終極回收站,在Scrap、CacheView、ViewCacheExtension都不愿意回收的時候,都會丟到RecycledViewPool中回收;
public static class RecycledViewPool {
// mScrap 的大小
private static final int DEFAULT_MAX_SCRAP = 5;
static class ScrapData {
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
}
SparseArray<ScrapData> mScrap = new SparseArray<>();
}

RecycledViewPool的本質是一個SparseArray,SparseArray的value是ScrapData(存放VH的ArrayList),緩存池定義了默認的緩存大小DEFAULT_MAX_SCRAP = 5,這個數(shù)量不是說整個緩存池只能緩存這多個ViewHolder,而是不同itemType的ViewHolder的list的緩存數(shù)量,即mScrap的數(shù)量,說明最多只有5組不同類型的mScrapHeap。mMaxScrap = DEFAULT_MAX_SCRAP說明每種不同類型的ViewHolder默認保存5個,當然mMaxScrap的值是可以設置的。
SparseArray分析:避免裝箱,節(jié)省空間,不需要hash映射
其實,Scrap緩存池不參與滾動的回收復用,CacheView緩存池被稱為一級緩存,又因為ViewCacheExtension緩存池是給開發(fā)者定義的緩存池,一般不用到,所以RecycledViewPool緩存池被稱為二級緩存,那么這樣來說實際只有兩層緩存。
緩存復用
在RV滑動中,分析了onTouchEvent(),在Action_MOVE的時候會調用scrollByInternal()去滑動
boolean scrollByInternal(int x, int y, MotionEvent ev) {
...
if (x != 0) {
consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
unconsumedX = x - consumedX;
}
if (y != 0) {
consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
unconsumedY = y - consumedY;
}
...
}
ScrollByInternal()會將滑動操作托付給LayoutManager,LayoutManager會調用自己的fill()處理,這個方法源碼注釋描述為The magic functions
- 在繪制時
step2()中調用LayoutManger的onLayoutChildren()進行child的measure和layout; - 滑動時,會調用它去進行回收和復用;
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
...
recycleByLayoutState(recycler, layoutState);
layoutChunk(recycler, state, layoutState, layoutChunkResult);
...
}

layoutChunk():
這個方法在繪制篇已經(jīng)分析過,在繪制時step2()中調用LayoutManger的onLayoutChildren()

上面是關于繪制時候的分析,下面補充一下復用的邏輯
next();
注意:新版本中,tryGetViewHolderForPositionByDeadline() 改名為 getViewForPosition()
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
}
View getViewForPosition(int position, boolean dryRun) {
...
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrap = holder != null;
}
// 1) Find from scrap by position
if (holder == null) {
holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);
// 2) Find from scrap via stable ids, if exists
if (mAdapter.hasStableIds()) {
holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
// 自定義緩存獲取
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
// 調用adapter去創(chuàng)建新的ViewHolder
holder = mAdapter.createViewHolder(RecyclerView.this, type);
...
}

至此,RV的復用機制已經(jīng)分析的很清楚了,下面看回收機制
recycleByLayoutState()回收的入口方法:

recycleByLayoutState()的代碼就不貼了,最后它調用了回收的核心方法:
recyclerViewHolderInternal():
void recycleViewHolderInternal(ViewHolder holder) {
...
if (cachedViewSize < mViewCacheMax) {
mCachedViews.add(holder);
cached = true;
}
}
if (!cached) {
addViewHolderToRecycledViewPool(holder);
recycled = true;
}
...
}
void addViewHolderToRecycledViewPool(ViewHolder holder) {
ViewCompat.setAccessibilityDelegate(holder.itemView, null);
dispatchViewRecycled(holder);
holder.mOwnerRecyclerView = null;
getRecycledViewPool().putRecycledView(holder);
}
上面的代碼我們可以看到在RV滑動的時候,真正回收的只有兩級緩存,mCachedViews 和 RecycledViewPool;

至此,RV的兩級回收和復用已經(jīng)分析完畢,下面看一下第一級緩存的回收工作, 第一級緩存Scrap的回收是在繪制的時候工作的;
Scarp回收和復用
在繪制的時候,Step2除了調用fill()去對chail進行測量和布局,在此之前還計算了錨點和調用了detachAndScrapAttachedViews(),這些操作都是由LayoutManager完成的,RV28.0把step1,step2,step3移除了,但是這些實現(xiàn)的思路任然是一樣的;
public void detachAndScrapAttachedViews(Recycler recycler) {
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
...
if (viewHolder.isInvalid() && !viewHolder.isRemoved() && !viewHolder.isChanged() &&
!mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
detachViewAt(index);
recycler.scrapView(view);
}
}
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
holder.setScrapContainer(this);
if (!holder.isChanged() || !supportsChangeAnimations()) {
if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
throw new IllegalArgumentException("Called scrap view with an invalid view."
+ " Invalid views cannot be reused from scrap, they should rebound from"
+ " recycler pool.");
}
mAttachedScrap.add(holder);
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
mChangedScrap.add(holder);
}
}
在這里我們看到了Scrap緩存的核心回收方法scrapView(),通過viewHolder的狀態(tài),判斷當前是在mAttachedScrap回收,還是在mChangedScrap回收
復用的話也是在上文的核心復用方法里面老版本是tryGetViewHolderForPositionByDeadline(),新版本是getViewForPosition()