RecyclerView 源碼分析(二) —— 緩存機制 RecyclerView 源碼分析(一) —— 繪製流程解析 RecyclerView 源碼分析(一) —— 繪製流程解析

在前一篇文章 RecyclerView 源碼分析(一) —— 繪製流程解析 介紹了 RecyclerView 的繪製流程,RecyclerView 通過將繪製流程從 View 中抽取出來,放到 LayoutManager 中,使得 RecyclerView 在不同的 LayoutManager 中,擁有不同的樣式,使得 RecyclerView 異常靈活,大大加強了 RecyclerView 使用場景。

當然,RecyclerView 的緩存機制也是它特有的一個優點,減少了對內存的佔用以及重複的繪製工作,因此,本文意在介紹和學習 RecyclerView 的緩存設計思想。

當我們在討論混存的時候,一定會經歷創建-緩存-複用的過程。因此對於 RecyclerView 的緩存機制也是按照如下的步驟進行。

創建 ViewHolder(VH)

在講到對子 itemView 測量的時候,layoutChunk 方法中會先獲得每一個 itemView,在獲取後,在將其添加到 RecyclerView 中。所以我們先來看看創建的過程:

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

next 就是調用 RecyclerView 的 getViewForPosition 方法來獲取一個 View 的。而 getViewForPosition 方法最終會調用到 RecyclerView tryGetViewHolderForPositionByDeadline 方法。

tryGetViewHolderForPositionByDeadline

這個方法很長,但是其實邏輯很簡單,整個過程前面部分是先從緩存嘗試獲取 VH,如果找不到,就會創建新的 VH,然後綁定數據,最後將再將 VH 綁定到 LayoutParams (LP) 上。

        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
            if (position < 0 || position >= mState.getItemCount()) {
                throw new IndexOutOfBoundsException("Invalid item position " + position
                        + "(" + position + "). Item count:" + mState.getItemCount()
                        + exceptionLabel());
            }
            boolean fromScrapOrHiddenOrCache = false;
            ViewHolder holder = null;
            // 省略從緩存查找 VH 的邏輯,下面是如果還是沒找到,就會創建一個新的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;
                    }
            // 創建 VH holder
= mAdapter.createViewHolder(RecyclerView.this, type); if (ALLOW_THREAD_GAP_WORK) { // only bother finding nested RV if prefetching RecyclerView innerView = findNestedRecyclerView(holder.itemView); if (innerView != null) { holder.mNestedRecyclerView = new WeakReference<>(innerView); } } long end = getNanoTime(); mRecyclerPool.factorInCreateTime(type, end - start); if (DEBUG) { Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder"); } } } // This is very ugly but the only place we can grab this information // before the View is rebound and returned to the LayoutManager for post layout ops. // We don't need this in pre-layout since the VH is not updated by the LM. if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) { holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST); if (mState.mRunSimpleAnimations) { int changeFlags = ItemAnimator .buildAdapterChangeFlagsForAnimations(holder); changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT; final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState, holder, changeFlags, holder.getUnmodifiedPayloads()); recordAnimationInfoIfBouncedHiddenView(holder, info); } } boolean bound = false; if (mState.isPreLayout() && holder.isBound()) { // do not update unless we absolutely have to. holder.mPreLayoutPosition = position; } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) { if (DEBUG && holder.isRemoved()) { throw new IllegalStateException("Removed holder should be bound and it should" + " come here only in pre-layout. Holder: " + holder + exceptionLabel()); } final int offsetPosition = mAdapterHelper.findPositionOffset(position);
          // 進行數據綁定 bound
= tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs); } final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); final LayoutParams rvLayoutParams;
       // 下面邏輯就是將 VH 綁定到 LP, LP 又設置到 ItemView 上
