RecyclerView的緩存機制

1、四級緩存

RecyclerView的緩存的工作主要由其內部類Recycler來完成的,代碼如下:
Recycler

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

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

        private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
        int mViewCacheMax = DEFAULT_CACHE_SIZE;

        RecycledViewPool mRecyclerPool;

        private ViewCacheExtension mViewCacheExtension;

        static final int DEFAULT_CACHE_SIZE = 2;
        //省略。。。
}

RecycledViewPool

public static class RecycledViewPool {
        private static final int DEFAULT_MAX_SCRAP = 5;
        static class ScrapData {
            ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
            int mMaxScrap = DEFAULT_MAX_SCRAP;
            long mCreateRunningAverageNs = 0;
            long mBindRunningAverageNs = 0;
        }
        SparseArray<ScrapData> mScrap = new SparseArray<>();
        //省略。。。
}

從上面代碼可知,RecyclerView不同于ListView,RecyclerView緩存的是ViewHolder,而ListView緩存的是View。
RecyclerView的緩存優先級從高到低如下:

  • 1、mAttachedScrap:緩存屏幕中可見范圍中的ViewHolder。
  • 2、mCachedViews :緩存滑動中即將與RecyclerView分離的ViewHolder,默認最大為2個。
  • 3、mViewCacheExtension:自定義實現的緩存。
  • 4、mRecyclerPool:ViewHolder緩存池,可支持不同的ViewType。

1.1、mAttachedScrap

mAttachedScrap緩存的是當前屏幕上的ViewHolder,對應的數據結構是ArrayList,沒有大小限制。在調用LayoutManager#onLayoutChildren方法時對views進行布局,此時會將RecyclerView上的ViewHolder全部暫存到該集合中。

RecyclerView需要做表項的動畫,就需要做兩次布局:預布局和后布局,分別記錄動畫的起始狀態和終止狀態。兩次布局意味著
LayoutManager#onLayoutChildren就需要調用兩次,為了防止子元素的重復添加以及重復的創建和數據綁定,RecyclerView在每次添加子元素前,先通過detachAndScrapAttachedViews(recycler)移除屏幕上的子元素并暫存在mAttachedScrap中,在下次布局時,直接從mAttachedScrap中取出并添加。

該緩存中ViewHolder的特性是:如果和RecyclerView上的position或者itemId匹配上了,那么就可以直接拿來使用,不需要調用onBindViewHolder重新綁定數據。

1.2、mCachedViews

mCachedViews緩存滑動時即將與RecyclerView分離的ViewHolder,其數據結構為ArrayList,該緩存對大小是有限制的,默認為2個。

該緩存中的ViewHolder的特性是:只要position和itemId匹配上了,則可直接使用,不需要調用onBindViewHolder重新綁定數據。
開發者可以調用setItemViewCacheSize(size)方法來改變緩存的大小,該層級緩存觸發的一個常見的場景是滑動RecyclerView。

在滑動過程中,將與RecyclerView分離的ViewHolder存儲在mCachedViews中,當mCachedViews的大小超過2個,就會按照FIFO從mCachedViews中移除并添加到RecycledViewPool中。

1.1.3、viewCacheExtension

ViewCacheExtension是需要開發者自己實現的緩存,一般不用。

1.1.4、RecyclerViewPool

ViewHolder緩存池,本質上是一個SparseArray,其中key是ViewType,value是ArrayList<ViewHolder>,默認每個ArrayList中最多存儲5個。

ViewHolder存儲在緩存池的前會進行重置變成一個干凈的ViewHolder,所以在復用時,需要調用onBindViewHolder重綁數據。

2、復用流程

