【Android進階】RecyclerView之快取(二)
上一篇,說了 ItemDecoration
,這一篇,我們來說說 RecyclerView
的回收複用邏輯。
【Android進階】RecyclerView之ItemDecoration(一)
問題
假如有100個 item
,首屏最多展示2個半(一屏同時最多展示4個), RecyclerView
滑動時,會建立多少個 viewholder
?
先別急著回答,我們寫個 demo 看看
首先,是 item
的佈局
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:id="@+id/tv_repeat" android:layout_width="match_parent" android:layout_height="200dp" android:gravity="center" /> <TextView android:layout_width="match_parent" android:layout_height="2dp" android:background="@color/colorAccent" /> </LinearLayout> 複製程式碼
然後是 RepeatAdapter
,這裡使用的是原生的 Adapter
public class RepeatAdapter extends RecyclerView.Adapter<RepeatAdapter.RepeatViewHolder> { private List<String> list; private Context context; public RepeatAdapter(List<String> list, Context context) { this.list = list; this.context = context; } @NonNull @Override public RepeatViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { View view = LayoutInflater.from(context).inflate(R.layout.item_repeat, viewGroup, false); Log.e("cheng", "onCreateViewHolderviewType=" + i); return new RepeatViewHolder(view); } @Override public void onBindViewHolder(@NonNull RepeatViewHolder viewHolder, int i) { viewHolder.tv_repeat.setText(list.get(i)); Log.e("cheng", "onBindViewHolderposition=" + i); } @Override public int getItemCount() { return list.size(); } class RepeatViewHolder extends RecyclerView.ViewHolder { public TextView tv_repeat; public RepeatViewHolder(@NonNull View itemView) { super(itemView); this.tv_repeat = (TextView) itemView.findViewById(R.id.tv_repeat); } } } 複製程式碼
在 Activity
中使用
List<String> list = new ArrayList<>(); for (int i = 0; i < 100; i++) { list.add("第" + i + "個item"); } RepeatAdapter repeatAdapter = new RepeatAdapter(list, this); rvRepeat.setLayoutManager(new LinearLayoutManager(this)); rvRepeat.setAdapter(repeatAdapter); 複製程式碼
當我們滑動時,log如下:

