RecyclerView緩存介紹

轉載請以連接形式標明出處: 本文出自:103style的博客android

源碼 base on androidx.recyclerview:recyclerview:1.1.0-alpha05git

代碼說明中已省略了暫不須要關注的代碼.github

測試demo地址緩存

更新於:2019/10/29 15.04 以前因爲用Android P模擬器測試因此一直顯示 mAttachedScrap 中的數據一直爲空。真機顯示正常,爲屏幕內當前顯示item的條數。bash


目錄

  • 獲取 RecyclerView 子項的流程
  • 緩存相關的數據變量的介紹
  • 獲取緩存的邏輯介紹
  • 經過demo測試緩存
  • 小結

獲取 RecyclerView 子項的流程

獲取的流程大體爲: RecyclerView.onLayout(...)LayoutManager.onLayoutChildren(...)LayoutState.next(...)ide

源代碼調用以下:佈局

RecyclerViewonLayout(...)方法:測試

protected void onLayout(boolean changed, int l, int t, int r, int b) {
    dispatchLayout();
}
void dispatchLayout() {
        dispatchLayoutStep2();
}
private void dispatchLayoutStep2() {
    mLayout.onLayoutChildren(mRecycler, mState);
}
複製代碼

RecyclerView 設置的 LayoutManageronLayoutChildren(...)方法: 以 LinearLayoutManager 爲例:動畫

public void onLayoutChildren(...) {
        fill(recycler, mLayoutState, state, false);
}
int fill(...) {
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
}
void layoutChunk(...) {
    View view = layoutState.next(recycler);
}
List<RecyclerView.ViewHolder> mScrapList;
static class LayoutState {
    View next(RecyclerView.Recycler recycler) {
        if (mScrapList != null) {
            return nextViewFromScrapList();
        }
        final View view = recycler.getViewForPosition(mCurrentPosition);
        mCurrentPosition += mItemDirection;
        return view;
    }
}
複製代碼

這裏主要看 recycler.getViewForPosition(mCurrentPosition)mScrapList 是有動畫效果時預佈局保存的ViewHolderui


緩存相關的數據變量的介紹

Recycler中的 mAttachedScrap、mChangedScrap、mCachedViews

ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
static final int DEFAULT_CACHE_SIZE = 2;
複製代碼

mAttachedScrap:用於緩存顯示在屏幕上並符合對應規則的 ViewHolder,模擬器的話會出現 裏面的數據時空的 。 mChangedScrap:用於緩存顯示在屏幕上不符合 mAttachedScrap 規則的 ViewHolder

Recycler.scrapView(View view), 在 調用 attchViewdetachView的時候調用。

void scrapView(View view) {
    final ViewHolder holder = getChildViewHolderInt(view);
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
            || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
        mAttachedScrap.add(holder);
    } else {
        if (mChangedScrap == null) {
            mChangedScrap = new ArrayList<ViewHolder>();
        }
        mChangedScrap.add(holder);
    }
}
複製代碼

mCachedViews:保存滑動過程當中從可見到不可見的ViewHolder,大小爲DEFAULT_CACHE_SIZE = 2,而且本來數據信息都在,因此能夠直接添加到 RecyclerView 中顯示,不須要再次從新 onBindViewHolder(),在數據多時滑動過程當中會變成 3

private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback =
        new ViewInfoStore.ProcessCallback() {
            @Override
            public void unused(ViewHolder viewHolder) {
                mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler);
            }
        };
void process(ProcessCallback callback) {
    for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
        final RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
        final InfoRecord record = mLayoutHolderMap.removeAt(index);
        if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
            //從可見變成不可見
            callback.unused(viewHolder);
        } else if ((record.flags & FLAG_DISAPPEARED) != 0) {
            if (record.preInfo == null) {
                // 相似出現消失但發生在不一樣的佈局通道之間。 
                // 當佈局管理器使用自動測量時,可能會發生這種狀況
                callback.unused(viewHolder);
            }
        }
    }
}
複製代碼

ChildHelper.mHiddenViews

mHiddenViews 中保留在添加或刪除數據或其餘操做,保存在屏幕內可見的 帶動畫子視圖

List<View> mHiddenViews = new ArrayList<View>();
複製代碼

mHiddenViews 是在 RecycleViewaddAnimatingView(ViewHolder viewHolder)的時候,根據特定狀況添加的。