緩存的復用肯定是在填充子元素過程中完成的,而填充子元素的方法為LinearLayoutManager#fill()(這里以LinearLayoutManager為例。)

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    
    if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
        // TODO ugly bug fix. should not happen
        if (layoutState.mAvailable < 0) {
            layoutState.mScrollingOffset += layoutState.mAvailable;
        }
        //滑動時回收與RecyclerView分離的ViewHolder到mCachedViews、mRecyclerPool中
        recycleByLayoutState(recycler, layoutState);
    }
    int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
    //循環填充,直到沒有空間
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        //...
        //填充子View
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
    //...
    return start - layoutState.mAvailable;
}

填充子View的邏輯在layoutChunk()中

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
    //獲取一個合適的View
    View view = layoutState.next(recycler);
    //...
    LayoutParams params = (LayoutParams) view.getLayoutParams();
    if (layoutState.mScrapList == null) {
        if (mShouldReverseLayout == (layoutState.mLayoutDirection
                == LayoutState.LAYOUT_START)) {
            //添加View
            addView(view);
        } else {
            addView(view, 0);
        }
    } else {
        if (mShouldReverseLayout == (layoutState.mLayoutDirection
                == LayoutState.LAYOUT_START)) {
            addDisappearingView(view);
        } else {
            addDisappearingView(view, 0);
        }
    }
    //子View的測量
    measureChildWithMargins(view, 0, 0);
    //...
    //子View的布局
    layoutDecoratedWithMargins(view, left, top, right, bottom);
    //...
    result.mFocusable = view.hasFocusable();
}

在layoutChunk中通過next獲取一個View

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

在next中調用Recycler的getViewForPosition方法獲取View,最終會調用tryGetViewHolderForPositionByDeadline(),復用的主要邏輯就這個方法中。

    @Nullable
    ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
        ViewHolder holder = null;
        // 0) 如果它是改變的廢棄的ViewHolder,在scrap的mChangedScrap找
        if (mState.isPreLayout()) {
            holder = getChangedScrapViewForPosition(position);
            fromScrapOrHiddenOrCache = holder != null;
        }
        // 1)根據position分別在scrap的mAttachedScrap、mChildHelper、mCachedViews中查找
        if (holder == null) {
            holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        }

        if (holder == null) {
            final int type = mAdapter.getItemViewType(offsetPosition);
            // 2)根據id在scrap的mAttachedScrap、mCachedViews中查找
            if (mAdapter.hasStableIds()) {
                holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
            }
            if (holder == null && mViewCacheExtension != null) {
                //3)在ViewCacheExtension中查找,一般不用到,所以沒有緩存
                final View view = mViewCacheExtension
                        .getViewForPositionAndType(this, position, type);
                if (view != null) {
                    holder = getChildViewHolder(view);
                }
            }
            //4)在RecycledViewPool中查找
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                holder.resetInternal();
                if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
                }
            }
        }
        //5)到最后如果還沒有找到復用的ViewHolder,則新建一個
        holder = mAdapter.createViewHolder(RecyclerView.this, type);
    }

可以看到,tryGetViewHolderForPositionByDeadline()方法分別去scrap、CacheView、ViewCacheExtension、RecycledViewPool中獲取ViewHolder,如果沒有則創建一個新的ViewHolder。

2.1、getChangedScrapViewForPosition

一般情況下,當我們調用adapter的notifyItemChanged()方法,數據發生變化時,item緩存在mChangedScrap和mAttachedScrap中,后續拿到的ViewHolder需要重新綁定數據。此時查找ViewHolder就會通過position和id分別在scrap的mChangedScrap中查找。

   ViewHolder getChangedScrapViewForPosition(int position) {
        //通過position
        for (int i = 0; i < changedScrapSize; i++) {
            final ViewHolder holder = mChangedScrap.get(i);
            return holder;
        }
        // 通過id
        if (mAdapter.hasStableIds()) {
            final long id = mAdapter.getItemId(offsetPosition);
            for (int i = 0; i < changedScrapSize; i++) {
                final ViewHolder holder = mChangedScrap.get(i);
                return holder;
            }
        }
        return null;
    }

2.2、getScrapOrHiddenOrCachedHolderForPosition

