這是RecyclerView
緩存機制系列文章的第三篇,系列文章的目錄以下:android
若是想直接看結論能夠移步到第四篇末尾(你會後悔的,過程更加精彩)。緩存
上一篇以列表滑動事件爲起點沿着調用鏈一直往下尋找,驗證了「滑出屏幕的表項」會被回收。那它們被回收去哪裏了?沿着上一篇的調用鏈繼續往下探究:bash
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()
:app
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()
回收表項:ide
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
。post
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緩存機制(咋複用?)。fetch
還記得RecyclerView緩存機制(咋複用?)中得出的結論嗎?這裏再引用一下:ui
- 雖然爲了獲取
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
不須要從新綁定數據