onCreateViewHolder
,也就是說,總共100個item,只建立了7個
viewholder
(篇幅問題,沒有截到100,有興趣的同學可以自己試試)
WHY?
通過閱讀原始碼,我們發現, RecyclerView
的快取單位是 viewholder
,而獲取 viewholder
最終呼叫的方法是 Recycler#tryGetViewHolderForPositionByDeadline
原始碼如下:
@Nullable RecyclerView.ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) { ...省略程式碼... holder = this.getChangedScrapViewForPosition(position); ...省略程式碼... if (holder == null) { holder = this.getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); } ...省略程式碼... if (holder == null) { View view = this.mViewCacheExtension.getViewForPositionAndType(this, position, type); if (view != null) { holder = RecyclerView.this.getChildViewHolder(view); } } ...省略程式碼... if (holder == null) { holder = this.getRecycledViewPool().getRecycledView(type); } ...省略程式碼... if (holder == null) { holder = RecyclerView.this.mAdapter.createViewHolder(RecyclerView.this, type); } ...省略程式碼... } 複製程式碼
從上到下,依次是 mChangedScrap
、 mAttachedScrap
、 mCachedViews
、 mViewCacheExtension
、 mRecyclerPool
最後才是 createViewHolder
-
mChangedScrap
完整原始碼如下:
if (RecyclerView.this.mState.isPreLayout()) { holder = this.getChangedScrapViewForPosition(position); fromScrapOrHiddenOrCache = holder != null; } 複製程式碼
由於 isPreLayout
方法取決於 mInPreLayout
,而 mInPreLayout
預設為 false
,即mChangedScrap不參與回收複用邏輯。
-
mAttachedScrap
完整原始碼如下:
RecyclerView.ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) { int scrapCount = this.mAttachedScrap.size(); int cacheSize; RecyclerView.ViewHolder vh; for(cacheSize = 0; cacheSize < scrapCount; ++cacheSize) { vh = (RecyclerView.ViewHolder)this.mAttachedScrap.get(cacheSize); if (!vh.wasReturnedFromScrap() && vh.getLayoutPosition() == position && !vh.isInvalid() && (RecyclerView.this.mState.mInPreLayout || !vh.isRemoved())) { vh.addFlags(32); return vh; } } } 複製程式碼
這段程式碼什麼時候會生效呢,那得找找什麼時候將 viewholder
新增到 mAttachedScrap
的 我們在原始碼中全域性搜尋 mAttachedScrap.add
,發現是 Recycler#scrapView()
方法
void scrapView(View view) { ...省略程式碼... this.mAttachedScrap.add(holder); ...省略程式碼... } 複製程式碼
什麼時候呼叫 scrapView()
方法呢? 繼續全域性搜尋,發現最終是 Recycler#detachAndScrapAttachedViews()
方法,這個方法又是什麼時候會被呼叫的呢? 答案是 LayoutManager#onLayoutChildren()
。我們知道 onLayoutChildren
負責item的佈局工作(這部分後面再說),所以,mAttachedScrap應該存放是當前螢幕上顯示的 viewhoder
,我們來看下 detachAndScrapAttachedViews
的原始碼
public void detachAndScrapAttachedViews(@NonNull RecyclerView.Recycler recycler) { int childCount = this.getChildCount(); for(int i = childCount - 1; i >= 0; --i) { View v = this.getChildAt(i); this.scrapOrRecycleView(recycler, i, v); } } 複製程式碼
其中, childCount
即為螢幕上顯示的item數量。那同學們就要問了, mAttachedScrap
有啥用? 答案當時是有用的,比如說, 拖動排序 ,比如說第1個item和第2個item 互換,這個時候,mAttachedScrap就派上了用場,直接從這裡拿 viewholder
,都不用經過 onCreateViewHolder
和 onBindViewHolder
。
-
mCachedViews
完整程式碼如下:
cacheSize = this.mCachedViews.size(); for(int i = 0; i < cacheSize; ++i) { RecyclerView.ViewHolder holder = (RecyclerView.ViewHolder)this.mCachedViews.get(i); if (!holder.isInvalid() && holder.getLayoutPosition() == position) { if (!dryRun) { this.mCachedViews.remove(i); } return holder; } } 複製程式碼
我們先來找找 viewholder
是在什麼時候新增進 mCachedViews
?是在 Recycler#recycleViewHolderInternal()
方法
void recycleViewHolderInternal(RecyclerView.ViewHolder holder) { if (!holder.isScrap() && holder.itemView.getParent() == null) { if (holder.isTmpDetached()) { throw new IllegalArgumentException("Tmp detached view should be removed from RecyclerView before it can be recycled: " + holder + RecyclerView.this.exceptionLabel()); } else if (holder.shouldIgnore()) { throw new IllegalArgumentException("Trying to recycle an ignored view holder. You should first call stopIgnoringView(view) before calling recycle." + RecyclerView.this.exceptionLabel()); } else { boolean transientStatePreventsRecycling = holder.doesTransientStatePreventRecycling(); boolean forceRecycle = RecyclerView.this.mAdapter != null && transientStatePreventsRecycling && RecyclerView.this.mAdapter.onFailedToRecycleView(holder); boolean cached = false; boolean recycled = false; if (forceRecycle || holder.isRecyclable()) { if (this.mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(526)) { int cachedViewSize = this.mCachedViews.size(); if (cachedViewSize >= this.mViewCacheMax && cachedViewSize > 0) { this.recycleCachedViewAt(0); --cachedViewSize; } int targetCacheIndex = cachedViewSize; if (RecyclerView.ALLOW_THREAD_GAP_WORK && cachedViewSize > 0 && !RecyclerView.this.mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) { int cacheIndex; for(cacheIndex = cachedViewSize - 1; cacheIndex >= 0; --cacheIndex) { int cachedPos = ((RecyclerView.ViewHolder)this.mCachedViews.get(cacheIndex)).mPosition; if (!RecyclerView.this.mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) { break; } } targetCacheIndex = cacheIndex + 1; } this.mCachedViews.add(targetCacheIndex, holder); cached = true; } if (!cached) { this.addViewHolderToRecycledViewPool(holder, true); recycled = true; } } RecyclerView.this.mViewInfoStore.removeViewHolder(holder); if (!cached && !recycled && transientStatePreventsRecycling) { holder.mOwnerRecyclerView = null; } } } else { throw new IllegalArgumentException("Scrapped or attached views may not be recycled. isScrap:" + holder.isScrap() + " isAttached:" + (holder.itemView.getParent() != null) + RecyclerView.this.exceptionLabel()); } } 複製程式碼
最上層是 RecyclerView#removeAndRecycleViewAt
方法
public void removeAndRecycleViewAt(int index, @NonNull RecyclerView.Recycler recycler) { View view = this.getChildAt(index); this.removeViewAt(index); recycler.recycleView(view); } 複製程式碼
這個方法是在哪裡呼叫的呢?答案是 LayoutManager
,我們寫個demo效果看著比較直觀 定義 MyLayoutManager
,並重寫 removeAndRecycleViewAt
,然後新增log
class MyLayoutManager extends LinearLayoutManager { public MyLayoutManager(Context context) { super(context); } @Override public void removeAndRecycleViewAt(int index, @NonNull RecyclerView.Recycler recycler) { super.removeAndRecycleViewAt(index, recycler); Log.e("cheng", "removeAndRecycleViewAt index=" + index); } } 複製程式碼
將其設定給 RecyclerView
,然後滑動,檢視日誌輸出情況


