Recyclerview緩存機制分析

最近做項目的時候突然想到一個問題,就是在項目裏面使用了多種ViewHolder,但是在onBindView這個方法中,RecyclerView是如何知道我在哪個位置需要的是哪種ViewHolder呢?就這個問題趁機看了一下源碼,終於找到了答案,原來RecyclerView的緩存機制是區分type的,也就是Recyclerview.Adapter.getItemViewType()這個方法的返回值來區分的。
那麼從哪裏開始看RecyclerView的源碼呢?
因爲View創建出來需要加到RecyclerView中,所以從view的measure、layout中去找,RecyclerView的佈局都是交給LayoutManager,這裏以LinearLayoutManager爲例,從onLayoutChildren開始,調用鏈爲

onLayoutChildren --> fill --> layoutChunk

這裏fill、layoutChunk都是佈局的核心代碼,在layoutChunk開頭有這麼一段:

 View view = layoutState.next(recycler);

這個地方返回一個view,傳進去的參數爲recycler,大概可以猜到這和緩存有關了,點進去看看:

View next(RecyclerView.Recycler recycler) {
            if (mScrapList != null) {
                return nextViewFromScrapList();
            }
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        }

果然,使用到了recycler.getViewForPosition,這個就是緩存的其實方法,最終看到了tryGetViewHolderForPositionByDeadline這個方法,這裏就是緩存邏輯的核心代碼,這裏只挑重點看:

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
             .......
             //和動畫有關係,而且scrapList是在LayoutManager內部的,和recyclerview關係不大
            if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }
            // 1) Find by position from 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);
                ......
                //擴展的緩存
                if (holder == null && mViewCacheExtension != null) {
                    // We are NOT sending the offsetPosition because LayoutManager does not
                    // know it.
                    final View view = mViewCacheExtension
                            .getViewForPositionAndType(this, position, type);
                   .......
                }
                if (holder == null) { // fallback to pool
                    if (DEBUG) {
                        Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
                                + position + ") fetching from shared pool");
                    }
                    //從Recycler中獲取緩存
                    holder = getRecycledViewPool().getRecycledView(type);
                    if (holder != null) {
                        holder.resetInternal();
                        if (FORCE_INVALIDATE_DISPLAY_LIST) {
                            invalidateDisplayListInt(holder);
                        }
                    }
                }
                if (holder == null) {
                    long start = getNanoTime();
                    if (deadlineNs != FOREVER_NS
                            && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
                        // abort - we have a deadline we can't meet
                        return null;
                    }
                    //創建ViewHolder
                    holder = mAdapter.createViewHolder(RecyclerView.this, type);
              	......
            }
            return holder;
        }

上面的流程其實就是按照這麼四級緩存一步一步查找下去,分別是

mAttachedScrap , mCachedViews ,mViewCacheExtension, recycledViewPool

這四級緩存都是由Recycler這個類來管理的
下面分別介紹這四種緩存的作用:

  • 一級緩存: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;
                }
            }

在滑動過程中一般不到這個緩存中去查找,因爲此緩存是和layout相關的,在layout過程中,會首先將recyclerview的子view全部detach下來,放到這個數組裏面,然後經過layout過程,再一個個的加回去,在LinearLayoutManager.onLayoutChildren中先調用detachAndScrapAttachedViews,然後再進行fill操作:

public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
            final int childCount = getChildCount();
            for (int i = childCount - 1; i >= 0; i--) {
                final View v = getChildAt(i);
                scrapOrRecycleView(recycler, i, v);
            }
        }

上面很明顯就是把所有childView detach下來,具體的detach操作還是ChildViewHelper來進行的。
那爲什麼在layout之前進行detach操作呢?
我猜這是和recyclerview特性相關的,因爲recyclerview事先並不知道自己能有多少個childView,這不僅取決於childView的大小還取決於layoutmanager的具體佈局方式,也就是說measure和layout的過程是相關的,等到childview放滿了recyclerview就不再添加childView了。而一般的ViewGroup的chilgview個數都是確定的,childview個數和measure、layout完全無關,只要全部測量完,然後挨個layout就行。
由於以上的性質,recyclerview的measure和layout過程和一般的viewgroup都不一樣,recyclerview是先測量childview1,然後擺放childview1.接着測量childview2,然後擺放childview2.然後繼續,直到擺滿recyclerview。
在代碼中這樣體現:

@Override
    protected void onMeasure(int widthSpec, int heightSpec) {
    
        if (mLayout.isAutoMeasureEnabled()) {
        ......
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);

            final boolean measureSpecModeIsExactly =
                    widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
            if (measureSpecModeIsExactly || mAdapter == null) {
                return;
            }

            if (mState.mLayoutStep == State.STEP_START) {
                dispatchLayoutStep1();
            }
            // set dimensions in 2nd step. Pre-layout should happen with old dimensions for
            // consistency
            mLayout.setMeasureSpecs(widthSpec, heightSpec);
            mState.mIsMeasuring = true;
            dispatchLayoutStep2();
 }

可以看出在Recyclerview的onMeasure階段,就開始進行layout了。

  • 二級緩存:mCachedViews
    這個緩存中也是精確匹配的,也就是需要位置一樣才能複用,並且複用之後不用重新bind數據,因爲位置一樣,數據就是一樣的,不用重新bind,這個緩存的大小爲2,數據結構使用了arraylist。
  • 三級緩存:mViewCacheExtension
    這個屬於自定義緩存,一般不使用
  • 四級緩存:mRecyclerPool
    這個緩存中是根據type來複用的,每個type緩存5個holder,可以使用map+arraylist的數據結構緩存,因爲type爲int類型,所以使用SparseArray來作爲鍵值對查找,效率更高。所以是SparseArray+ArrayList結構:
static class ScrapData {
            final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
            int mMaxScrap = DEFAULT_MAX_SCRAP;
            long mCreateRunningAverageNs = 0;
            long mBindRunningAverageNs = 0;
        }
        SparseArray<ScrapData> mScrap = new SparseArray<>();

基於上面的緩存說明,寫了一個例子來驗證:
在這裏插入圖片描述
這裏recyclerview寬高爲正方形的三倍,我們來計算在整個滑動過程中會創建幾個viewHolder,首先一屏之內最多可以出現12個item,因此先創建12個,當第四行出現,第一行消失時,先將第一行的三個viewholder回收,那麼這時緩存池中的情況是,mCachedViews=2,mRecyclerPool=1,因爲mCachedViews是精確匹配的,因此只有複用mRecyclerPool中的一個,所以第四行只能有一個複用,剩下兩個創建,因此總計創建14個ViewHolder,再滑動其實就不會創建viewholder了。上面的計算是沒有預加載的情況下得到的,默認Recyclerview是有預加載的,可以禁止,使用layoutmanager.setItemPrefetchEnabled(false)禁止預加載,預加載是爲了滑動更順滑,不過在這裏爲了簡化分析,所以禁止了預加載。
具體demo鏈接:
https://github.com/whoami-I/RecyclerViewExample

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章