private void addAnimatingView(ViewHolder viewHolder) {
    final boolean alreadyParented = view.getParent() == this;
    if (viewHolder.isTmpDetached()) {
        mChildHelper.attachViewToParent(view, -1, view.getLayoutParams(), true);
    } else if (!alreadyParented) {
        mChildHelper.addView(view, true);
    } else {
        mChildHelper.hide(view);
    }
}
複製代碼

RecycledViewPool.mScrap

每一個 viewType 的緩存最可能是 DEFAULT_MAX_SCRAP = 5 個,viewTypeRecyclerView子view 的樣式,即 adapter 中的 getItemViewType獲取的值。

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<>();
複製代碼

添加和獲取的方法:

  • 咱們能夠看到每獲取一個值,則會從緩存中刪掉。取數據即從緩存中取數據時調用。
  • 添加的時候,當數量超過DEFAULT_MAX_SCRAP = 5時,則丟棄。 存數據即爲 釋放 mCachedViews 的數據則會放入緩存池。
public ViewHolder getRecycledView(int viewType) {
    final ScrapData scrapData = mScrap.get(viewType);
    if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
        final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
        for (int i = scrapHeap.size() - 1; i >= 0; i--) {
            if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
                return scrapHeap.remove(i);
            }
        }
    }
    return null;
}
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;
    }
    scrapHeap.add(scrap);
}
複製代碼

獲取緩存的邏輯介紹

RecyclerView.RecyclergetViewForPosition(int position)方法:

public View getViewForPosition(int position) {
    return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
    return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
複製代碼

RecyclerView.RecyclertryGetViewHolderForPositionByDeadline(...) 方法獲取 holder也就是對應的緩存機制的主要邏輯,找到即返回

  • 首先判斷mState.isPreLayout(),是否是執行動畫的預佈局,是的話則嘗試從 mChangedScrap 中獲取。
  • 而後嘗試從mAttachedScrapmHiddenViewsmCachedViews獲取。
  • 而後根據 mAdapter.hasStableIds() 以及 mViewCacheExtension!=null 判斷是否從用戶設置的相關邏輯中獲取。
  • 而後再嘗試從緩存池 RecycledViewPool 中的 mScrapScrapData 中獲取。
  • 還沒找到則經過 mAdapter.createViewHolder 從新建立。
ViewHolder tryGetViewHolderForPositionByDeadline(...) {
    ...
    if (mState.isPreLayout()) {
        //若是是預佈局,即執行簡單的動畫 就會嘗試這裏
        holder = getChangedScrapViewForPosition(position);
    }
    // 從 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);
        //經過 adapter.setHasStableIds設置 默認爲false
        if (mAdapter.hasStableIds()) {}
        //mViewCacheExtension 經過 RecyclerView.setViewCacheExtension設置 默認爲null
        if (holder == null && mViewCacheExtension != null) {
            //經過自定義的緩存機制區獲取
        }
        if (holder == null) {
            // 嘗試從RecycledViewPool中去獲取holder
            holder = getRecycledViewPool().getRecycledView(type);
        }
        if (holder == null) {
            //調用Adapter.createViewHolder建立holder
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
        }
    }
    ...
    return holder;
}
複製代碼

若是是預佈局,則嘗試從 mChangedScrap 中獲取

ViewHolder getChangedScrapViewForPosition(int position) {
    // 若是是預佈局,請檢查 mChangedScrap 是否徹底匹配。
    for (int i = 0; i < mChangedScrap.size(); i++) {
        final ViewHolder holder = mChangedScrap.get(i);
        if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
            holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
            return holder;
        }
    }
    // 默認 false
    if (mAdapter.hasStableIds()) {
        ...
    }
    return null;
}
複製代碼

嘗試從緩存中 mAttachedScrap、mHiddenViews、mCachedViews獲取RecyclerView.RecyclergetScrapOrHiddenOrCachedHolderForPosition(...) 獲取 holder.

ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
    final int scrapCount = mAttachedScrap.size();

    // 嘗試從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;
        }
    }
    //dryRun: getViewForPosition(position, false)傳入的是false
    if (!dryRun) {
        View view = mChildHelper.findHiddenNonRemovedView(position);
        if (view != null) {
        ...
            return vh;
        }
    }
    // 從一級緩存中嘗試獲取
    final int cacheSize = mCachedViews.size();
    for (int i = 0; i < cacheSize; i++) {
        final ViewHolder holder = mCachedViews.get(i);
        // 若是適配器具備穩定的ID,則無效的視圖持有者可能在緩存中,
        // 由於能夠經過getScrapOrCachedViewForId檢索它們
        if (!holder.isInvalid() && holder.getLayoutPosition() == position
                && !holder.isAttachedToTransitionOverlay()) {
            if (!dryRun) {
                mCachedViews.remove(i);
            }
            return holder;
        }
    }
    return null;
}
複製代碼

