RecyclerView緩存機(jī)制

在之前的兩篇文章介紹了RV的繪制和滑動(dòng),留下了兩個(gè)方法沒(méi)有具體看,scrollByInternal()
tryGetViewHolderForPositionByDeadline(),本文會(huì)補(bǔ)充這兩個(gè)方法的分析;

RV的四級(jí)緩存

Recycler類是RV復(fù)用機(jī)制的核心實(shí)現(xiàn)類,設(shè)計(jì)了四級(jí)緩存,優(yōu)先級(jí)順序是:Scrap、CacheView、ViewCacheExtension、RecycledViewPool

Recycler類

    public final class Recycler {
        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
        ArrayList<ViewHolder> mChangedScrap = null;

        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

        private final List<ViewHolder>
                mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);

        private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
        int mViewCacheMax = DEFAULT_CACHE_SIZE;

        RecycledViewPool mRecyclerPool;

        private ViewCacheExtension mViewCacheExtension;

        static final int DEFAULT_CACHE_SIZE = 2;
}
  • mAttachedScrap:不參與滑動(dòng)時(shí)的回收復(fù)用,只保存重新布局時(shí)從RecyclerView分離的item的無(wú)效、未移除、未更新的holder。因?yàn)镽ecyclerView在onLayout的時(shí)候,會(huì)先把children全部移除掉,再重新添加進(jìn)入,mAttachedScrap臨時(shí)保存這些holder復(fù)用。
  • mChangedScrap:mChangedScrap和mAttachedScrap類似,不參與滑動(dòng)時(shí)的回收復(fù)用,只是用作臨時(shí)保存的變量,它只會(huì)負(fù)責(zé)保存重新布局時(shí)發(fā)生變化的item的無(wú)效、未移除的holder,那么會(huì)重走adapter綁定數(shù)據(jù)的方法。
  • mCachedViews : 用于保存最新被移除(remove)的ViewHolder,已經(jīng)和RecyclerView分離的視圖;它的作用是滾動(dòng)的回收復(fù)用時(shí)如果需要新的ViewHolder時(shí),精準(zhǔn)匹配(根據(jù)position/id判斷)是不是原來(lái)被移除的那個(gè)item;如果是,則直接返回ViewHolder使用,不需要重新綁定數(shù)據(jù);如果不是則不返回,再去mRecyclerPool中找holder實(shí)例返回,并重新綁定數(shù)據(jù)。這一級(jí)的緩存是有容量限制的,最大數(shù)量為2。
  • mViewCacheExtension: RecyclerView給開(kāi)發(fā)者預(yù)留的緩存池,開(kāi)發(fā)者可以自己拓展回收池,一般不會(huì)用到,用RecyclerView系統(tǒng)自帶的已經(jīng)足夠了。
  • mRecyclerPool:是一個(gè)終極回收站,真正存放著被標(biāo)識(shí)廢棄(其他池都不愿意回收)的ViewHolder的緩存池,如果上述mAttachedScrap、mChangedScrap、mCachedViews、mViewCacheExtension都找不到ViewHolder的情況下,就會(huì)從mRecyclerPool返回一個(gè)廢棄的ViewHolder實(shí)例,但是這里的ViewHolder是已經(jīng)被抹除數(shù)據(jù)的,沒(méi)有任何綁定的痕跡,需要重新綁定數(shù)據(jù)。它是根據(jù)itemType來(lái)存儲(chǔ)的,是以SparseArray嵌套一個(gè)ArraryList的形式保存ViewHolder的。

