RecyclerView復(fù)用機制

上一篇文章分析RecyclerView刷新機制知道LayoutManager在布局子View時會向Recycler索要一個ViewHolder。但從Recycler中獲取一個ViewHolder的前提是Recycler中要有ViewHolder。那Recycler中是如何有ViewHolder的呢?
本文會分析兩個問題:

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

何時存、怎么存何時取、怎么取的問題。何時取已經(jīng)很明顯了:LayoutManager在布局子View時會從Recycler中獲取子View。 所以本文要理清的是其他3個問題。在文章繼續(xù)之前要知道Recycler管理的基本單元是ViewHolder,LayoutManager操作的基本單元是View,即ViewHolderitemview。本文不會分析RecyclerView動畫時view的復(fù)用邏輯。

為了接下來的內(nèi)容更容易理解,先回顧一下Recycler的組成結(jié)構(gòu):

Recycler的組成.png
  • mChangedScrap : 用來保存RecyclerView做動畫時,被detach的ViewHolder
  • mAttachedScrap : 用來保存RecyclerView做數(shù)據(jù)刷新(notify),被detach的ViewHolder
  • mCacheViews : Recycler的一級ViewHolder緩存。
  • RecyclerViewPool : mCacheViews集合中裝滿時,會放到這里。

先看一下如何從Recycler中取一個ViewHolder來復(fù)用。

從Recycler中獲取一個ViewHolder的邏輯

LayoutManager會調(diào)用Recycler.getViewForPosition(pos)來獲取一個指定位置(這個位置是子View布局所在的位置)的view。getViewForPosition()會調(diào)用tryGetViewHolderForPositionByDeadline(position...), 這個方法是從Recycler中獲取一個View的核心方法。它就是如何從Recycler中獲取一個ViewHolder的邏輯,即怎么取。方法太長, 我做了很多裁剪:

ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
    ...
    if (mState.isPreLayout()) {     //動畫相關(guān)
        holder = getChangedScrapViewForPosition(position);  //從緩存中拿嗎?不應(yīng)該不是緩存?
        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); //獲取這個位置的數(shù)據(jù)的類型。  子Adapter復(fù)寫的方法
        // 2) Find from scrap/cache via stable ids, if exists
        if (mAdapter.hasStableIds()) {    //stable id 就是標(biāo)識一個viewholder的唯一性, 即使它做動畫改變了位置
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),  //根據(jù) 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) { 
            ...
            //實在沒有就會創(chuàng)建
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
            ...
        }
    }
    ...
    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) { //動畫時不會想去調(diào)用 onBindData
        ...
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        ...
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);  //調(diào)用 bindData 方法
    }

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

即大致步驟是:

  1. 如果執(zhí)行了RecyclerView動畫的話,嘗試根據(jù)positionmChangedScrap集合中尋找一個ViewHolder
  2. 嘗試根據(jù)positionscrap集合、hide的view集合、mCacheViews(一級緩存)中尋找一個ViewHolder
  3. 根據(jù)LayoutManagerposition更新到對應(yīng)的Adapterposition。 (這兩個position在大部分情況下都是相等的,不過在子view刪除或移動時可能產(chǎn)生不對應(yīng)的情況)
  4. 根據(jù)Adapter position,調(diào)用Adapter.getItemViewType()來獲取ViewType
  5. 根據(jù)stable id(用來表示ViewHolder的唯一,即使位置變化了)scrap集合mCacheViews(一級緩存)中尋找一個ViewHolder
  6. 根據(jù)position和viewType嘗試從用戶自定義的mViewCacheExtension中獲取一個ViewHolder
  7. 根據(jù)ViewType嘗試從RecyclerViewPool中獲取一個ViewHolder
  8. 調(diào)用mAdapter.createViewHolder()來創(chuàng)建一個ViewHolder
  9. 如果需要的話調(diào)用mAdapter.bindViewHolder來設(shè)置ViewHolder。
  10. 調(diào)整ViewHolder.itemview的布局參數(shù)為Recycler.LayoutPrams,并返回Holder

雖然步驟很多,邏輯還是很簡單的,即從幾個緩存集合中獲取ViewHolder,如果實在沒有就創(chuàng)建。但比較疑惑的可能就是上述ViewHolder緩存集合中什么時候會保存ViewHolder。接下來分幾個RecyclerView的具體情形,來一點一點弄明白這些ViewHolder緩存集合的問題。

情形一 : 由無到有

即一開始RecyclerView中沒有任何數(shù)據(jù),添加數(shù)據(jù)源后adapter.notifyXXX。狀態(tài)變化如下圖:

State由無到有.png

