RecyclerView回收和復(fù)用機(jī)制分析

開始

最近在研究 RecyclerView 的回收復(fù)用機(jī)制,順便記錄一下。我們知道,RecyclerView 在 layout 子 View 時(shí),都通過回收復(fù)用機(jī)制來管理。網(wǎng)上關(guān)于回收復(fù)用機(jī)制的分析講解的文章也有一大堆了,分析得也都很詳細(xì),什么四級(jí)緩存啊,先去 mChangedScrap 取再去哪里取啊之類的;但其實(shí),我想說的是,RecyclerView 的回收復(fù)用機(jī)制確實(shí)很完善,覆蓋到各種場景中,但并不是每種場景的回收復(fù)用時(shí)都會(huì)將機(jī)制的所有流程走一遍的。舉個(gè)例子說,在 setLayoutManager、setAdapter、notifyDataSetChanged 或者滑動(dòng)時(shí)等等這些場景都會(huì)觸發(fā)回收復(fù)用機(jī)制的工作。但是如果只是 RecyclerView 滑動(dòng)的場景觸發(fā)的回收復(fù)用機(jī)制工作時(shí),其實(shí)并不需要四級(jí)緩存都參與的。

問題

假設(shè)有一個(gè)20個(gè)item的RecyclerView,每五個(gè)占滿一個(gè)屏幕,在從頭滑到尾的過程中,onCreatViewHolder會(huì)調(diào)用多少次?

正題

RecyclerView 的回收復(fù)用機(jī)制的內(nèi)部實(shí)現(xiàn)都是由 Recycler 內(nèi)部類實(shí)現(xiàn),下面就都以這樣一種頁面的滑動(dòng)場景來講解 RecyclerView 的回收復(fù)用機(jī)制。

相應(yīng)的版本:

RecyclerView: recyclerview-v7-25.1.0.jar

LayoutManager: GridLayoutManager extends LinearLayoutManager (recyclerview-v7-25.1.0.jar)

這個(gè)頁面每行可顯示5個(gè)卡位,每個(gè)卡位的 item 布局 type 一致。開始分析回收復(fù)用機(jī)制之前,先提幾個(gè)問題:

Q1:如果向下滑動(dòng),新一行的5個(gè)卡位的顯示會(huì)去復(fù)用緩存的 ViewHolder,第一行的5個(gè)卡位會(huì)移出屏幕被回收,那么在這個(gè)過程中,是先進(jìn)行復(fù)用再回收?還是先回收再復(fù)用?還是邊回收邊復(fù)用?也就是說,新一行的5個(gè)卡位復(fù)用的 ViewHolder 有可能是第一行被回收的5個(gè)卡位嗎?
回答問題之前,先看幾張圖片:
先向下再向上滑動(dòng)


image

黑框表示屏幕,RecyclerView 先向下滑動(dòng),第三行卡位顯示出來,再向上滑動(dòng),第三行移出屏幕,第一行顯示出來。我們分別在 Adapter 的 onCreateViewHolder() 和 onBindViewHolder() 里打日志,下面是這個(gè)過程的日志:


image

紅框1是 RecyclerView 向下滑動(dòng)操作的日志,第三行5個(gè)卡位的顯示都是重新創(chuàng)建的 ViewHolder ;紅框2是再次向上滑動(dòng)時(shí)的日志,第一行5個(gè)卡位的重新顯示用的 ViewHolder 都是復(fù)用的,因?yàn)闆]有 create viewHolder 的日志,然后只有后面3個(gè)卡位重新綁定數(shù)據(jù),調(diào)用了onBindViewHolder();那么問題來了:

Q2: 在這個(gè)過程中,為什么當(dāng) RecyclerView 再次向上滑動(dòng)重新顯示第一行的5個(gè)卡位時(shí),只有后面3個(gè)卡位觸發(fā)了 onBindViewHolder() 方法,重新綁定數(shù)據(jù)呢?明明5個(gè)卡位都是復(fù)用的。

在上面的操作基礎(chǔ)上,我們繼續(xù)往下操作:先向下再向下

image

在第二個(gè)問題操作的基礎(chǔ)上,目前已經(jīng)創(chuàng)建了15個(gè) ViewHolder,此時(shí)顯示的是第1、2行的卡位,那么繼續(xù)向下滑動(dòng)兩次,這個(gè)過程的日志如下:


image

紅框1是第二個(gè)問題操作的日志,在這里截出來只是為了顯示接下去的日志是在上面的基礎(chǔ)上繼續(xù)操作的;

紅框2就是第一次向下滑時(shí)的日志,對(duì)比問題2的日志,這次第三行的5個(gè)卡位用的 ViewHolder 也都是復(fù)用的,而且也只有后面3個(gè)卡位觸發(fā)了 onBindViewHolder() 重新綁定數(shù)據(jù);

紅框3是第二次向下滑動(dòng)時(shí)的日志,這次第四行的5個(gè)卡位,前3個(gè)的卡位用的 ViewHolder 是復(fù)用的,后面2個(gè)卡位的 ViewHolder 則是重新創(chuàng)建的,而且5個(gè)卡位都調(diào)用了 onBindViewHolder() 重新綁定數(shù)據(jù);