removeAndRecycleViewAt()
方法,需要注意的是,此
index
表示的是該
item
在
chlid
中的下標,也就是在當前螢幕中的下標,而不是在
RecyclerView
的。
事實是不是這樣的呢?讓我們來看看原始碼,以 LinearLayoutManager
為例,預設是垂直滑動的,此時控制其滑動距離的方法是 scrollVerticallyBy()
,其呼叫的是 scrollBy()
方法
int scrollBy(int dy, Recycler recycler, State state) { if (this.getChildCount() != 0 && dy != 0) { this.mLayoutState.mRecycle = true; this.ensureLayoutState(); int layoutDirection = dy > 0 ? 1 : -1; int absDy = Math.abs(dy); this.updateLayoutState(layoutDirection, absDy, true, state); int consumed = this.mLayoutState.mScrollingOffset + this.fill(recycler, this.mLayoutState, state, false); if (consumed < 0) { return 0; } else { int scrolled = absDy > consumed ? layoutDirection * consumed : dy; this.mOrientationHelper.offsetChildren(-scrolled); this.mLayoutState.mLastScrollDelta = scrolled; return scrolled; } } else { return 0; } } 複製程式碼
關鍵程式碼是 fill()
方法中的 recycleByLayoutState()
,判斷滑動方向,從第一個還是最後一個開始回收。
private void recycleByLayoutState(Recycler recycler, LinearLayoutManager.LayoutState layoutState) { if (layoutState.mRecycle && !layoutState.mInfinite) { if (layoutState.mLayoutDirection == -1) { this.recycleViewsFromEnd(recycler, layoutState.mScrollingOffset); } else { this.recycleViewsFromStart(recycler, layoutState.mScrollingOffset); } } } 複製程式碼
扯的有些遠了,讓我們回顧下 recycleViewHolderInternal()
方法,當 cachedViewSize >= this.mViewCacheMax
時,會移除第1個,也就是最先加入的 viewholder
, mViewCacheMax
是多少呢?
public Recycler() { this.mUnmodifiableAttachedScrap = Collections.unmodifiableList(this.mAttachedScrap); this.mRequestedCacheMax = 2; this.mViewCacheMax = 2; } 複製程式碼
mViewCacheMax
為2,也就是mCachedViews的初始化大小為2,超過這個大小時, viewholer
將會被移除,放到哪裡去了呢?帶著這個疑問我們繼續往下看
-
mViewCacheExtension
ViewCacheExtension
這個類需要使用者通過setViewCacheExtension()
方法傳入,RecyclerView
自身並不會實現它,一般正常的使用也用不到。 -
mRecyclerPool
我們帶著之前的疑問,繼續看原始碼,之前提到mCachedViews初始大小為2,超過這個大小,最先放入的會被移除,移除的viewholder
到哪裡去了呢?我們來看recycleCachedViewAt()
方法的原始碼
void recycleCachedViewAt(int cachedViewIndex) { RecyclerView.ViewHolder viewHolder = (RecyclerView.ViewHolder)this.mCachedViews.get(cachedViewIndex); this.addViewHolderToRecycledViewPool(viewHolder, true); this.mCachedViews.remove(cachedViewIndex); } 複製程式碼
addViewHolderToRecycledViewPool()
方法
void addViewHolderToRecycledViewPool(@NonNull RecyclerView.ViewHolder holder, boolean dispatchRecycled) { RecyclerView.clearNestedRecyclerViewIfNotNested(holder); if (holder.hasAnyOfTheFlags(16384)) { holder.setFlags(0, 16384); ViewCompat.setAccessibilityDelegate(holder.itemView, (AccessibilityDelegateCompat)null); } if (dispatchRecycled) { this.dispatchViewRecycled(holder); } holder.mOwnerRecyclerView = null; this.getRecycledViewPool().putRecycledView(holder); } 複製程式碼
我們繼續看看 RecycledViewPool
的原始碼
public static class RecycledViewPool { private static final int DEFAULT_MAX_SCRAP = 5; SparseArray<RecyclerView.RecycledViewPool.ScrapData> mScrap = new SparseArray(); private int mAttachCount = 0; public RecycledViewPool() { } ...省略程式碼... } 複製程式碼
static class ScrapData { final ArrayList<RecyclerView.ViewHolder> mScrapHeap = new ArrayList(); int mMaxScrap = 5; long mCreateRunningAverageNs = 0L; long mBindRunningAverageNs = 0L; ScrapData() { } } 複製程式碼
可以看到,其內部有一個 SparseArray
用來存放 viewholder
。
總結
RecycledView
總共有 mAttachedScrap
、 mCachedViews
、 mViewCacheExtension
、 mRecyclerPool
4級快取,其中 mAttachedScrap
只儲存佈局時,螢幕上顯示的 viewholder
,一般不參與回收、複用( 拖動排序時會參與 ); mCachedViews
主要儲存剛移除螢幕的 viewholder
,初始大小為2; mViewCacheExtension
為預留的快取池,需要自己去實現; mRecyclerPool
則是最後一級快取,當 mCachedViews
滿了之後, viewholder
會被存放在 mRecyclerPool
,繼續複用。其中 mAttachedScrap
、 mCachedViews
為精確匹配,即為對應 position
的 viewholder
才會被複用, mRecyclerPool
為模糊匹配,只匹配 viewType
,所以複用時,需要呼叫 onBindViewHolder
為其設定新的資料。