很明顯在這種情形下Recycler中是不會存在任何可復(fù)用的ViewHolder。所以所有的ViewHolder都是新創(chuàng)建的。即會調(diào)用Adapter.createViewHolder()和Adapter.bindViewHolder()。那這些創(chuàng)建的ViewHolder會緩存起來嗎?

這時候新創(chuàng)建的這些ViewHolder是不會被緩存起來的。 即在這種情形下: Recycler只會通過Adapter創(chuàng)建ViewHolder,并且不會緩存這些新創(chuàng)建的ViewHolder

情形二 : 在原有數(shù)據(jù)的情況下進(jìn)行整體刷新

就是下面這種狀態(tài):

State由有到有.png

其實就是相當(dāng)于用戶在feed中做了下拉刷新。實現(xiàn)中的偽代碼如下:

dataSource.clear()
dataSource.addAll(newList)
adapter.notifyDatasetChanged()

在這種情形下猜想Recycler肯定復(fù)用了老的卡片(卡片的類型不變),那么問題是 : 在用戶刷新時舊ViewHolder保存在哪里? 如何調(diào)用舊ViewHolderAdapter.bindViewHolder()來重新設(shè)置數(shù)據(jù)的?

其實在上一篇文章Recycler刷新機制中,LinearLayoutManager在確定好布局錨點View之后就會把當(dāng)前attachRecyclerView上的子View全部設(shè)置為scrap狀態(tài):

void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...
    onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);  // RecyclerView指定錨點,要準(zhǔn)備正式布局了
    detachAndScrapAttachedViews(recycler);   // 在開始布局時,把所有的View都設(shè)置為 scrap 狀態(tài)
    ...
}

什么是scrap狀態(tài)呢? 在前面的文章其實已經(jīng)解釋過: ViewHolder被標(biāo)記為FLAG_TMP_DETACHED狀態(tài),并且其itemviewparent被設(shè)置為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);  //設(shè)置RecyclerView這個位置的view的parent為null, 并標(biāo)記ViewHolder為FLAG_TMP_DETACHED
    recycler.scrapView(view); //添加到mAttachedScrap集合中  
    ...
}

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

接下來繼續(xù)看,LinearLayoutManager在布局時如何復(fù)用mAttachedScrap集合中的ViewHolder。

前面已經(jīng)說了LinearLayoutManager會當(dāng)前布局子View的位置向Recycler要一個子View,即調(diào)用到tryGetViewHolderForPositionByDeadline(position..)。我們上面已經(jīng)列出了這個方法的邏輯,其實在前面的第二步:

嘗試根據(jù)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的位置和入?yún)osition相等,并且holder是有效的話這個holder就是可以復(fù)用的。所以綜上所述,在情形二下所有的ViewHolder幾乎都是復(fù)用Recycler中mAttachedScrap集合中的。
并且重新布局完畢后Recycler中是不存在可復(fù)用的ViewHolder的。

情形三 : 滾動復(fù)用

這個情形分析是在情形二的基礎(chǔ)上向下滑動時ViewHolder的復(fù)用情況以及RecyclerViewHolder的保存情況, 如下圖:

State滾動復(fù)用.png

在這種情況下滾出屏幕的View會優(yōu)先保存到mCacheViews, 如果mCacheViews中保存滿了,就會保存到RecyclerViewPool中。

在前一篇文章RecyclerView刷新機制中分析過,RecyclerView在滑動時會調(diào)用LinearLayoutManager.fill()方法來根據(jù)滾動的距離來向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); //根據(jù)滾動的距離來回收View
        }
    }
}

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

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

mCacheViews集合為什么要這樣緩存? 看一下下面這張圖 :

mCacheViews的緩存邏輯.png

我是這樣認(rèn)為的,如上圖,往上滑動一段距離,被滑動出去的ViewHolder會被緩存在mCacheViews集合,并且位置是被記錄的。如果用戶此時再下滑的話,可以參考文章開頭的從Recycler中獲取ViewHolder的邏輯:

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

上面對于mCacheViews集合兩步操作,其實第一步就已經(jīng)命中了緩存的ViewHolder。并且這時候都不需要調(diào)用Adapter.bindViewHolder()方法的。即是十分高效的。

所以在普通的滾動復(fù)用的情況下,ViewHolder的復(fù)用主要來自于mCacheViews集合, 舊的ViewHolder會被放到mCacheViews集合, mCacheViews集合擠出來的更老的ViewHolder放到了RecyclerViewPool

到這里基本的復(fù)用情形都覆蓋了,其他的就涉及到RecyclerView動畫了。這些點在下一篇文章繼續(xù)看。

歡迎關(guān)注我的Android進(jìn)階計劃??锤喔韶?/p>

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

相關(guān)閱讀更多精彩內(nèi)容

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