Q3:接下去不管是向上滑動(dòng)還是向下滑動(dòng),滑動(dòng)幾次,都不會(huì)再有 onCreateViewHolder() 的日志了,也就是說 RecyclerView 總共創(chuàng)建了17個(gè) ViewHolder,但有時(shí)一行的5個(gè)卡位只有3個(gè)卡位需要重新綁定數(shù)據(jù),有時(shí)卻又5個(gè)卡位都需要重新綁定數(shù)據(jù),這是為什么呢?

如果明白 RecyclerView 的回收復(fù)用機(jī)制,那么這三個(gè)問題也就都知道原因了;反過來,如果知道這三個(gè)問題的原因,那么理解 RecyclerView 的回收復(fù)用機(jī)制也就更簡單了;所以,帶著問題,在特定的場景下去分析源碼的話,應(yīng)該會(huì)比較容易。

源碼分析

其實(shí),根據(jù)問題2的日志,我們就可以回答問題1了。在目前顯示1、2行,ViewHolder 的個(gè)數(shù)為10個(gè)的基礎(chǔ)上,第三行的5個(gè)新卡位要顯示出來都需要重新創(chuàng)建 ViewHolder,也就是說,在這個(gè)向下滑動(dòng)的過程,是5個(gè)新卡位的復(fù)用機(jī)制先進(jìn)行工作,然后第1行的5個(gè)被移出屏幕的卡位再進(jìn)行回收機(jī)制工作。那么,就先來看看復(fù)用機(jī)制的源碼。

復(fù)用機(jī)制

getViewForPosition()

//入口在這里 
public View getViewForPosition(int position) { 
    return getViewForPosition(position, false); 
} 

View getViewForPosition(int position, boolean dryRun) { 
    return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView; 
} 

ViewHolder tryGetViewHolderForPositionByDeadline(int position, 
            boolean dryRun, long deadlineNs) {  
    //復(fù)用機(jī)制工作原理都在這里 
    //... 
}

這個(gè)方法是復(fù)用機(jī)制的入口,也就是 Recycler 開放給外部使用復(fù)用機(jī)制的api,外部調(diào)用這個(gè)方法就可以返回想要的 View,而至于這個(gè) View 是復(fù)用而來的,還是重新創(chuàng)建得來的,就都由 Recycler 內(nèi)部實(shí)現(xiàn),對(duì)外隱藏。

tryGetViewHolderForPositionByDeadline()

所以,Recycler 的復(fù)用機(jī)制內(nèi)部實(shí)現(xiàn)就在這個(gè)方法里。分析邏輯之前,先看一下 Recycler 的幾個(gè)結(jié)構(gòu)體,用來緩存 ViewHolder 的。

 public final class Recycler { 
    final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>(); 
    ArrayList<ViewHolder> mChangedScrap = null; 
    //這個(gè)是本篇的重點(diǎn) 
    final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>(); 

    private final List<ViewHolder> 
            mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap); 

    private int mRequestedCacheMax = DEFAULT_CACHE_SIZE; 
    int mViewCacheMax = DEFAULT_CACHE_SIZE; 
    //這個(gè)也是本篇的重點(diǎn) 
    RecycledViewPool mRecyclerPool; 

    private ViewCacheExtension mViewCacheExtension; 

    static final int DEFAULT_CACHE_SIZE = 2; 
 }

mAttachedScrap:用于緩存顯示在屏幕上的 item 的 ViewHolder,場景好像是 RecyclerView 在 onLayout 時(shí)會(huì)先把 children 都移除掉,再重新添加進(jìn)去,所以這個(gè) List 應(yīng)該是用在布局過程中臨時(shí)存放 children 的,反正在 RecyclerView 滑動(dòng)過程中不會(huì)在這里面來找復(fù)用的 ViewHolder 就是了

mChangedScrap: 這個(gè)沒理解是干嘛用的,看名字應(yīng)該跟 ViewHolder 的數(shù)據(jù)發(fā)生變化時(shí)有關(guān)吧,在 RecyclerView 滑動(dòng)的過程中,也沒有發(fā)現(xiàn)到這里找復(fù)用的 ViewHolder,所以這個(gè)可以先暫時(shí)放一邊。

mCachedViews:這個(gè)就重要得多了,滑動(dòng)過程中的回收和復(fù)用都是先處理的這個(gè) List,這個(gè)集合里存的 ViewHolder 的原本數(shù)據(jù)信息都在,所以可以直接添加到 RecyclerView 中顯示,不需要再次重新 onBindViewHolder()。

mUnmodifiableAttachedScrap: 不清楚干嘛用的,暫時(shí)跳過。

mRecyclerPool:這個(gè)也很重要,但存在這里的 ViewHolder 的數(shù)據(jù)信息會(huì)被重置掉,相當(dāng)于 ViewHolder 是一個(gè)重創(chuàng)新建的一樣,所以需要重新調(diào)用 onBindViewHolder 來綁定數(shù)據(jù)。

mViewCacheExtension:這個(gè)是留給我們自己擴(kuò)展的,好像也沒怎么用,就暫時(shí)不分析了。

那么接下去就看看復(fù)用的邏輯:

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()); 
    } 
    //...省略代碼 
}
第一步很簡單,position 如果在 item 的范圍之外的話,那就拋異常吧。繼續(xù)往下看:

