RecyclerView 的複用機制

RecyclerView 是一個強大又靈活的 View,能夠用有限的 View 來展現大量的數據。今天咱們來看下 RecyclerView 內部是經過怎樣的緩存複用機制來實現這一功能的。java

Recycler

Recycler 是 RecyclerView 的內部類,也是這套複用機制的核心,顯然 Recycler 的主要成員變量也都是用來緩存和複用 ViewHolder 的:程序員

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

    final ArrayList<ViewHolder> mCachedViews = new ArrayList<>();
    
    RecycledViewPool mRecyclerPool;
    
    private ViewCacheExtension mViewCacheExtension;
}
複製代碼

這些緩存集合能夠分爲 4 個級別,按優先級從高到底爲:數組

  • 一級緩存:mAttachedScrap 和 mChangedScrap ,用來緩存還在屏幕內的 ViewHolder緩存

    • mAttachedScrap 存儲的是當前還在屏幕中的 ViewHolder;按照 id 和 position 來查找 ViewHolder
    • mChangedScrap 表示數據已經改變的 ViewHolder 列表, 存儲 notifyXXX 方法時須要改變的 ViewHolder
  • 二級緩存:mCachedViews ,用來緩存移除屏幕以外的 ViewHolder,默認狀況下緩存容量是 2,能夠經過 setViewCacheSize 方法來改變緩存的容量大小。若是 mCachedViews 的容量已滿,則會根據 FIFO 的規則移除舊 ViewHolderide

  • 三級緩存:ViewCacheExtension ,開發給用戶的自定義擴展緩存,須要用戶本身管理 View 的建立和緩存。我的感受這個拓展脫離了 Adapter.createViewHolder 使用的話會形成 View 建立和數據綁定和其它代碼太分散,不利於維護,使用場景不多僅作了解佈局

    /* * Note that, Recycler never sends Views to this method to be cached. It is developers * responsibility to decide whether they want to keep their Views in this custom cache or let * the default recycling policy handle it. */
    public abstract static class ViewCacheExtension {
    	public abstract View getViewForPositionAndType(...);
    }
    複製代碼
  • 四級緩存:RecycledViewPool ,ViewHolder 緩存池,在有限的 mCachedViews 中若是存不下新的 ViewHolder 時,就會把 ViewHolder 存入 RecyclerViewPool 中。動畫

    • 按照 Type 來查找 ViewHolder
    • 每一個 Type 默認最多緩存 5 個
    • 能夠多個 RecyclerView 共享 RecycledViewPool

接下來咱們看下這四級緩存是怎麼工做的this

複用

RecyclerView 做爲一個 「平平無奇」 的 View,子 View 的排列和佈局固然是從 onLayout 入手了,調用鏈:spa

RecyclerView.onLayout(...)
-> RecyclerView.dispatchLayout()    
-> RecyclerView.dispatchLayoutStep2() // do the actual layout of the views for the final state.
-> mLayout.onLayoutChildren(mRecycler, mState) // mLayout 類型爲 LayoutManager
-> LinearLayoutManager.onLayoutChildren(...) // 以 LinearLayoutManager 爲例
-> LinearLayoutManager.fill(...) // The magic functions :) 填充給定的佈局,註釋很自信的說這個方法很獨立,稍微改動就能做爲幫助類的一個公開方法,程序員的快樂就是這麼樸實無華。
-> LinearLayoutManager.layoutChunk(recycler, layoutState) // 循環調用,每次調用填充一個 ItemView 到 RV
-> LinearLayoutManager.LayoutState.next(recycler) 
-> RecyclerView.Recycler.getViewForPosition(int) // 回到主角了,經過 Recycler 獲取指定位置的 ItemView 
-> Recycler.getViewForPosition(int, boolean) // 調用下面方法獲取 ViewHolder,並返回上面須要的 viewHolder.itemView 
-> Recycler.tryGetViewHolderForPositionByDeadline(...) // 終於找到你,還好沒放棄~ 
複製代碼

能夠看出最終調用 tryGetViewHolderForPositionByDeadline ,來看看這個方法是怎麼拿到相應位置上的 ViewHolder :code

ViewHolder tryGetViewHolderForPositionByDeadline(int position, ...) {
    if (mState.isPreLayout()) {
        // 0) 預佈局從 mChangedScrap 裏面去獲取 ViewHolder,動畫相關
        holder = getChangedScrapViewForPosition(position);
    }
    
    if (holder == null) {
        // 1) 分別從 mAttachedScrap、 mHiddenViews、mCachedViews 獲取 ViewHolder
        // 這個 mHiddenViews 是用來作動畫期間的複用
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
    }
    
    if (holder == null) {
        final int type = mAdapter.getItemViewType(offsetPosition);
        // 2) 若是 Adapter 的 hasStableIds 方法返回爲 true
        // 優先經過 ViewType 和 ItemId 兩個條件從 mAttachedScrap 和 mCachedViews 尋找
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
        }
      
        if (holder == null && mViewCacheExtension != null) {
            // We are NOT sending the offsetPosition because LayoutManager does not know it.
            // 3) 從自定義緩存獲取,別問,問就是別用
            View view = mViewCacheExtension getViewForPositionAndType(this, position, type);
            holder = getChildViewHolder(view);
        }
    }
    
    if (holder == null) {
        // 4) 從 RecycledViewPool 獲取 ViewHolder
        holder = getRecycledViewPool().getRecycledView(type);
    }
  
    if (holder == null) {
        // 緩存全取過了,沒有,那隻好 create 一個咯
        holder = mAdapter.createViewHolder(RecyclerView.this, type);
    }
    
    // This is very ugly but the only place we can grab this information
    // 大半夜看到這句註釋的時候笑出聲!像極了我寫出醜代碼時的無奈。
    
    // 在這後面有一些刷新 ViewHolder 信息的代碼,放這很醜,但又只能放這,爲了能走到這,前面有多少 if (holder == null)
}
複製代碼

