這是RecyclerView
快取機制系列文章的第三篇,系列文章的目錄如下:
如果想直接看結論可以移步到第四篇末尾(你會後悔的,過程更加精彩)。
回收入口
上一篇以列表滑動事件為起點沿著呼叫鏈一直往下尋找,驗證了“滑出螢幕的表項”會被回收。那它們被回收去哪裡了?沿著上一篇的呼叫鏈繼續往下探究:
public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
...
/**
* Recycles views that went out of bounds after scrolling towards the end of the layout.
* 當向列表尾部滾動時回收滾出螢幕的表項
* <p>
* Checks both layout position and visible position to guarantee that the view is not visible.
*
* @param recycler Recycler instance of {@link android.support.v7.widget.RecyclerView}
* @param dt This can be used to add additional padding to the visible area. This is used
* to detect children that will go out of bounds after scrolling, without
* actually moving them.(該引數被用於檢測滾出螢幕的表項)
*/
private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) {
...
// ignore padding, ViewGroup may not clip children.
final int limit = dt;
final int childCount = getChildCount();
if (mShouldReverseLayout) {
for (int i = childCount - 1; i >= 0; i--) {
View child = getChildAt(i);
if (mOrientationHelper.getDecoratedEnd(child) > limit
|| mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
// stop here
recycleChildren(recycler, childCount - 1, i);
return;
}
}
} else {
//遍歷LinearLayoutManager的孩子找出其中應該被回收的
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
//直到表項底部縱座標大於某個值後,回收該表項以上的所有表項
if (mOrientationHelper.getDecoratedEnd(child) > limit
|| mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
// stop here
//回收索引為0到i-1的表項
recycleChildren(recycler, 0, i);
return;
}
}
}
}
...
}
複製程式碼
recycleViewsFromStart()
通過遍歷找到滑出螢幕的表項,然後呼叫了recycleChildren()
回收他們:
public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
/**
* Recycles children between given indices.
* 回收孩子
*
* @param startIndex inclusive
* @param endIndex exclusive
*/
private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
if (startIndex == endIndex) {
return;
}
if (DEBUG) {
Log.d(TAG, "Recycling " + Math.abs(startIndex - endIndex) + " items");
}
if (endIndex > startIndex) {
for (int i = endIndex - 1; i >= startIndex; i--) {
removeAndRecycleViewAt(i, recycler);
}
} else {
for (int i = startIndex; i > endIndex; i--) {
removeAndRecycleViewAt(i, recycler);
}
}
}
}
複製程式碼
最終呼叫了父類LayoutManager.removeAndRecycleViewAt()
:
public abstract static class LayoutManager {
/**
* Remove a child view and recycle it using the given Recycler.
*
* @param index Index of child to remove and recycle
* @param recycler Recycler to use to recycle child
*/
public void removeAndRecycleViewAt(int index, Recycler recycler) {
final View view = getChildAt(index);
removeViewAt(index);
recycler.recycleView(view);
}
}
複製程式碼
先從LayoutManager
中刪除表項,然後呼叫Recycler.recycleView()
回收表項:
public final class Recycler {
/**
* Recycle a detached view. The specified view will be added to a pool of views
* for later rebinding and reuse.
*
* <p>A view must be fully detached (removed from parent) before it may be recycled. If the
* View is scrapped, it will be removed from scrap list.</p>
*
* @param view Removed view for recycling
* @see LayoutManager#removeAndRecycleView(View, Recycler)
*/
public void recycleView(View view) {
// This public recycle method tries to make view recycle-able since layout manager
// intended to recycle this view (e.g. even if it is in scrap or change cache)
ViewHolder holder = getChildViewHolderInt(view);
if (holder.isTmpDetached()) {
removeDetachedView(view, false);
}
if (holder.isScrap()) {
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
}
}
複製程式碼
通過表項檢視拿到了對應ViewHolder
,然後把其傳入Recycler.recycleViewHolderInternal()
,現在就可以更準地回答上一篇的那個問題“回收些啥?”:回收的是滑出螢幕表項對應的ViewHolder
。
public final class Recycler {
...
int mViewCacheMax = DEFAULT_CACHE_SIZE;
static final int DEFAULT_CACHE_SIZE = 2;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
...
/**
* internal implementation checks if view is scrapped or attached and throws an exception
* if so.
* Public version un-scraps before calling recycle.
*/
void recycleViewHolderInternal(ViewHolder holder) {
...
if (forceRecycle || holder.isRecyclable()) {
//先存在mCachedViews裡面
//這裡的判斷條件決定了複用mViewCacheMax中的ViewHolder時不需要重新繫結資料
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
// Retire oldest cached view
//如果mCachedViews大小超限了,則刪掉最老的被快取的ViewHolder
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize--;
}
int targetCacheIndex = cachedViewSize;
if (ALLOW_THREAD_GAP_WORK
&& cachedViewSize > 0
&& !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
// when adding the view, skip past most recently prefetched views
int cacheIndex = cachedViewSize - 1;
while (cacheIndex >= 0) {
int cachedPos = mCachedViews.get(cacheIndex).mPosition;
if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
break;
}
cacheIndex--;
}
targetCacheIndex = cacheIndex + 1;
}
//ViewHolder加到快取中
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
//若ViewHolder沒有入快取則存入回收池
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
} else {
...
}
...
}
複製程式碼
- 通過
cached
這個布林值,實現互斥,即ViewHolder
要麼存入mCachedViews
,要麼存入pool
mCachedViews
有大小限制,預設只能存2個ViewHolder
,當第三個ViewHolder
存入時會把第一個移除掉,程式碼如下:
public final class Recycler {
...
void recycleCachedViewAt(int cachedViewIndex) {
if (DEBUG) {
Log.d(TAG, "Recycling cached view at index " + cachedViewIndex);
}
ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
if (DEBUG) {
Log.d(TAG, "CachedViewHolder to be recycled: " + viewHolder);
}
//將ViewHolder加入到回收池
addViewHolderToRecycledViewPool(viewHolder, true);
//將ViewHolder從cache中移除
mCachedViews.remove(cachedViewIndex);
}
...
}
複製程式碼
從mCachedViews
移除掉的ViewHolder
會加入到回收池中。 mCachedViews
有點像“回收池預備佇列”,即總是先回收到mCachedViews
,當它放不下的時候,按照先進先出原則將最先進入的ViewHolder
存入回收池 :
public final class Recycler {
/**
* Prepares the ViewHolder to be removed/recycled, and inserts it into the RecycledViewPool.
* 將viewHolder存入回收池
*
* Pass false to dispatchRecycled for views that have not been bound.
*
* @param holder Holder to be added to the pool.
* @param dispatchRecycled True to dispatch View recycled callbacks.
*/
void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
clearNestedRecyclerViewIfNotNested(holder);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE)) {
holder.setFlags(0, ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE);
ViewCompat.setAccessibilityDelegate(holder.itemView, null);
}
if (dispatchRecycled) {
dispatchViewRecycled(holder);
}
holder.mOwnerRecyclerView = null;
getRecycledViewPool().putRecycledView(holder);
}
}
public static class RecycledViewPool {
static class ScrapData {
ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
//每種型別的ViewHolder最多存5個
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
//以viewType為鍵,ScrapData為值,作為回收池中ViewHolder的容器
SparseArray<ScrapData> mScrap = new SparseArray<>();
//ViewHolder入池 按viewType分類入池,相同的ViewType存放在List中
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");
}
//入回收池之前重置ViewHolder
scrap.resetInternal();
scrapHeap.add(scrap);
}
}
複製程式碼
ViewHolder
會按viewType
分類存入回收池,最終儲存在ScrapData
的ArrayList
中,回收池資料結構分析詳見RecyclerView快取機制(咋複用?)。
快取優先順序
還記得RecyclerView快取機制(咋複用?)中得出的結論嗎?這裡再引用一下:
- 雖然為了獲取
ViewHolder
做了5次嘗試(共從6個地方獲取),先排除3種特殊情況,即從mChangedScrap
獲取、通過id獲取、從自定義快取獲取,正常流程中只剩下3種獲取方式,優先順序從高到低依次是:
- 從
mAttachedScrap
獲取- 從
mCachedViews
獲取- 從
mRecyclerPool
獲取
- 這樣的快取優先順序是不是意味著,對應的複用效能也是從高到低?(複用效能越好意味著所做的昂貴操作越少)
- 最壞情況:重新建立
ViewHodler
並重新繫結資料- 次好情況:複用
ViewHolder
但重新繫結資料- 最好情況:複用
ViewHolder
且不重新繫結資料
當時分析了mAttachedScrap
和mRecyclerPool
的複用效能,即 從mRecyclerPool
中複用的ViewHolder
需要重新繫結資料,從mAttachedScrap
中複用的ViewHolder
不需要重新建立也不需要重新繫結資料
把存入mCachedViews
的程式碼和複用時繫結資料的程式碼結合起來看一下:
void recycleViewHolderInternal(ViewHolder holder) {
...
//滿足這個條件才能存入mCachedViews
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
}
...
}
ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
...
//滿足這個條件就需要重新繫結資料
if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()){
}
...
複製程式碼
重新繫結資料的三個條件中,holder.needsUpdate()
和holder.isInvalid()
都是false
時才能存入mCachedViews
,而!holder.isBound()
對於mCachedViews
中的ViewHolder
來說必然為false
,因為只有當呼叫ViewHolder.resetInternal()
重置ViewHolder
後,才會將其設定為未繫結狀態,而只有存入回收池時才會重置ViewHolder
。所以 從mCachedViews
中複用的ViewHolder
不需要重新繫結資料
總結
- 滑出螢幕表項對應的
ViewHolder
會被回收到mCachedViews
+mRecyclerPool
結構中,mCachedViews
是ArrayList
,預設儲存最多2個ViewHolder
,當它放不下的時候,按照先進先出原則將最先進入的ViewHolder
存入回收池的方式來騰出空間。mRecyclerPool
是SparseArray
,它會按viewType
分類儲存ViewHolder
,預設每種型別最多存5個。 - 從
mRecyclerPool
中複用的ViewHolder
需要重新繫結資料 - 從
mCachedViews
中複用的ViewHolder
不需要重新繫結資料