ViewHolder tryGetViewHolderForPositionByDeadline(int position, 
                boolean dryRun, long deadlineNs) { 
    //...省略看過的代碼 
    boolean fromScrapOrHiddenOrCache = false; 
    ViewHolder holder = null; 
    // 0) If there is a changed scrap, try to find from there 
    //上面是Google留的注釋,大意是...(emmm,這里我也沒理解) 
    if (mState.isPreLayout()) { 
        holder = getChangedScrapViewForPosition(position); 
        fromScrapOrHiddenOrCache = holder != null; 
    } 
}

如果是在 isPreLayout() 時(shí),那么就去 mChangedScrap 中找。那么這個(gè) isPreLayout 表示的是什么?共5有個(gè)賦值的地方。

//只顯示相關(guān)代碼,無關(guān)代碼省略 
protected void onMeasure(int widthSpec, int heightSpec) { 
    if (mLayout.mAutoMeasure) { 
        //... 
    } else { 
        // custom onMeasure 
        if (mAdapterUpdateDuringMeasure) { 
            if (mState.mRunPredictiveAnimations) { 
                mState.mInPreLayout = true; 
            } else { 
                // consume remaining updates to provide a consistent state with the layout pass. 
                mAdapterHelper.consumeUpdatesInOnePass(); 
                mState.mInPreLayout = false; 
            } 
        }  
    } 
    //... 
    mState.mInPreLayout = false; // clear 
} 

private void dispatchLayoutStep1() { 
    //... 
    mState.mInPreLayout = mState.mRunPredictiveAnimations; 
    //... 
} 

private void dispatchLayoutStep2() { 
    //... 
    mState.mInPreLayout = mState.mRunPredictiveAnimations; 
    mLayout.onLayoutChildren(mRecycler, mState); 
    //...
}

emmm,看樣子,在 LayoutManager 的 onLayoutChildren 前就會(huì)置為 false,不過我還是不懂這個(gè)過程是干嘛的,滑動(dòng)過程中好像 mState.mInPreLayou = false,所以并不會(huì)來這里,先暫時(shí)跳過,繼續(xù)往下。

ViewHolder tryGetViewHolderForPositionByDeadline(int position, 
                boolean dryRun, long deadlineNs) { 
    //...省略看過的代碼 
    // 1) Find by position from scrap/hidden list/cache 
    if (holder == null) { 
        //這里是第一次找可復(fù)用的ViewHolder了,得跟進(jìn)去看看 
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); 
        //... 
    } 
}

跟進(jìn)這個(gè)方法看看:

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

    // Try first for an exact, non-invalid match from scrap. 
    for (int i = 0; i < scrapCount; i++) { 
        //首先去mAttachedScrap中遍歷尋找,匹配條件也很多 
        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 中尋找 position 一致的 viewHolder,需要匹配一些條件,大致是這個(gè) viewHolder 沒有被移除,是有效的之類的條件,滿足就返回這個(gè) viewHolder。所以,這里的關(guān)鍵就是要理解這個(gè) mAttachedScrap 到底是什么,存的是哪些 ViewHolder。一次遙控器按鍵的操作,不管有沒有發(fā)生滑動(dòng),都會(huì)導(dǎo)致 RecyclerView 的重新 onLayout,那要 layout 的話,RecyclerView 會(huì)先把所有 children 先 remove 掉,然后再重新 add 上去,完成一次 layout 的過程。那么這暫時(shí)性的 remove 掉的 viewHolder 要存放在哪呢,就是放在這個(gè) mAttachedScrap 中了,這就是我的理解了。所以,感覺這個(gè) mAttachedScrap 中存放的 viewHolder 跟回收和復(fù)用關(guān)系不大。

網(wǎng)上一些分析的文章有說,RecyclerView 在復(fù)用時(shí)會(huì)按順序去 mChangedScrap, mAttachedScrap 等等緩存里找,沒有找到再往下去找,從代碼上來看是這樣沒錯(cuò),但我覺得這樣表述有問題。因?yàn)榫臀覀冞@篇文章基于 RecyclerView 的滑動(dòng)場景來說,新卡位的復(fù)用以及舊卡位的回收機(jī)制,其實(shí)都不會(huì)涉及到 mChangedScrap 和 mAttachedScrap,所以我覺得還是基于某種場景來分析相對(duì)應(yīng)的回收復(fù)用機(jī)制會(huì)比較好。就像 mChangedScrap 我雖然沒理解是干嘛用的,但我猜測應(yīng)該是在當(dāng)數(shù)據(jù)發(fā)生變化時(shí)才會(huì)涉及到的復(fù)用場景,所以當(dāng)我分析基于滑動(dòng)場景時(shí)的復(fù)用時(shí),即使我對(duì)這塊不理解,影響也不會(huì)很大。繼續(xù)向下看:

ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position,boolean dryRun) { 
    //...省略看過的代碼 
    if (!dryRun) {//dryRun一直為false 
        //這段代碼可看可不看 
        View view = mChildHelper.findHiddenNonRemovedView(position); 
        if (view != null) { 
            // This View is good to be used. We just need to unhide, detach and move to the 
            // scrap list. 
            final ViewHolder vh = getChildViewHolderInt(view); 
            mChildHelper.unhide(view); 
            int layoutIndex = mChildHelper.indexOfChild(view); 
            if (layoutIndex == RecyclerView.NO_POSITION) { 
                 throw new IllegalStateException("layout index should not be -1 after " 
                        + "unhiding a view:" + vh); 
            } 
            mChildHelper.detachViewFromParent(layoutIndex); 
            scrapView(view); 
            vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP 
                    | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST); 
            return vh; 
        } 
    } 
}