從緩存池RecycledViewPool中的 mScrap 的 ScrapData 中獲取ViewHolder,並從緩存池中移除RecycledViewPoolgetRecycledView(int viewType):

SparseArray<ScrapData> mScrap = new SparseArray<>();

public ViewHolder getRecycledView(int viewType) {
    final ScrapData scrapData = mScrap.get(viewType);
    if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
        final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
        for (int i = scrapHeap.size() - 1; i >= 0; i--) {
            if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
                return scrapHeap.remove(i);
            }
        }
    }
    return null;
}
複製代碼

經過demo測試緩存

測試demo地址

demo主要是添加了 兩種類型 的數據,默認添加 50 條數據,設置一屏幕最多顯示 8 條數據, 而後經過 滑動添加刪除 數據,再經過反射來獲取對應緩存的數據。


  • 啓動程序時
    啓動
    咱們能夠看到 調用了 8 次 onCreateViewHolder;mAttachedScrap 中的數據也是有8條。
TestAdapter: onCreateViewHolder onCreateViewHolderCount = 1
TestAdapter: onBindViewHolder position = 0
TestAdapter: onCreateViewHolder onCreateViewHolderCount = 2
TestAdapter: onBindViewHolder position = 1
...
TestAdapter: onCreateViewHolder onCreateViewHolderCount = 8
TestAdapter: onBindViewHolder position = 7
MainActivity1572332870607: ChildHelper.mHiddenViews =0
MainActivity1572332870607: LayoutManager.LayoutState.mScrapList =0
MainActivity1572332870607: Recycler.mViewCacheMax =2
MainActivity1572332870607: Recycler.mAttachedScrap.size() = 0
MainActivity1572332870607: Recycler.mChangedScrap.size() = 0
MainActivity1572332870607: Recycler.mCachedViews.size() = 0
MainActivity1572332870607: RecycledViewPool.mScrap.get(viewType = 1).mMaxScrapField = 5
MainActivity1572332870607: RecycledViewPool.mScrap.get(viewType = 1).mScrapHeap.size() = 0
MainActivity1572332870607: RecycledViewPool.mScrap.get(viewType = 2).mMaxScrapField = 5
MainActivity1572332870607: RecycledViewPool.mScrap.get(viewType = 2).mScrapHeap.size() = 0
MainActivity1572332870611: ChildHelper.mHiddenViews =0
MainActivity1572332870611: LayoutManager.LayoutState.mScrapList =0
MainActivity1572332870611: Recycler.mViewCacheMax =2
MainActivity1572332870611: Recycler.mAttachedScrap.size() = 8
MainActivity1572332870611: Recycler.mChangedScrap.size() = 0
MainActivity1572332870611: Recycler.mCachedViews.size() = 0
MainActivity1572332870611: RecycledViewPool.mScrap.get(viewType = 1).mMaxScrapField = 5
MainActivity1572332870611: RecycledViewPool.mScrap.get(viewType = 1).mScrapHeap.size() = 0
MainActivity1572332870611: RecycledViewPool.mScrap.get(viewType = 2).mMaxScrapField = 5
MainActivity1572332870611: RecycledViewPool.mScrap.get(viewType = 2).mScrapHeap.size() = 0
複製代碼

  • 把第一條數據徹底移出屏幕時:
    把第一條數據徹底移出屏幕
    咱們看到調用了兩次 onCreateViewHolder, 而後 Recycler 中的 mViewCacheMax 變成了 3, Recycler.mCachedViews 緩存了 position09 對應位置的數據。即屏幕上下都有不可見的數據是,緩存的是上下各一條不可見視圖的數據。