if (lp == null) { rvLayoutParams = (LayoutParams) generateDefaultLayoutParams(); holder.itemView.setLayoutParams(rvLayoutParams); } else if (!checkLayoutParams(lp)) { rvLayoutParams = (LayoutParams) generateLayoutParams(lp); holder.itemView.setLayoutParams(rvLayoutParams); } else { rvLayoutParams = (LayoutParams) lp; } rvLayoutParams.mViewHolder = holder; rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound; return holder; }

 即使省略了中間從緩存查找 VH 的邏輯,剩下部分的代碼還是很長。那我再概括下 tryGetViewHolderForPositionByDeadline 方法所做的事:

  1. 從緩存查找 VH ;

  2. 緩存沒有,那麼就創建一個 VH;

  3. 判斷 VH 需不需要更新數據,如果需要就會調用 tryBindViewHolderByDeadline 綁定數據;

  4. 將 VH 綁定到 LP, LP 又設置到 ItemView 上,互相依賴;

到這裏關於創建 VH 的邏輯就講完了。

緩存

在介紹添加到緩存的邏輯時,還是需要介紹緩存相關的類和變量。

緩存整體設計

由圖可知,RecyclerView 緩存是一個四級緩存的架構。當然,從 RecyclerView 的代碼註釋來看,官方認爲只有三級緩存,即mCachedViews是一級緩存,mViewCacheExtension 是二級緩存,mRecyclerPool 是三級緩存。從開發者的角度來看,mAttachedScrap 和 mChangedScrap 對開發者是不透明的,官方並未暴露出任何可以改變他們行爲的方法。

緩存機制 Recycler 詳解

Recycler 是 RecyclerView 的一個內部類。我們來看一下它的主要的成員變量。

  • mAttachedScrap 緩存屏幕中可見範圍的 ViewHolder

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

  • mChangedScrap 緩存滑動時即將與 RecyclerView 分離的ViewHolder,按子View的position或id緩存,默認最多存放2個

    ArrayList<ViewHolder> mChangedScrap = null;

  • mCachedViews  ViewHolder 緩存列表,其大小由 mViewCacheMax 決定,默認 DEFAULT_CACHE_SIZE 爲 2,可動態設置。

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

  • ViewCacheExtension 開發者可自定義的一層緩存,是虛擬類 ViewCacheExtension 的一個實例,開發者可實現方法 getViewForPositionAndType(Recycler recycler, int position, int type) 來實現自己的緩存。

    private ViewCacheExtension mViewCacheExtension;

  • RecycledViewPool ViewHolder 緩存池,在有限的 mCachedViews 中如果存不下 ViewHolder 時,就會把 ViewHolder 存入 RecyclerViewPool 中。

    RecycledViewPool mRecyclerPool; 

添加到緩存

VH 被創建之後,是要被緩存,然後重複利用的,那麼他們是什麼時候被添加到緩存的呢?此處還是以 LinearLayoutManager 舉例說明。在 RecyclerView 源碼分析(一) —— 繪製流程解析 一文中曾提到一個方法:

 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
     // ...
     detachAndScrapAttachedViews(recycler);  
     // ...
  }

 onLayoutChildren 是對子 view 進行繪製。在對子 view 會先調用 detachAndScrapAttachedViews 方法,下面來看看這個方法。

detachAndScrapAttachedViews

