RecyclerView的複用機制

本文是RecyclerView徹底解析系列第三篇文章,內容是緊跟前兩篇:RecyclerView基本設計結構RecyclerView刷新機制git

經過前面分析知道LayoutManager在佈局子View時會向Recycler索要一個ViewHolder。但從Recycler中獲取一個ViewHolder的前提是Recycler中要有ViewHolder。那Recycler中是如何有ViewHolder的呢? 本文會分析兩個問題:github

  1. RecyclerViewView是在何時放入到Recycler中的。以及在Recycler中是如何保存的。
  2. LayoutManager在向Recycler獲取ViewHolder時,Recycler尋找ViewHolder的邏輯是什麼。

什麼時候存、怎麼存什麼時候取、怎麼取的問題。什麼時候取已經很明顯了:LayoutManager在佈局子View時會從Recycler中獲取子View。 因此本文要理清的是其餘3個問題。在文章繼續以前要知道Recycler管理的基本單元是ViewHolderLayoutManager操做的基本單元是View,即ViewHolderitemview。本文不會分析RecyclerView動畫時view的複用邏輯。緩存

爲了接下來的內容更容易理解,先回顧一下Recycler的組成結構:bash

  • mChangedScrap : 用來保存RecyclerView作動畫時,被detach的ViewHolder
  • mAttachedScrap : 用來保存RecyclerView作數據刷新(notify),被detach的ViewHolder
  • mCacheViews : Recycler的一級ViewHolder緩存。
  • RecyclerViewPool : mCacheViews集合中裝滿時,會放到這裏。

先看一下如何從Recycler中取一個ViewHolder來複用。ide

從Recycler中獲取一個ViewHolder的邏輯

LayoutManager會調用Recycler.getViewForPosition(pos)來獲取一個指定位置(這個位置是子View佈局所在的位置)的viewgetViewForPosition()會調用tryGetViewHolderForPositionByDeadline(position...), 這個方法是從Recycler中獲取一個View的核心方法。它就是如何從Recycler中獲取一個ViewHolder的邏輯,即怎麼取, 方法太長, 我作了不少裁剪:佈局

ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
    ...
    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); //從 attach 和 mCacheViews 中獲取
        if (holder != null) {
            ... //校驗這個holder是否可用
        }
    }
    if (holder == null) {
        ...
        final int type = mAdapter.getItemViewType(offsetPosition); //獲取這個位置的數據的類型。  子Adapter複寫的方法
        // 2) Find from scrap/cache via stable ids, if exists
        if (mAdapter.hasStableIds()) {    //stable id 就是標識一個viewholder的惟一性, 即便它作動畫改變了位置
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),  //根據 stable id 從 scrap 和 mCacheViews中獲取
                    type, dryRun);
            ....
        }
        if (holder == null && mViewCacheExtension != null) { // 從用戶自定義的緩存集合中獲取
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);  //你返回的View要是RecyclerView.LayoutParams屬性的
            if (view != null) {
                holder = getChildViewHolder(view);  //把它包裝成一個ViewHolder
                ...
            }
        }
        if (holder == null) { // 從 RecyclerViewPool中獲取
            holder = getRecycledViewPool().getRecycledView(type);
            ...
        }
        if (holder == null) { 
            ...
            //實在沒有就會建立
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
            ...
        }
    }
    ...
    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) { //動畫時不會想去調用 onBindData
        ...
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        ...
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);  //調用 bindData 方法
    }

    final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
    final LayoutParams rvLayoutParams;
    ...調整LayoutParams
    return holder;
}

複製代碼

即大體步驟是:post

  1. 若是執行了RecyclerView動畫的話,嘗試根據positionmChangedScrap集合中尋找一個ViewHolder
  2. 嘗試根據positionscrap集合hide的view集合mCacheViews(一級緩存)中尋找一個ViewHolder
  3. 根據LayoutManagerposition更新到對應的Adapterposition。 (這兩個position在大部分狀況下都是相等的,不過在子view刪除或移動時可能產生不對應的狀況)
  4. 根據Adapter position,調用Adapter.getItemViewType()來獲取ViewType
  5. 根據stable id(用來表示ViewHolder的惟一,即便位置變化了)scrap集合mCacheViews(一級緩存)中尋找一個ViewHolder
  6. 根據position和viewType嘗試從用戶自定義的mViewCacheExtension中獲取一個ViewHolder
  7. 根據ViewType嘗試從RecyclerViewPool中獲取一個ViewHolder
  8. 調用mAdapter.createViewHolder()來建立一個ViewHolder
  9. 若是須要的話調用mAdapter.bindViewHolder來設置ViewHolder
  10. 調整ViewHolder.itemview的佈局參數爲Recycler.LayoutPrams,並返回Holder

雖然步驟不少,邏輯仍是很簡單的,即從幾個緩存集合中獲取ViewHolder,若是實在沒有就建立。但比較疑惑的可能就是上述ViewHolder緩存集合中何時會保存ViewHolder。接下來分幾個RecyclerView的具體情形,來一點一點弄明白這些ViewHolder緩存集合的問題。動畫

情形一 : 由無到有

即一開始RecyclerView中沒有任何數據,添加數據源後adapter.notifyXXX。狀態變化以下圖:ui

很明顯在這種情形下Recycler中是不會存在任何可複用的ViewHolder。因此全部的ViewHolder都是新建立的。即會調用Adapter.createViewHolder()和Adapter.bindViewHolder()。那這些建立的ViewHolder會緩存起來嗎?this