emmm,這段也還是沒看懂,但估計(jì)應(yīng)該需要一些特定的場景下所使用的復(fù)用策略吧,看名字,應(yīng)該跟 hidden 有關(guān)?不懂,跳過這段,應(yīng)該也沒事,滑動(dòng)過程中的回收復(fù)用跟這個(gè)應(yīng)該也關(guān)系不大。

ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position,boolean dryRun) { 
    //...省略看過的代碼 
    // Search in our first-level recycled view cache. 
    //下面就是重點(diǎn)了,去mCachedViews里遍歷 
    final int cacheSize = mCachedViews.size(); 
    for (int i = 0; i < cacheSize; i++) { 
        final ViewHolder holder = mCachedViews.get(i); 
        // invalid view holders may be in cache if adapter has stable ids as they can be 
        // retrieved via getScrapOrCachedViewForId 
        // 上面的大意是即使是失效的holser也有可能可以拿來復(fù)用,但需要我們重寫adapter的setHasStadleId并且提供一個(gè)id時(shí),在getScrapOrCachedViewForId()里就可以再去mCachedViews里找一遍。   
        if (!holder.isInvalid() && holder.getLayoutPosition() == position) { 
            if (!dryRun) { //dryRun一直為false 
                mCachedViews.remove(i);//所以,如果position匹配,那么就將這個(gè)ViewHolder移除mCachedViews 
            } 
            if (DEBUG) { 
                Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position 
                        + ") found match in cache: " + holder); 
            } 
            return holder; 

    } 
    return null; 
}

這里就要畫重點(diǎn)啦,記筆記記筆記,滑動(dòng)場景中的復(fù)用會(huì)用到這里的機(jī)制。mCachedViews 的大小默認(rèn)為2。遍歷 mCachedViews,找到 position 一致的 ViewHolder,之前說過,mCachedViews 里存放的 ViewHolder 的數(shù)據(jù)信息都保存著,所以 mCachedViews 可以理解成,只有原來的卡位可以重新復(fù)用這個(gè) ViewHolder,新位置的卡位無法從 mCachedViews 里拿 ViewHolder出來用。 找到 viewholder 后:

ViewHolder tryGetViewHolderForPositionByDeadline(int position, 
                boolean dryRun, long deadlineNs) { 
    //...省略看過的代碼 
    // 1) Find by position from scrap/hidden list/cache 
    if (holder == null) { 
        //這里是第一次找可復(fù)用的ViewHolder了,得跟進(jìn)去看看 
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); 
        //之前分析跟進(jìn)了上面那個(gè)方法,找到ViewHolder后 
        if (holder != null) { 
            //需要再次驗(yàn)證一下這個(gè)ViewHodler是否可以拿來復(fù)用 
            if (!validateViewHolderForOffsetPosition(holder)) { 
                // recycle holder (and unscrap if relevant) since it can't be used 
                if (!dryRun) { 
                    // we would like to recycle this but need to make sure it is not used by 
                    // animation logic etc. 
                    //如果不能復(fù)用,就把它要么仍到mAttachedScrap或者扔到ViewPool里 
                    holder.addFlags(ViewHolder.FLAG_INVALID); 
                    if (holder.isScrap()) { 
                        removeDetachedView(holder.itemView, false); 
                        holder.unScrap(); 
                    } else if (holder.wasReturnedFromScrap()) { 
                        holder.clearReturnedFromScrapFlag(); 
                    } 
                    recycleViewHolderInternal(holder); 
                } 
                holder = null; 
            } else { 
                fromScrapOrHiddenOrCache = true; 
            } 
        } 
    } 
}

就算 position 匹配找到了 ViewHolder,還需要判斷一下這個(gè) ViewHolder 是否已經(jīng)被 remove 掉,type 類型一致不一致,如下:

boolean validateViewHolderForOffsetPosition(ViewHolder holder) { 
    // if it is a removed holder, nothing to verify since we cannot ask adapter anymore 
    // if it is not removed, verify the type and id. 
    if (holder.isRemoved()) { 
        if (DEBUG && !mState.isPreLayout()) { 
            throw new IllegalStateException("should not receive a removed view unless it" 
                    + " is pre layout"); 
        } 
        return mState.isPreLayout(); 
    } 
    if (holder.mPosition < 0 || holder.mPosition >= mAdapter.getItemCount()) { 
        throw new IndexOutOfBoundsException("Inconsistency detected. Invalid view holder " 
                + "adapter position" + holder); 
    } 
    //如果type類型不一樣,那就不能復(fù)用 
    if (!mState.isPreLayout()) { 
        // don't check type if it is pre-layout. 
        final int type = mAdapter.getItemViewType(holder.mPosition); 
        if (type != holder.getItemViewType()) { 
            return false; 
        } 
    } 
    if (mAdapter.hasStableIds()) { 
        return holder.getItemId() == mAdapter.getItemId(holder.mPosition); 
    } 
    return true;     
}

以上是在 mCachedViews 中尋找,沒有找到的話,就繼續(xù)再找一遍,剛才是通過 position 來找,那這次就換成id,然后重復(fù)上面的步驟再找一遍,如下:

ViewHolder tryGetViewHolderForPositionByDeadline(int position, 
                boolean dryRun, long deadlineNs) { 
    //...省略看過的代碼 
    if (holder == null) { 
        final int offsetPosition = mAdapterHelper.findPositionOffset(position); 
        if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) { 
            throw new IndexOutOfBoundsException("http://省略..."); 
        } 

        final int type = mAdapter.getItemViewType(offsetPosition); 
        // 2) Find from scrap/cache via stable ids, if exists 
        if (mAdapter.hasStableIds()) {//如果有設(shè)置stableIs,就再從Scrap和cached里根據(jù)id找一次 
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), 
                   type, dryRun); 
            if (holder != null) { 
                // update position 
                holder.mPosition = offsetPosition; 
                fromScrapOrHiddenOrCache = true; 
            } 
        } 
        //省略之后步驟,后續(xù)分析... 
    } 
}

getScrapOrCachedViewForId() 做的事跟 getScrapOrHiddenOrCacheHolderForPosition() 其實(shí)差不多,只不過一個(gè)是通過 position 來找 ViewHolder,一個(gè)是通過 id 來找。而這個(gè) id 并不是我們在 xml 中設(shè)置的 android:id, 而是 Adapter 持有的一個(gè)屬性,默認(rèn)是不會(huì)使用這個(gè)屬性的,所以這里其實(shí)是不會(huì)執(zhí)行的,除非我們重寫了 Adapter 的 setHasStableIds(),既然不是常用的場景,那就先略過吧,那就繼續(xù)往下。

ViewHolder tryGetViewHolderForPositionByDeadline(int position, 
                boolean dryRun, long deadlineNs) { 
    //...省略看過的代碼 
    if (holder == null) { 
        final int offsetPosition = mAdapterHelper.findPositionOffset(position); 
        //省略無關(guān)代碼... 
        final int type = mAdapter.getItemViewType(offsetPosition); 
        //省略上述步驟跟getScrapOrCachedViewForId()相關(guān)的代碼... 
        //這里開始就又去另一個(gè)地方找了,ViewCacheExtension 
        if (holder == null && mViewCacheExtension != null) { 
            // We are NOT sending the offsetPosition because LayoutManager does not 
            // know it. 
            final View view = mViewCacheExtension 
                    .getViewForPositionAndType(this, position, type); 
            if (view != null) { 
                holder = getChildViewHolder(view); 
                if (holder == null) { 
                    throw new IllegalArgumentException("getViewForPositionAndType returned" + " a view which does not have a ViewHolder"); 
                } else if (holder.shouldIgnore()) { 
                    throw new IllegalArgumentException("getViewForPositionAndType returned" + " a view that is ignored. You must call stopIgnoring before" + " returning this view."); 
                } 
            } 
        } 
        //省略之后步驟,后續(xù)分析... 
    } 
}

這個(gè)就是常說擴(kuò)展類了,RecyclerView 提供給我們自定義實(shí)現(xiàn)的擴(kuò)展類,我們可以重寫 getViewForPositionAndType() 方法來實(shí)現(xiàn)自己的復(fù)用策略。不過,也沒用過,那這部分也當(dāng)作不會(huì)執(zhí)行,略過。繼續(xù)往下:

ViewHolder tryGetViewHolderForPositionByDeadline(int position, 
                boolean dryRun, long deadlineNs) { 
    //...省略看過的代碼 
    if (holder == null) { 
        final int offsetPosition = mAdapterHelper.findPositionOffset(position); 
        //省略無關(guān)代碼... 
        final int type = mAdapter.getItemViewType(offsetPosition); 
        //省略看過的的代碼... 
        //這里開始就又去另一個(gè)地方找了,RecycledViewPool 
        if (holder == null) { // fallback to pool 
            if (DEBUG) { 
                Log.d(TAG, "tryGetViewHolderForPositionByDeadline("+ position + ") fetching from shared pool"); 
            } 
            //跟進(jìn)這個(gè)方法看看 
            holder = getRecycledViewPool().getRecycledView(type); 
            if (holder != null) { 
                //如果在ViewPool里找到可復(fù)用的ViewHolder,那就重置ViewHolder的數(shù)據(jù),這樣ViewHolder就可以當(dāng)作全新的來使用了 
                holder.resetInternal(); 
                if (FORCE_INVALIDATE_DISPLAY_LIST) { 
                    invalidateDisplayListInt(holder); 
                } 
            } 
        } 
        //省略之后步驟,后續(xù)分析... 
    } 
}

這里也是重點(diǎn)了,記筆記記筆記。這里是去 RecyclerViewPool 里取 ViewHolder,ViewPool 會(huì)根據(jù)不同的 item type 創(chuàng)建不同的 List,每個(gè) List 默認(rèn)大小為5個(gè)??匆幌氯?ViewPool 里是怎么找的:

public ViewHolder getRecycledView(int viewType) { 
    //根據(jù)type,只要不為空,就將最后一個(gè)ViewHolder移出來復(fù)用 
    final ScrapData scrapData = mScrap.get(viewType); 
    if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) { 
        final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap; 
        return scrapHeap.remove(scrapHeap.size() - 1); 
    } 
    return null; 
}