TestAdapter: onCreateViewHolder onCreateViewHolderCount = 9
TestAdapter: onBindViewHolder position = 8
TestAdapter: onCreateViewHolder onCreateViewHolderCount = 10
TestAdapter: onBindViewHolder position = 9
MainActivity1572332997091: ChildHelper.mHiddenViews =0
MainActivity1572332997091: LayoutManager.LayoutState.mScrapList =0
MainActivity1572332997091: Recycler.mViewCacheMax =3
MainActivity1572332997091: Recycler.mAttachedScrap.size() = 8
MainActivity1572332997091: Recycler.mChangedScrap.size() = 0
MainActivity1572332997091: Recycler.mCachedViews.size() = 2
MainActivity: mCachedViews:---0 = ViewHolder{1bcb007 position=0 id=-1, oldPos=-1, pLpos:-1 no parent}
MainActivity: mCachedViews:---1 = ViewHolder{c735183 position=9 id=-1, oldPos=-1, pLpos:-1 no parent}
MainActivity1572332997091: RecycledViewPool.mScrap.get(viewType = 1).mMaxScrapField = 5
MainActivity1572332997091: RecycledViewPool.mScrap.get(viewType = 1).mScrapHeap.size() = 0
MainActivity1572332997091: RecycledViewPool.mScrap.get(viewType = 2).mMaxScrapField = 5
MainActivity1572332997091: RecycledViewPool.mScrap.get(viewType = 2).mScrapHeap.size() = 0
複製代碼

  • 把第一條數據徹底拉進屏幕時:
    把第一條數據徹底拉進屏幕時

咱們看到沒有調用 onCreateViewHolder, Recycler 中的 mViewCacheMax 仍是 3Recycler.mCachedViews 緩存了 position98 對應位置的數據。即滑動到頂端緩存的是 下方不可見的兩條數據。

MainActivity1572333360514: ChildHelper.mHiddenViews =0
MainActivity1572333360514: LayoutManager.LayoutState.mScrapList =0
MainActivity1572333360514: Recycler.mViewCacheMax =3
MainActivity1572333360514: Recycler.mAttachedScrap.size() = 8
MainActivity1572333360514: Recycler.mChangedScrap.size() = 0
MainActivity1572333360514: Recycler.mCachedViews.size() = 2
MainActivity: mCachedViews:---0 = ViewHolder{c735183 position=9 id=-1, oldPos=-1, pLpos:-1 no parent}
MainActivity: mCachedViews:---1 = ViewHolder{9f16d94 position=8 id=-1, oldPos=-1, pLpos:-1 no parent}
MainActivity1572333360514: RecycledViewPool.mScrap.get(viewType = 1).mMaxScrapField = 5
MainActivity1572333360514: RecycledViewPool.mScrap.get(viewType = 1).mScrapHeap.size() = 0
MainActivity1572333360514: RecycledViewPool.mScrap.get(viewType = 2).mMaxScrapField = 5
MainActivity1572333360514: RecycledViewPool.mScrap.get(viewType = 2).mScrapHeap.size() = 0
複製代碼

  • 把前面四條數據移出屏幕
    把前面四條數據移出屏幕

咱們看到調用了 2 次 onCreateViewHolder, Recycler 中的 mViewCacheMax 仍是 3, Recycler.mCachedViews 緩存了 position2312 對應位置的的 三條 數據。 RecycledViewPool中緩存了一條 viewType = 2 的數據,即上圖有兩列的 item.

TestAdapter: onBindViewHolder position = 1
MainActivity1572333690740: ChildHelper.mHiddenViews =1
MainActivity: mHiddenViews:---0 = android.widget.LinearLayout{9d33071 V.E...... ......I. 0,2268-1440,2588}
MainActivity1572333690740: LayoutManager.LayoutState.mScrapList =0
MainActivity1572333690740: Recycler.mViewCacheMax =3
MainActivity1572333690740: Recycler.mAttachedScrap.size() = 8
MainActivity1572333690740: Recycler.mChangedScrap.size() = 0
MainActivity1572333690740: Recycler.mCachedViews.size() = 3
MainActivity: mCachedViews:---0 = ViewHolder{15ffd7e position=11 id=-1, oldPos=-1, pLpos:-1 no parent}
MainActivity: mCachedViews:---1 = ViewHolder{c735183 position=10 id=-1, oldPos=-1, pLpos:-1 no parent}
MainActivity: mCachedViews:---2 = ViewHolder{9f16d94 position=9 id=-1, oldPos=-1, pLpos:-1 no parent}
MainActivity1572333690740: RecycledViewPool.mScrap.get(viewType = 1).mMaxScrapField = 5
MainActivity1572333690740: RecycledViewPool.mScrap.get(viewType = 1).mScrapHeap.size() = 0
MainActivity1572333690740: RecycledViewPool.mScrap.get(viewType = 2).mMaxScrapField = 5
MainActivity1572333690740: RecycledViewPool.mScrap.get(viewType = 2).mScrapHeap.size() = 0
複製代碼

  • 滑動到頂端點擊添加或刪除
    滑動到頂端點擊添加或刪除