分析完複用的部分,接下來再看一下 ViewHolder 存入緩存的部分

緩存

所謂的緩存,就是看一下是怎麼樣往前面提到的四級緩存添加數據的

  • mAttachedScrap 和 mChangedScrap
  • mCachedViews
  • ViewCacheExtension 前面說了,這個的建立和緩存徹底由開發者本身控制,系統未往這裏添加數據
  • RecycledViewPool

mAttachedScrap 和 mChangedScrap

若是調用了 Adapter 的 notifyXXX 方法,會從新回調到 LayoutManager 的onLayoutChildren 方法裏面, 而在 onLayoutChildren 方法裏面,會將屏幕上全部的 ViewHolder 回收到 mAttachedScrap 和 mChangedScrap。

// 調用鏈 
   LinearLayoutManager.onLayoutChildren(...)
-> LayoutManager.detachAndScrapAttachedViews(recycler)
-> LayoutManager.scrapOrRecycleView(..., view)
-> Recycler.scrapView(view);  

private void scrapOrRecycleView(Recycler recycler, int index, View view) {
    final ViewHolder viewHolder = getChildViewHolderInt(view);
    if (viewHolder.isInvalid() && !viewHolder.isRemoved()
            && !mRecyclerView.mAdapter.hasStableIds()) {
        // 後面會講,緩存到 mCacheViews 和 RecyclerViewPool
        recycler.recycleViewHolderInternal(viewHolder);
    } else {
        // 緩存到 scrap
        recycler.scrapView(view); 
    }
}

void scrapView(View view) {
    final ViewHolder holder = getChildViewHolderInt(view);
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
        || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
        // 標記爲移除或失效的 || 徹底沒有改變 || item 無動畫或動畫不復用
        mAttachedScrap.add(holder);
    } else {
        mChangedScrap.add(holder);
    }
}
複製代碼

其實還有一種狀況會調用到 scrapView , 從 mHiddenViews 得到一個 ViewHolder 的話(發生在支持動畫的操做),會先將這個 ViewHolder 從 mHiddenViews 數組裏面移除,而後調用:

Recycler.tryGetViewHolderForPositionByDeadline(...)
-> Recycler.getScrapOrHiddenOrCachedHolderForPosition(...)
-> Recycler.scrapView(view)
複製代碼

mCacheViews 和 RecyclerViewPool

這兩級緩存的代碼都在 Recycler 的這個方法裏:

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) {
              // 1. mCacheViews 滿了,最先加入的不要了放 RecyclerViewPool
              recycleCachedViewAt(0); 
          }
           mCachedViews.add(targetCacheIndex, holder);
           cached = true;
        }
        
        if (!cached) { 
            // 2. 不能放進 mCacheViews 的放 RecyclerViewPool
            addViewHolderToRecycledViewPool(holder, true);
        }
    }   
}

// Recycles a cached view and removes the view from the list
void recycleCachedViewAt(int cachedViewIndex) {
    ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
    addViewHolderToRecycledViewPool(viewHolder, true);
    mCachedViews.remove(cachedViewIndex);
}
複製代碼

在這咱們知道 recycleViewHolderInternal 會把 ViewHolder 緩存到 mCacheViews ,而不知足加到 mCacheViews 的會緩存到 RecycledViewPool 。那又是何時調用的 recycleViewHolderInternal 呢?有如下三種狀況:

  1. 從新佈局,主要是調用 Adapter.notifyDataSetChange 且 Adapter 的 hasStableIds 方法返回爲 false 時調用。從這邊也能夠看出爲何通常狀況下 notifyDataSetChange 效率比其它 notifyXXX 方法低(使用二級緩存及優先級更低的緩存 ),同時也知道了,若是咱們設置 Adapter.setHasStableIds(ture) 以及其它相關須要的實現,則能夠提升效率(使用一級緩存)
  2. 在複用時,從一級緩存裏面獲取到 ViewHolder,可是此時這個 ViewHolder 已經不符合一級緩存的特色了(好比 Position 失效了,跟 ViewType 對不齊),就會從一級緩存裏面移除這個 ViewHolder,添加到這兩級緩存裏面
  3. 當調用 removeAnimatingView 方法時,若是當前 ViewHolder 被標記爲 remove ,會調用 recycleViewHolderInternal 方法來回收對應的 ViewHolder。調用 removeAnimatingView 方法的時機表示當前的 ItemAnimator 已經作完了

總結

到這裏,RecyclerView 的緩存複用機制就分析完了,總結一下:

  • RecyclerView 的緩存複用機制,主要是經過內部類 Recycler 來實現
  • Recycler 有 4 級緩存,每一級的緩存都有各自的做用,會按優先級使用。
  • ViewHolder 會從某一級緩存移至其它級別的緩存
  • mHideenViews 的存在是爲了解決在動畫期間進行復用的問題。
  • 緩存複用 ViewHolder 時會針對內部不一樣的狀態 ( mFlags ) 進行相應的處理。

本文源碼基於:recyclerview:1.2.0-alpha03

相關文章
相關標籤/搜索