四級(jí)緩存:

  • Scrap:最輕量級(jí)的復(fù)用,包含mAttachedScrapmAttachedScrap兩個(gè)部分,Scrap不會(huì)參與滑動(dòng)時(shí)的回收復(fù)用,作為重新布局的臨時(shí)緩存,當(dāng)RV重新布局時(shí),mAttachedScrap負(fù)責(zé)保存其中沒(méi)有改變的ViewHolder;剩下的由mChangedScrap負(fù)責(zé)保存,布局結(jié)束時(shí),Scrap列表應(yīng)該是空的,緩存的數(shù)據(jù)要么重新布局出來(lái),要么被清空;

  • CacheView:用于RV列表位置產(chǎn)生變動(dòng)時(shí),對(duì)剛剛移出屏幕的view進(jìn)行回收復(fù)用。CacheView的最大容量是2,如果一直向一個(gè)方向滑動(dòng),緩存的Item超過(guò)兩個(gè),就將CacheView中第一個(gè)item放入RecycledViewPool中,再將新的放入CacheView,如果一直朝一個(gè)方向滾動(dòng),CacheView并沒(méi)有在效率上產(chǎn)生幫助,它只是把后面滑過(guò)的ViewHolder緩存起來(lái),如果經(jīng)常來(lái)回滑動(dòng),那么從CacheView根據(jù)對(duì)應(yīng)位置的item直接復(fù)用,不需要重新綁定數(shù)據(jù),將會(huì)得到很好的利用。

  • ViewCacheExtension:自定義緩存,額外提供了一層緩存池給開(kāi)發(fā)者,開(kāi)發(fā)者視情況而定是否使用ViewCacheExtension增加一層緩存池;

  • RecycledViewPool:終極回收站,在Scrap、CacheView、ViewCacheExtension都不愿意回收的時(shí)候,都會(huì)丟到RecycledViewPool中回收;

    public static class RecycledViewPool {
        //  mScrap 的大小
        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<>();
    }
RecycledViewPool

RecycledViewPool的本質(zhì)是一個(gè)SparseArray,SparseArray的value是ScrapData(存放VH的ArrayList),緩存池定義了默認(rèn)的緩存大小DEFAULT_MAX_SCRAP = 5,這個(gè)數(shù)量不是說(shuō)整個(gè)緩存池只能緩存這多個(gè)ViewHolder,而是不同itemType的ViewHolder的list的緩存數(shù)量,即mScrap的數(shù)量,說(shuō)明最多只有5組不同類型的mScrapHeap。mMaxScrap = DEFAULT_MAX_SCRAP說(shuō)明每種不同類型的ViewHolder默認(rèn)保存5個(gè),當(dāng)然mMaxScrap的值是可以設(shè)置的。

SparseArray分析:避免裝箱,節(jié)省空間,不需要hash映射

其實(shí),Scrap緩存池不參與滾動(dòng)的回收復(fù)用,CacheView緩存池被稱為一級(jí)緩存,又因?yàn)閂iewCacheExtension緩存池是給開(kāi)發(fā)者定義的緩存池,一般不用到,所以RecycledViewPool緩存池被稱為二級(jí)緩存,那么這樣來(lái)說(shuō)實(shí)際只有兩層緩存。

緩存復(fù)用

在RV滑動(dòng)中,分析了onTouchEvent(),在Action_MOVE的時(shí)候會(huì)調(diào)用scrollByInternal()去滑動(dòng)

boolean scrollByInternal(int x, int y, MotionEvent ev) {
    ...
            if (x != 0) {
                consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
                unconsumedX = x - consumedX;
            }
            if (y != 0) {
                consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
                unconsumedY = y - consumedY;
            }
...
 }

ScrollByInternal()會(huì)將滑動(dòng)操作托付給LayoutManager,LayoutManager會(huì)調(diào)用自己的fill()處理,這個(gè)方法源碼注釋描述為The magic functions

  • 在繪制時(shí)step2()中調(diào)用LayoutMangeronLayoutChildren()進(jìn)行child的measure和layout;
  • 滑動(dòng)時(shí),會(huì)調(diào)用它去進(jìn)行回收和復(fù)用;
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
...

            recycleByLayoutState(recycler, layoutState);

            layoutChunk(recycler, state, layoutState, layoutChunkResult);
...
    }
layoutChunk():

這個(gè)方法在繪制篇已經(jīng)分析過(guò),在繪制時(shí)step2()中調(diào)用LayoutManger的onLayoutChildren()


上面是關(guān)于繪制時(shí)候的分析,下面補(bǔ)充一下復(fù)用的邏輯next();