這時候新建立的這些ViewHolder是不會被緩存起來的。 即在這種情形下: Recycler只會經過Adapter建立ViewHolder,而且不會緩存這些新建立的ViewHolder

情形二 : 在原有數據的狀況下進行總體刷新

就是下面這種狀態:

其實就是至關於用戶在feed中作了下拉刷新。實現中的僞代碼以下:

dataSource.clear()
dataSource.addAll(newList)
adapter.notifyDatasetChanged()
複製代碼

在這種情形下猜測Recycler確定複用了老的卡片(卡片的類型不變),那麼問題是 : 在用戶刷新時舊ViewHolder保存在哪裏? 如何調用舊ViewHolderAdapter.bindViewHolder()來從新設置數據的?

其實在上一篇文章Recycler刷新機制中,LinearLayoutManager在肯定好佈局錨點View以後就會把當前attachRecyclerView上的子View所有設置爲scrap狀態:

void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...
    onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);  // RecyclerView指定錨點,要準備正式佈局了
    detachAndScrapAttachedViews(recycler);   // 在開始佈局時,把全部的View都設置爲 scrap 狀態
    ...
}
複製代碼

什麼是scrap狀態呢? 在前面的文章其實已經解釋過: ViewHolder被標記爲FLAG_TMP_DETACHED狀態,而且其itemviewparent被設置爲null

detachAndScrapAttachedViews就是把全部的view保存到RecyclermAttachedScrap集合中:

public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
    for (int i = getChildCount() - 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);
    ...刪去了一些判斷邏輯
    detachViewAt(index);  //設置RecyclerView這個位置的view的parent爲null, 並標記ViewHolder爲FLAG_TMP_DETACHED
    recycler.scrapView(view); //添加到mAttachedScrap集合中  
    ...
}
複製代碼

因此在這種情形下LinearLayoutManager在真正擺放子View以前,會把全部舊的子View按順序保存到RecyclermAttachedScrap集合

接下來繼續看,LinearLayoutManager在佈局時如何複用mAttachedScrap集合中的ViewHolder

前面已經說了LinearLayoutManager會當前佈局子View的位置向Recycler要一個子View,即調用到tryGetViewHolderForPositionByDeadline(position..)。咱們上面已經列出了這個方法的邏輯,其實在前面的第二步:

嘗試根據positionscrap集合hide的view集合mCacheViews(一級緩存)中尋找一個ViewHolder

即從mAttachedScrap中就能夠得到一個ViewHolder:

ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
    final int scrapCount = mAttachedScrap.size();
    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;
        }
    }
    ...
}
複製代碼

即若是mAttachedScrap中holder的位置和入參position相等,而且holder是有效的話這個holder就是能夠複用的。因此綜上所述,在情形二下全部的ViewHolder幾乎都是複用Recycler中mAttachedScrap集合中的。 而且從新佈局完畢後Recycler中是不存在可複用的ViewHolder的。

情形三 : 滾動複用

這個情形分析是在情形二的基礎上向下滑動時ViewHolder的複用狀況以及RecyclerViewHolder的保存狀況, 以下圖:

在這種狀況下滾出屏幕的View會優先保存到mCacheViews, 若是mCacheViews中保存滿了,就會保存到RecyclerViewPool中。

在前一篇文章RecyclerView刷新機制中分析過,RecyclerView在滑動時會調用LinearLayoutManager.fill()方法來根據滾動的距離來向RecyclerView填充子View,其實在個方法在填充完子View以後就會把滾動出屏幕的View作回收:

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {
    ...
    int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
    ...
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        ...
        layoutChunk(recycler, state, layoutState, layoutChunkResult); //填充一個子View

        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            recycleByLayoutState(recycler, layoutState); //根據滾動的距離來回收View
        }
    }
}
複製代碼

fill每填充一個子View都會調用recycleByLayoutState()來回收一個舊的子View,這個方法在層層調用以後會調用到Recycler.recycleViewHolderInternal()。這個方法是ViewHolder回收的核心方法,不過邏輯很簡單:

  1. 檢查mCacheViews集合中是否還有空位,若是有空位,則直接放到mCacheViews集合
  2. 若是沒有的話就把mCacheViews集合中最前面的ViewHolder拿出來放到RecyclerViewPool中,而後再把最新的這個ViewHolder放到mCacheViews集合
  3. 若是沒有成功緩存到mCacheViews集合中,就直接放到RecyclerViewPool

mCacheViews集合爲何要這樣緩存? 看一下下面這張圖 :

我是這樣認爲的,如上圖,往上滑動一段距離,被滑動出去的ViewHolder會被緩存在mCacheViews集合,而且位置是被記錄的。若是用戶此時再下滑的話,能夠參考文章開頭的從Recycler中獲取ViewHolder的邏輯:

  1. 先按照位置從mCacheViews集合中獲取
  2. 按照viewTypemCacheViews集合中獲取

上面對於mCacheViews集合兩步操做,其實第一步就已經命中了緩存的ViewHolder。而且這時候都不須要調用Adapter.bindViewHolder()方法的。便是十分高效的。

因此在普通的滾動複用的狀況下,ViewHolder的複用主要來自於mCacheViews集合, 舊的ViewHolder會被放到mCacheViews集合, mCacheViews集合擠出來的更老的ViewHolder放到了RecyclerViewPool

到這裏基本的複用情形都覆蓋了,其餘的就涉及到RecyclerView動畫了。這些點在下一篇文章繼續看。

歡迎關注個人Android進階計劃。看更多幹貨。

相關文章
相關標籤/搜索