咱們發現 ChildHelper.mHiddenViews中多了條數據。只有看到添加或刪除動畫時,mHiddenViews中才會有數據 。

TestAdapter: onBindViewHolder position = 1
MainActivity1571565149149: ChildHelper.mHiddenViews =1
MainActivity: mHiddenViews:---0 = android.widget.LinearLayout{c44a637 V.E...... ......I. 0,1589-1080,1813}
MainActivity1571565149149: LayoutManager.LayoutState.mScrapList =0
MainActivity1571565149149: Recycler.mViewCacheMax =3
MainActivity1571565149149: Recycler.mAttachedScrap.size() = 8
MainActivity1571565149149: Recycler.mChangedScrap.size() = 0
MainActivity1571565149149: Recycler.mCachedViews.size() = 3
MainActivity: mCachedViews:---0 = ViewHolder{3d52e3f position=11 id=-1, oldPos=-1, pLpos:-1 no parent}
MainActivity: mCachedViews:---1 = ViewHolder{e8dba5e position=10 id=-1, oldPos=-1, pLpos:-1 no parent}
MainActivity: mCachedViews:---2 = ViewHolder{6647799 position=9 id=-1, oldPos=-1, pLpos:-1 no parent}
MainActivity1571565149149: RecycledViewPool.mScrap.get(viewType = 1).mMaxScrapField = 5
MainActivity1571565149149: RecycledViewPool.mScrap.get(viewType = 1).mScrapHeap.size() = 0
MainActivity1571565149149: RecycledViewPool.mScrap.get(viewType = 2).mMaxScrapField = 5
MainActivity1571565149149: RecycledViewPool.mScrap.get(viewType = 2).mScrapHeap.size() = 1
MainActivity: mScrapHeap:---0 = ViewHolder{996130c position=-1 id=-1, oldPos=-1, pLpos:-1 unbound no parent}
複製代碼

小結

經過上文咱們能夠了解到:

獲取 子View 最終是調用了 LayoutManagerLayoutStatenext(RecyclerView.Recycler recycler)。 而後判斷是不是顯示動畫效果,是的話則直接從 mScrapList 獲取,不然經過緩存機制去獲取。


緩存數據相關的變量分別表明的意思: mAttachedScrap:用於緩存顯示在屏幕上並符合對應規則的 ViewHolder,測試發如今Android P 的模擬器上 裏面的數據時空的。 mChangedScrap:用於緩存顯示在屏幕上不符合 mAttachedScrap 規則的 ViewHolder。 mCachedViews:保存滑動過程當中從可見到不可見的 ViewHolder,大小爲DEFAULT_CACHE_SIZE = 2,而且本來數據信息都在,因此能夠直接添加到 RecyclerView 中顯示,不須要再次從新 onBindViewHolder(),在數據多時,滑動過程當中 mCachedViews的大小會變成 3mHiddenViews 中保留在添加或刪除數據或其餘操做,保存在屏幕內可見的 帶動畫子視圖


RecyclerView 的緩存機制大體是:

  • 首先判斷是否是執行動畫的預佈局,是的話則嘗試從 mChangedScrap 中獲取。
  • 而後嘗試從mAttachedScrapmHiddenViewsmCachedViews獲取。
  • 而後根據 mAdapter.hasStableIds() 以及 mViewCacheExtension!=null 判斷是否從用戶自定義設置的相關邏輯中獲取。
  • 而後再嘗試從緩存池 RecycledViewPoolmScrapScrapData 中獲取,每種樣式最多緩存 5個
  • 還沒找到則經過 mAdapter.createViewHolder 建立新的。

若是有寫錯的地方,請幫忙評論指正,感謝!

若是以爲不錯的話,請幫忙點個讚唄。

以上


掃描下面的二維碼,關注個人公衆號 Android1024, 點關注,不迷路。

Android1024
相關文章
相關標籤/搜索