如果沒有找到視圖,根據position分別在scrap的mAttachedScrap、mHiddenViews、mCachedViews中查找,涉及的方法如下。

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

        // 首先從mAttachedScrap中查找,精準匹配有效的ViewHolder
        for (int i = 0; i < scrapCount; i++) {
            final ViewHolder holder = mAttachedScrap.get(i);
            return holder;
        }
        //接著在mChildHelper中mHiddenViews查找隱藏的ViewHolder
        if (!dryRun) {
            View view = mChildHelper.findHiddenNonRemovedView(position);
            if (view != null) {
                final ViewHolder vh = getChildViewHolderInt(view);
                scrapView(view);
                return vh;
            }
        }
        //最后從我們的一級緩存中mCachedViews查找。
        final int cacheSize = mCachedViews.size();
        for (int i = 0; i < cacheSize; i++) {
            final ViewHolder holder = mCachedViews.get(i);
            return holder;
        }
    }

可以看到,getScrapOrHiddenOrCachedHolderForPosition查找ViewHolder的順序如下:

  • 首先,從mAttachedScrap中查找,精準匹配有效的ViewHolder;
  • 接著,在mChildHelper中mHiddenViews查找隱藏的ViewHolder;
  • 最后,從一級緩存中mCachedViews查找。

2.3 getScrapOrCachedViewForId

getScrapOrCachedViewForId和getScrapOrHiddenOrCachedHolderForPosition的邏輯類似,只不過這里是以id進行查找的,而getScrapOrHiddenOrCachedHolderForPosition是以position進行查找的。

2.4 mViewCacheExtension

mViewCacheExtension是由開發者定義的一層緩存策略,Recycler并沒有將任何view緩存到這里

2.5 RecycledViewPool

在ViewHolder的四級緩存中,我們有提到過RecycledViewPool,它是通過itemType把ViewHolder的List緩存到SparseArray中的,在getRecycledViewPool().getRecycledView(type)根據itemType從SparseArray獲取ScrapData ,然后再從里面獲取ArrayList<ViewHolder>,從而獲取到ViewHolder。

    @Nullable
    public ViewHolder getRecycledView(int viewType) {
        final ScrapData scrapData = mScrap.get(viewType);//根據viewType獲取對應的ScrapData 
        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;
    }

2.6 創建新的ViewHolder

如果還沒有獲取到ViewHolder,則通過mAdapter.createViewHolder()創建一個新的ViewHolder返回。

  // 如果還沒有找到復用的ViewHolder,則新建一個
  holder = mAdapter.createViewHolder(RecyclerView.this, type);

下面是尋找ViewHolder的一個完整的流程圖:


327484616-c1811ca79564e15c.png

3、回收流程

RecyclerView回收的入口有很多, 但是不管怎么樣操作,RecyclerView 的回收或者復用必然涉及到add View 和 remove View 操作, 所以我們從onLayout的流程入手分析回收和復用的機制。

首先,在LinearLayoutManager中,我們來到itemView布局入口的方法onLayoutChildren(),如下所示。

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
            if (state.getItemCount() == 0) {
                removeAndRecycleAllViews(recycler);
                return;
            }
        }
        ensureLayoutState();
        mLayoutState.mRecycle = false;//禁止回收
        //顛倒繪制布局
        resolveShouldLayoutReverse();
        onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);

        //暫時分離已經附加的view,即將所有child detach并通過Scrap回收
        detachAndScrapAttachedViews(recycler);
    }

在onLayoutChildren布局時,會調用detachAndScrapAttachedViews()方法將屏幕上的item與RecyclerView進行分離并存儲到緩存中,在重新布局時,再將ViewHolder重新一個一個放到新位置上去。(RecyclerView為了實現item動畫,會進行兩次onLayoutChildren的調用。)

將屏幕上的ViewHolder從RecyclerView的布局中拿下來后,存放在Scrap中,Scrap包括mAttachedScrap和mChangedScrap,它們是一個list,用來保存從RecyclerView布局中拿下來ViewHolder列表。