之前說過,ViewPool 會(huì)根據(jù)不同的 viewType 創(chuàng)建不同的集合來存放 ViewHolder,那么復(fù)用的時(shí)候,只要 ViewPool 里相同的 type 有 ViewHolder 緩存的話,就將最后一個(gè)拿出來復(fù)用,不用像 mCachedViews 需要各種匹配條件,只要有就可以復(fù)用。拿到 ViewHolder 之后,還會(huì)再次調(diào)用 resetInternal() 來重置 ViewHolder,這樣 ViewHolder 就可以當(dāng)作一個(gè)全新的 ViewHolder 來使用了,這也就是為什么從這里拿的 ViewHolder 都需要重新 onBindViewHolder() 了。那如果在 ViewPool 里還是沒有找到呢,繼續(xù)往下看:

ViewHolder tryGetViewHolderForPositionByDeadline(int position, 
                boolean dryRun, long deadlineNs) { 
    //...省略看過的代碼 
    if (holder == null) { 
        final int offsetPosition = mAdapterHelper.findPositionOffset(position); 
        //省略無關(guān)代碼... 
        final int type = mAdapter.getItemViewType(offsetPosition); 
        //省略看過的的代碼... 
        //都沒找到的話,就調(diào)用Adapter.onCreateAdapter()來新建一個(gè)ViewHolder了 
        if (holder == null) { 
            //省略無關(guān)代碼... 
            holder = mAdapter.createViewHolder(RecyclerView.this, type);//新建一個(gè)ViewHolder 
            //省略無關(guān)代碼... 
        } 
    } 
    //省略之后步驟,后續(xù)分析 
}

如果 ViewPool 中都沒有找到 ViewHolder 來使用的話,那就調(diào)用 Adapter 的 onCreateViewHolder 來創(chuàng)建一個(gè)新的 ViewHolder 使用。上面一共有很多步驟來找 ViewHolder,不管在哪個(gè)步驟,只要找到 ViewHolder 的話,那下面那些步驟就不用管了,然后都要繼續(xù)往下判斷是否需要重新綁定數(shù)據(jù),還有檢查布局參數(shù)是否合法。如下:

 ViewHolder tryGetViewHolderForPositionByDeadline(int position,  
                boolean dryRun, long deadlineNs) {  
    //...省略上述分析的找ViewHolder的代碼...  
    //代碼執(zhí)行到這里,ViewHolder肯定不為Null了,因?yàn)榫退阍诟鱾€(gè)緩存里沒找到,最后一步也會(huì)重新創(chuàng)建一個(gè)  
    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);  
        }  
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);  
        //調(diào)用Adapter.onBindViewHolder()來重新綁定數(shù)據(jù)  
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);  
    }  
    //下面是驗(yàn)證itemView的布局參數(shù)是否可用,并設(shè)置可用的布局參數(shù)  
    final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();  
    final LayoutParams rvLayoutParams;  
    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;  
    //結(jié)束  
}

到這里,tryGetViewHolderForPositionByDeadline() 這個(gè)方法就結(jié)束了。這大概就是 RecyclerView 的復(fù)用機(jī)制,中間我們跳過很多地方,因?yàn)?RecyclerView 有各種場景可以刷新他的 view,比如重新 setLayoutManager(),重新 setAdapter(),或者 notifyDataSetChanged(),或者滑動(dòng)等等之類的場景,只要重新layout,就會(huì)去回收和復(fù)用 ViewHolder,所以這個(gè)復(fù)用機(jī)制需要考慮到各種各樣的場景。把代碼一行行的啃透有點(diǎn)吃力,所以我就只借助 RecyclerView 的滑動(dòng)的這種場景來分析它涉及到的回收和復(fù)用機(jī)制。下面就分析一下回收機(jī)制 。

回收機(jī)制

回收機(jī)制的入口就有很多了,因?yàn)?Recycler 有各種結(jié)構(gòu)體,比如mAttachedScrap,mCachedViews 等等,不同結(jié)構(gòu)體回收的時(shí)機(jī)都不一樣,入口也就多了。所以,還是基于 RecyclerView 的滑動(dòng)場景下,移出屏幕的卡位回收時(shí)的入口是:

//回收入口之一 
public void recycleView(View view) { 
    // This public recycle method tries to make view recycle-able since layout manager 
    // intended to recycle this view (e.g. even if it is in scrap or change cache) 
    ViewHolder holder = getChildViewHolderInt(view); 
    if (holder.isTmpDetached()) { 
        removeDetachedView(view, false); 
    } 
    if (holder.isScrap()) { 
        holder.unScrap(); 
    } else if (holder.wasReturnedFromScrap()){ 
        holder.clearReturnedFromScrapFlag(); 
    } 
    //回收的內(nèi)部實(shí)現(xiàn),跟進(jìn)看看 
    recycleViewHolderInternal(holder); 
}

本篇分析的滑動(dòng)場景,在 RecyclerView 滑動(dòng)時(shí),會(huì)交由 LinearLayoutManager 的 scrollVerticallyBy() 去處理,然后 LayoutManager 會(huì)接著調(diào)用 fill() 方法去處理需要復(fù)用和回收的卡位,最終會(huì)調(diào)用上述 recyclerView() 這個(gè)方法開始進(jìn)行回收工作。