注意:新版本中,tryGetViewHolderForPositionByDeadline() 改名為 getViewForPosition()

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

        }


       View getViewForPosition(int position, boolean dryRun) {
...
               // 0) If there is a changed scrap, try to find from there
            if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrap = holder != null;
            }

            // 1) Find from scrap by position
            if (holder == null) {
                holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);

                // 2) Find from scrap via stable ids, if exists
                if (mAdapter.hasStableIds()) {
                    holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
                  
                  //  自定義緩存獲取
                    final View view = mViewCacheExtension
                            .getViewForPositionAndType(this, position, type);
                    if (view != null) {
                        holder = getChildViewHolder(view);

                    //  調(diào)用adapter去創(chuàng)建新的ViewHolder
                    holder = mAdapter.createViewHolder(RecyclerView.this, type);
...
        }
next() 獲取viewHolder

至此,RV的復(fù)用機(jī)制已經(jīng)分析的很清楚了,下面看回收機(jī)制

recycleByLayoutState()回收的入口方法:


recycleByLayoutState()的代碼就不貼了,最后它調(diào)用了回收的核心方法:recyclerViewHolderInternal():

      void recycleViewHolderInternal(ViewHolder holder) {
...
                    if (cachedViewSize < mViewCacheMax) {       
                        mCachedViews.add(holder);
                        cached = true;
                    }
                }
                if (!cached) {
                    addViewHolderToRecycledViewPool(holder);
                    recycled = true;
                }
...
        }

        void addViewHolderToRecycledViewPool(ViewHolder holder) {
            ViewCompat.setAccessibilityDelegate(holder.itemView, null);
            dispatchViewRecycled(holder);
            holder.mOwnerRecyclerView = null;
            getRecycledViewPool().putRecycledView(holder);
        }

上面的代碼我們可以看到在RV滑動(dòng)的時(shí)候,真正回收的只有兩級(jí)緩存,mCachedViewsRecycledViewPool;

至此,RV的兩級(jí)回收和復(fù)用已經(jīng)分析完畢,下面看一下第一級(jí)緩存的回收工作, 第一級(jí)緩存Scrap的回收是在繪制的時(shí)候工作的;

Scarp回收和復(fù)用

在繪制的時(shí)候,Step2除了調(diào)用fill()去對(duì)chail進(jìn)行測(cè)量和布局,在此之前還計(jì)算了錨點(diǎn)和調(diào)用了detachAndScrapAttachedViews(),這些操作都是由LayoutManager完成的,RV28.0把step1,step2,step3移除了,但是這些實(shí)現(xiàn)的思路任然是一樣的;

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


        private void scrapOrRecycleView(Recycler recycler, int index, View view) {
...
            if (viewHolder.isInvalid() && !viewHolder.isRemoved() && !viewHolder.isChanged() &&
                    !mRecyclerView.mAdapter.hasStableIds()) {
                removeViewAt(index);
                recycler.recycleViewHolderInternal(viewHolder);
            } else {
                detachViewAt(index);
                recycler.scrapView(view);
            }
        }

       void scrapView(View view) {
            final ViewHolder holder = getChildViewHolderInt(view);
            holder.setScrapContainer(this);
            if (!holder.isChanged() || !supportsChangeAnimations()) {
                if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
                    throw new IllegalArgumentException("Called scrap view with an invalid view."
                            + " Invalid views cannot be reused from scrap, they should rebound from"
                            + " recycler pool.");
                }
                mAttachedScrap.add(holder);
            } else {
                if (mChangedScrap == null) {
                    mChangedScrap = new ArrayList<ViewHolder>();
                }
                mChangedScrap.add(holder);
            }
        }

在這里我們看到了Scrap緩存的核心回收方法scrapView(),通過(guò)viewHolder的狀態(tài),判斷當(dāng)前是在mAttachedScrap回收,還是在mChangedScrap回收

復(fù)用的話也是在上文的核心復(fù)用方法里面老版本是tryGetViewHolderForPositionByDeadline(),新版本是getViewForPosition()

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容