detachAndScrapAttachedViews()只會在onLayoutChildren()中調用,只有在布局的時候,才會把ViewHolder detach掉,然后再add進來重新布局。

但是大家需要注意:

  • Scrap只是保存從RecyclerView布局中當前屏幕顯示的item的ViewHolder,不參與回收復用,單純是為了現從RecyclerView中拿下來再重新布局上去。
  • 對于沒有保存到的item,會放到mCachedViews或者RecycledViewPool緩存中參與回收復用。
   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);
        }
    }

   private void scrapOrRecycleView(Recycler recycler, int index, View view) {
        final ViewHolder viewHolder = getChildViewHolderInt(view);
        if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                && !mRecyclerView.mAdapter.hasStableIds()) {
            removeViewAt(index);//移除VIew
            recycler.recycleViewHolderInternal(viewHolder);//緩存到CacheView或者RecycledViewPool中
        } else {
            detachViewAt(index);//分離View
            recycler.scrapView(view);//scrap緩存
            mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
        }
    }

然后,我們看detachViewAt()方法分離視圖,再通過scrapView()緩存到scrap中。

    void scrapView(View view) {
        final ViewHolder holder = getChildViewHolderInt(view);
        if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
            holder.setScrapContainer(this, false);
            mAttachedScrap.add(holder);//保存到mAttachedScrap中
        } else {
            if (mChangedScrap == null) {
                mChangedScrap = new ArrayList<ViewHolder>();
            }
            holder.setScrapContainer(this, true);
            mChangedScrap.add(holder);//保存到mChangedScrap中
        }
    }

然后,我們回到scrapOrRecycleView()方法中,進入if()分支。如果viewHolder是無效、未被移除、未被標記的則放到recycleViewHolderInternal()緩存起來,同時removeViewAt()移除了viewHolder。
在調用notifyDataSetChange方法時,會將viewHolder都標記成無效的,故會進入該判斷,將屏幕上的ViewHolder緩存在RecycledViewPool中。

   void recycleViewHolderInternal(ViewHolder holder) {
           ·····
        if (forceRecycle || holder.isRecyclable()) {
            if (mViewCacheMax > 0
                    && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                    | ViewHolder.FLAG_REMOVED
                    | ViewHolder.FLAG_UPDATE
                    | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {

                int cachedViewSize = mCachedViews.size();
                if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {//如果超出容量限制,把第一個移除
                    recycleCachedViewAt(0);
                    cachedViewSize--;
                }
                     ·····
                mCachedViews.add(targetCacheIndex, holder);//mCachedViews回收
                cached = true;
            }
            if (!cached) {
                addViewHolderToRecycledViewPool(holder, true);//放到RecycledViewPool回收
                recycled = true;
            }
        }
    }

如果符合條件,會優先緩存到mCachedViews中時,如果超出了mCachedViews的最大限制,通過recycleCachedViewAt()將CacheView緩存的第一個數據添加到終極回收池RecycledViewPool后再移除掉,最后才會add()新的ViewHolder添加到mCachedViews中。

剩下不符合條件的則通過addViewHolderToRecycledViewPool()緩存到RecycledViewPool中。

    void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
        clearNestedRecyclerViewIfNotNested(holder);
        View itemView = holder.itemView;
        ······
        holder.mOwnerRecyclerView = null;
        getRecycledViewPool().putRecycledView(holder);//將holder添加到RecycledViewPool中
    }

最后,在滑動過程中會調用填充布局調用fill()方法,它會回收移出屏幕的view到mCachedViews或者RecycledViewPool中。

 int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
              recycleByLayoutState(recycler, layoutState);//回收移出屏幕的view
        }
    }

而recycleByLayoutState()方法最終會調用recycleViewHolderInternal()就是用來回收移出屏幕的view,優先回到mCachedViews,當mCachedViews超過2個后,根據FIFO移除ViewHolder并添加到RecycledViewPool中。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容