void recycleViewHolderInternal(ViewHolder holder) { 
    //省略代碼... 
    if (forceRecycle || holder.isRecyclable()) { 
        //mViewCacheMax大小默認(rèn)為2 
        if (mViewCacheMax > 0 /*省略其他條件*/) { 
            // Retire oldest cached view 
            int cachedViewSize = mCachedViews.size(); 
            //回收時(shí),先將ViewHolder緩存在mCachedViews里,如果滿了,調(diào)用recycleCachedViewAt(0)移除一個(gè),好空出位置來 
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) { 
                recycleCachedViewAt(0); 
                cachedViewSize--; 
            } 

            //省略無關(guān)代碼... 

            //將最近剛剛回收的ViewHolder放在mCachedViews里 
            mCachedViews.add(targetCacheIndex, holder); 
            cached = true; 
        } 
        if (!cached) { 
            //如果設(shè)置不用mCachedViewd緩存的話,那回收時(shí)就扔進(jìn)ViewPool里等待復(fù)用 
            addViewHolderToRecycledViewPool(holder, true); 
            recycled = true; 
        } 
    }  
    //省略無關(guān)代碼... 
}

跟進(jìn) recycleCachedViewAt(0) 方法看看:

void recycleCachedViewAt(int cachedViewIndex) { 
    if (DEBUG) { 
        Log.d(TAG, "Recycling cached view at index " + cachedViewIndex); 
    } 
    ViewHolder viewHolder = mCachedViews.get(cachedViewIndex); 
    if (DEBUG) { 
        Log.d(TAG, "CachedViewHolder to be recycled: " + viewHolder); 
    } 
    //將mCachedViews里緩存的ViewHolder取出來,扔進(jìn)ViewPool里緩存 
    addViewHolderToRecycledViewPool(viewHolder, true); 
    mCachedViews.remove(cachedViewIndex); 
}
繼續(xù)跟進(jìn) addViewHolderToRecycledViewPool() 里看看,這個(gè)方法在上上代碼塊里也出現(xiàn) 

void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) { 
    clearNestedRecyclerViewIfNotNested(holder); 
    ViewCompat.setAccessibilityDelegate(holder.itemView, null); 
    if (dispatchRecycled) { 
        //這個(gè)方法會(huì)去回調(diào)Adapter里的onViewRecycle(),所以Adapter接收到該回調(diào)時(shí)是ViewHolder被扔進(jìn)ViewPool里才會(huì)觸發(fā)的 
        //如果ViewHolder只是被mCachedViews緩存了,那Adapter的onViewRecycle()是不會(huì)回調(diào)的,所以不是所有被移出屏幕的item都會(huì)觸發(fā)onViewRecycle()方法的 
        dispatchViewRecycled(holder); 
    } 
    holder.mOwnerRecyclerView = null 
    //在扔進(jìn)ViewPool前回調(diào)一些方法,并對(duì)ViewHolder的一些標(biāo)志置位,然后繼續(xù)跟進(jìn)看看 
    getRecycledViewPool().putRecycledView(holder); 
}

在 ViewHolder 扔進(jìn) ViewPool 里之前,會(huì)先去回調(diào) Adapter 里的 onViewRecycle(),所以 Adapter 接收到該回調(diào)時(shí)是 ViewHolder 被扔進(jìn) ViewPool 里才會(huì)觸發(fā)的。如果 ViewHolder 只是被 mCachedViews 緩存了,那 Adapter 的 onViewRecycle() 是不會(huì)回調(diào)的,所以不是所有被移出屏幕的 item 都會(huì)觸發(fā) onViewRecycle() 方法的,這點(diǎn)需要注意一下。繼續(xù)跟進(jìn)看看 :

public void putRecycledView(ViewHolder scrap) { 
    final int viewType = scrap.getItemViewType(); 
    final ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap; 
    if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) { 
        //如果ViewPool滿了,就不緩存了,默認(rèn)大小為5 
        return; 
    } 
    if (DEBUG && scrapHeap.contains(scrap)) { 
        throw new IllegalArgumentException("this scrap item already exists"); 
    } 
    //緩存前先將ViewHolder的信息重置,這樣ViewHolder下次被拿出來復(fù)用時(shí)就可以當(dāng)作全新的ViewHolder來使用了 
    scrap.resetInternal(); 
    scrapHeap.add(scrap); 
}

所以,ViewHolder 在扔進(jìn) ViewPool 前會(huì)先 reset,這里的重置指的是 ViewHolder 保存的一些信息,比如 position,跟它綁定的 RecycleView 啊之類的,并不會(huì)清空 itemView,所以復(fù)用時(shí)才會(huì)經(jīng)常出現(xiàn) itemView 顯示之前卡位的圖片信息之類的情況,這點(diǎn)需要區(qū)分一下。

回收的邏輯比較簡單,由 LayoutManager 來遍歷移出屏幕的卡位,然后對(duì)每個(gè)卡位進(jìn)行回收操作,回收時(shí),都是把 ViewHolder 放在 mCachedViews 里面,如果 mCachedViews 滿了,那就在 mCachedViews 里拿一個(gè) ViewHolder 扔到 ViewPool 緩存里,然后 mCachedViews 就可以空出位置來放新回收的 ViewHolder 了。

總結(jié)

RecyclerView 滑動(dòng)場景下的回收復(fù)用涉及到的結(jié)構(gòu)體兩個(gè):mCachedViews 和 RecyclerViewPool。