下面來看下這個方法:

       // recyclerview      
       public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
            final int childCount = getChildCount();
            for (int i = childCount - 1; i >= 0; i--) {
                final View v = getChildAt(i);
          // 每個 view 都會放到裏面 scrapOrRecycleView(recycler, i, v); } }
private void scrapOrRecycleView(Recycler recycler, int index, View view) { final ViewHolder viewHolder = getChildViewHolderInt(view); if (viewHolder.shouldIgnore()) { if (DEBUG) { Log.d(TAG, "ignoring view " + viewHolder); } return; }
        // 如果 VH 無效,並且已經被移除了,就會走另一個邏輯
if (viewHolder.isInvalid() && !viewHolder.isRemoved() && !mRecyclerView.mAdapter.hasStableIds()) { removeViewAt(index); recycler.recycleViewHolderInternal(viewHolder); } else {
          // 先 detch 掉,然後放入緩存中 detachViewAt(index); recycler.scrapView(view); mRecyclerView.mViewInfoStore.onViewDetached(viewHolder); } }

也就是在上面的邏輯裏,被放到緩存中。這裏就可以看到

  1. 如果是 remove,會執行 recycleViewHolderInternal(viewHolder) 方法,而這個方法最終會將 ViewHolder 加入 CacheView 和 Pool 中,

  2. 而當是 Detach,會將 View 加入到 ScrapViews 中

需要指出的一點是:需要區分兩個概念,Detach 和 Remove

  1. detach: 在 ViewGroup 中的實現很簡單,只是將 ChildView 從 ParentView 的 ChildView 數組中移除,ChildView 的 mParent 設置爲 null, 可以理解爲輕量級的臨時 remove, 因爲 View此時和 View 樹還是藕斷絲連, 這個函數被經常用來改變 ChildView 在 ChildView 數組中的次序。View 被 detach 一般是臨時的,在後面會被重新 attach。

  2. remove: 真正的移除,不光被從 ChildView 數組中除名,其他和 View 樹各項聯繫也會被徹底斬斷(不考慮 Animation/LayoutTransition 這種特殊情況), 比如焦點被清除,從TouchTarget 中被移除等。

recycleViewHolderInternal

下面來看 Recycler 兩個的具體邏輯方法:

        /**
         * 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) {
       // ...省略前面的代碼,前面都是在做檢驗
final boolean transientStatePreventsRecycling = holder .doesTransientStatePreventRecycling(); @SuppressWarnings("unchecked") final boolean forceRecycle = mAdapter != null && transientStatePreventsRecycling && mAdapter.onFailedToRecycleView(holder); boolean cached = false; boolean recycled = false; if (DEBUG && mCachedViews.contains(holder)) { throw new IllegalArgumentException("cached view received recycle internal? " + holder + exceptionLabel()); } if (forceRecycle || holder.isRecyclable()) { if (mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) { // Retire oldest cached view 如果緩存數量超了,就會先移除最先加入的 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; }
            // 添加到緩存 mCachedViews.add(targetCacheIndex, holder); cached
= true; } if (!cached) { addViewHolderToRecycledViewPool(holder, true); recycled = true; } } else { } // even if the holder is not removed, we still call this method so that it is removed // from view holder lists. mViewInfoStore.removeViewHolder(holder); if (!cached && !recycled && transientStatePreventsRecycling) { holder.mOwnerRecyclerView = null; } }

該方法所做的事具體如下:

  1. 檢驗該 VH 的有效性,確保已經不再被使用;

  2. 判斷緩存的容量,超了就會進行移除,然後找一個合適的位置進行添加。

  3. 如果不能加入到 CacheViews 中,則加入到 Pool 中。

mCachedViews

mCachedViews 對應的數據結構也是 ArrayList 但是該緩存對集合的大小是有限制的,默認是 2。該緩存中 ViewHolder 的特性和 mAttachedScrap 中的特性是一樣的,只要 position或者 itemId 對應上了,那麼它就是乾淨的,無需重新綁定數據。開發者可以調用 setItemViewCacheSize(size) 方法來改變緩存的大小。該層級緩存觸發的一個常見的場景是滑動 RV。當然 notifyXXX 也會觸發該緩存。該緩存和 mAttachedScrap 一樣特別高效。

RecyclerViewPool

RecyclerViewPool 緩存可以針對多ItemType,設置緩存大小。默認每個 ItemType 的緩存個數是 5。而且該緩存可以給多個 RecyclerView 共享。由於默認緩存個數爲 5,假設某個新聞 App,每屏幕可以展示 10 條新聞,那麼必然會導致緩存命中失敗,頻繁導致創建 ViewHolder 影響性能。所以需要擴大緩存size。

scrapView

接下去看 scrapView 這個方法:

        void scrapView(View view) {
            final ViewHolder holder = getChildViewHolderInt(view);
            if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                    || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
                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." + exceptionLabel());
                }
                holder.setScrapContainer(this, false); // 這裏的 false 
                mAttachedScrap.add(holder);
            } else {
                if (mChangedScrap == null) {
                    mChangedScrap = new ArrayList<ViewHolder>();
                }
                holder.setScrapContainer(this, true);  // 這裏是 true
                mChangedScrap.add(holder);
            }
        }

 

該方法就比較簡單了,沒有那麼多需要檢驗的邏輯。這裏根據條件,有兩種緩存類型可以選擇,具體就不展開了,大家都可以看懂。這裏講解下兩個 scrapView 的緩存。

mAttachedScrap

mAttachedScrap 的對應數據結構是ArrayList,在 LayoutManager#onLayoutChildren 方法中,對 views 進行佈局時,會將 RecyclerView 上的 Views 全部暫存到該集合中,以備後續使用,該緩存中的 ViewHolder 的特性是,如果和 RV 上的 position 或者 itemId 匹配上了,那麼認爲是乾淨的 ViewHolder,是可以直接拿出來使用的,無需調用 onBindViewHolder 方法。該 ArrayList 的大小是沒有限制的,屏幕上有多少個 View,就會創建多大的集合。

觸發該層級緩存的場景一般是調用 notifyItemXXX 方法。調用 notifyDataSetChanged 方法,只有當 Adapter hasStableIds 返回 true,會觸發該層級的緩存使用。

mChangedScrap

mChangedScrap 和 mAttachedScrap 是同一級的緩存,他們是平等的。但是mChangedScrap的調用場景是notifyItemChanged和notifyItemRangeChanged,只有發生變化的ViewHolder纔會放入到 mChangedScrap 中。mChangedScrap緩存中的ViewHolder是需要調用onBindViewHolder方法重新綁定數據的。那麼此時就有個問題了,爲什麼同一級別的緩存需要設計兩個不同的緩存?

在 dispatchLayoutStep2 階段 LayoutManager onLayoutChildren方法中最終會調用 layoutForPredictiveAnimations 方法,把 mAttachedScrap 中剩餘的 ViewHolder 填充到屏幕上,所以他們的區別就是,mChangedScrap 中的 ViewHolder 在 RV 填充滿的情況下,是不會強行填充到 RV 上的。那麼有辦法可以讓發生改變的 ViewHolder 進入 mAttachedScrap 緩存嗎?當然可以。調用 notifyItemChanged(int position, Object payload) 方法可以,實現局部刷新功能,payload 不爲空,那麼發生改變的 ViewHolder 是會被分離到 mAttachedScrap 中的。

使用緩存

下面進入到最後一節,使用緩存。這個在之前繪製篇幅也有提到,下面直接看對應的方法:

//根據傳入的position獲取ViewHolder 
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    ---------省略----------
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
    //預佈局 屬於特殊情況 從mChangedScrap中獲取ViewHolder
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    if (holder == null) {
        //1、嘗試從mAttachedScrap中獲取ViewHolder,此時獲取的是屏幕中可見範圍中的ViewHolder
        //2、mAttachedScrap緩存中沒有的話,繼續從mCachedViews嘗試獲取ViewHolder
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
     ----------省略----------
    }
    if (holder == null) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        ---------省略----------
        final int type = mAdapter.getItemViewType(offsetPosition);
        //如果Adapter中聲明瞭Id,嘗試從id中獲取,這裏不屬於緩存
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
        }
        if (holder == null && mViewCacheExtension != null) {
            3、從自定義緩存mViewCacheExtension中嘗試獲取ViewHolder,該緩存需要開發者實現
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
            }
        }
        if (holder == null) { // fallback to pool
            //4、從緩存池mRecyclerPool中嘗試獲取ViewHolder
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                //如果獲取成功,會重置ViewHolder狀態,所以需要重新執行Adapter#onBindViewHolder綁定數據
                holder.resetInternal();
                if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
                }
            }
        }
        if (holder == null) {
            ---------省略----------
          //5、若以上緩存中都沒有找到對應的ViewHolder,最終會調用Adapter中的onCreateViewHolder創建一個
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
        }
    }

    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        //6、如果需要綁定數據,會調用Adapter#onBindViewHolder來綁定數據
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
    ----------省略----------
    return holder;
}

上述邏輯用流程圖表示:

 

總結一下上述流程:通過 mAttachedScrap、mCachedViews 及 mViewCacheExtension 獲取的 ViewHolder 不需要重新創建佈局及綁定數據;通過緩存池 mRecyclerPool 獲取的 ViewHolder不需要重新創建佈局,但是需要重新綁定數據;如果上述緩存中都沒有獲取到目標 ViewHolder,那麼就會回調 Adapter#onCreateViewHolder 創建佈局,以及回調 Adapter#onBindViewHolder來綁定數據。

ViewCacheExtension

我們已經知道 ViewCacheExtension 屬於第三級緩存,需要開發者自行實現,那麼 ViewCacheExtension 在什麼場景下使用?又是如何實現的呢?

首先我們要明確一點,那就是 Recycler 本身已經設置了好幾級緩存了,爲什麼還要留個接口讓開發者去自行實現緩存呢?

關於這一點,來看看 Recycler 中的其他緩存:

  1. mAttachedScrap 用來處理可見屏幕的緩存;

  2. mCachedViews 裏存儲的數據雖然是根據 position 來緩存,但是裏面的數據隨時可能會被替換的;

  3. mRecyclerPool 裏按 viewType 去存儲 ArrayList< ViewHolder>,所以 mRecyclerPool 並不能按 position 去存儲 ViewHolder,而且從 mRecyclerPool 取出的 View 每次都要去走 Adapter#onBindViewHolder 去重新綁定數據。

假如我現在需要在一個特定的位置(比如 position=0 位置)一直展示某個 View,且裏面的內容是不變的,那麼最好的情況就是在特定位置時,既不需要每次重新創建 View,也不需要每次都去重新綁定數據,上面的幾種緩存顯然都是不適用的,這種情況該怎麼辦呢?可以通過自定義緩存 ViewCacheExtension 實現上述需求。 

RecyclerView & ListView緩存機制對比

結論援引自:Android ListView 與 RecyclerView 對比淺析--緩存機制

ListView和RecyclerView緩存機制基本一致:

  1. mActiveViews 和 mAttachedScrap 功能相似,意義在於快速重用屏幕上可見的列表項ItemView,而不需要重新 createView 和 bindView;

  2. mScrapView 和 mCachedViews + mReyclerViewPool功能相似,意義在於緩存離開屏幕的 ItemView,目的是讓即將進入屏幕的 ItemView 重用.

  3. RecyclerView 的優勢在於

    1. mCacheViews 的使用,可以做到屏幕外的列表項 ItemView 進入屏幕內時也無須bindView快速重用;

    2. mRecyclerPool 可以供多個 RecyclerView 共同使用,在特定場景下,如 viewpaper+ 多個列表頁下有優勢.客觀來說,RecyclerView 在特定場景下對 ListView 的緩存機制做了補強和完善。

不同使用場景:列表頁展示界面,需要支持動畫,或者頻繁更新,局部刷新,建議使用 RecyclerView,更加強大完善,易擴展;其它情況(如微信卡包列表頁)兩者都OK,但ListView在使用上會更加方便,快捷。

 

參考文章

https://www.jianshu.com/p/2b19e9bcda84

https://www.jianshu.com/p/6e6bf58b7f0d

https://www.jianshu.com/p/e1b257484961

RecyclerView加載了那麼多圖,爲什麼就是不崩呢?

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