mCachedViews 優(yōu)先級(jí)高于 RecyclerViewPool,回收時(shí),最新的 ViewHolder 都是往 mCachedViews 里放,如果它滿了,那就移出一個(gè)扔到 ViewPool 里好空出位置來緩存最新的 ViewHolder。

復(fù)用時(shí),也是先到 mCachedViews 里找 ViewHolder,但需要各種匹配條件,概括一下就是只有原來位置的卡位可以復(fù)用存在 mCachedViews 里的 ViewHolder,如果 mCachedViews 里沒有,那么才去 ViewPool 里找。

在 ViewPool 里的 ViewHolder 都是跟全新的 ViewHolder 一樣,只要 type 一樣,有找到,就可以拿出來復(fù)用,重新綁定下數(shù)據(jù)即可。

整體的流程圖如下:

最后,解釋一下開頭的問題

Q1:如果向下滑動(dòng),新一行的5個(gè)卡位的顯示會(huì)去復(fù)用緩存的 ViewHolder,第一行的5個(gè)卡位會(huì)移出屏幕被回收,那么在這個(gè)過程中,是先進(jìn)行復(fù)用再回收?還是先回收再復(fù)用?還是邊回收邊復(fù)用?也就是說,新一行的5個(gè)卡位復(fù)用的 ViewHolder 有可能是第一行被回收的5個(gè)卡位嗎?

答:先復(fù)用再回收,新一行的5個(gè)卡位先去目前的 mCachedViews 和 ViewPool 的緩存中尋找復(fù)用,沒有就重新創(chuàng)建,然后移出屏幕的那行的5個(gè)卡位再回收緩存到 mCachedViews 和 ViewPool 里面,所以新一行5個(gè)卡位和復(fù)用不可能會(huì)用到剛移出屏幕的5個(gè)卡位。

Q2: 在這個(gè)過程中,為什么當(dāng) RecyclerView 再次向上滑動(dòng)重新顯示第一行的5個(gè)卡位時(shí),只有后面3個(gè)卡位觸發(fā)了 onBindViewHolder() 方法,重新綁定數(shù)據(jù)呢?明明5個(gè)卡位都是復(fù)用的。

答:滑動(dòng)場景下涉及到的回收和復(fù)用的結(jié)構(gòu)體是 mCachedViews 和 ViewPool,前者默認(rèn)大小為2,后者為5。所以,當(dāng)?shù)谌酗@示出來后,第一行的5個(gè)卡位被回收,回收時(shí)先緩存在 mCachedViews,滿了再移出舊的到 ViewPool 里,所有5個(gè)卡位有2個(gè)緩存在 mCachedViews 里,3個(gè)緩存在 ViewPool,至于是哪2個(gè)緩存在 mCachedViews,這是由 LayoutManager 控制。上面講解的例子使用的是 GridLayoutManager,滑動(dòng)時(shí)的回收邏輯則是在父類 LinearLayoutManager 里實(shí)現(xiàn),回收第一行卡位時(shí)是從后往前回收,所以最新的兩個(gè)卡位是0、1,會(huì)放在 mCachedViews 里,而2、3、4的卡位則放在 ViewPool 里。

所以,當(dāng)再次向上滑動(dòng)時(shí),第一行5個(gè)卡位會(huì)去兩個(gè)結(jié)構(gòu)體里找復(fù)用,之前說過,mCachedViews 里存放的 ViewHolder 只有原本位置的卡位才能復(fù)用,所以0、1兩個(gè)卡位都可以直接去 mCachedViews 里拿 ViewHolder 復(fù)用,而且這里的 ViewHolder 是不用重新綁定數(shù)據(jù)的,至于2、3、4卡位則去 ViewPool 里找,剛好 ViewPool 里緩存著3個(gè) ViewHolder,所以第一行的5個(gè)卡位都是用的復(fù)用的,而從 ViewPool 里拿的復(fù)用需要重新綁定數(shù)據(jù),才會(huì)這樣只有三個(gè)卡位需要重新綁定數(shù)據(jù)。

Q3:接下去不管是向上滑動(dòng)還是向下滑動(dòng),滑動(dòng)幾次,都不會(huì)再有 onCreateViewHolder() 的日志了,也就是說 RecyclerView 總共創(chuàng)建了17個(gè) ViewHolder,但有時(shí)一行的5個(gè)卡位只有3個(gè)卡位需要重新綁定數(shù)據(jù),有時(shí)卻又5個(gè)卡位都需要重新綁定數(shù)據(jù),這是為什么呢?

答:有時(shí)一行只有3個(gè)卡位需要重新綁定的原因跟Q2一樣,因?yàn)?mCachedView 里正好緩存著當(dāng)前位置的 ViewHolder,本來就是它的 ViewHolder 當(dāng)然可以直接拿來用。而至于為什么會(huì)創(chuàng)建了17個(gè) ViewHolder,那是因?yàn)樵俚谒男械目ㄎ灰@示出來時(shí),ViewPool 里只有3個(gè)緩存,而第四行的卡位又用不了 mCachedViews 里的2個(gè)緩存,因?yàn)檫@兩個(gè)緩存的是6、7卡位的 ViewHolder,所以就需要再重新創(chuàng)建2個(gè) ViewHodler 來給第四行最后的兩個(gè)卡位使用。

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

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

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