RecyclerView回收和復用機制最全分析

最近庄萎,看見一篇RecyclerView 的回收復用機制的文章驳棱,也很理解這篇文章,所以暫且存下來哆窿。

開始

最近在研究 RecyclerView 的回收復用機制链烈,順便記錄一下。我們知道挚躯,RecyclerView 在 layout 子 View 時强衡,都通過回收復用機制來管理。
網(wǎng)上關(guān)于回收復用機制的分析講解的文章也有一大堆了码荔,分析得也都很詳細漩勤,什么四級緩存啊,先去 mChangedScrap 取再去哪里取啊之類的缩搅;但其實越败,我想說的是,RecyclerView 的回收復用機制確實很完善硼瓣,覆蓋到各種場景中究飞,但并不是每種場景的回收復用時都會將機制的所有流程走一遍的。
舉個例子說堂鲤,在 setLayoutManager亿傅、setAdapter、notifyDataSetChanged 或者滑動時等等這些場景都會觸發(fā)回收復用機制的工作瘟栖。但是如果只是 RecyclerView 滑動的場景觸發(fā)的回收復用機制工作時葵擎,其實并不需要四級緩存都參與的。

問題

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

正題

RecyclerView 的回收復用機制的內(nèi)部實現(xiàn)都是由 Recycler 內(nèi)部類實現(xiàn)镜沽,下面就都以這樣一種頁面的滑動場景來講解 RecyclerView 的回收復用機制敏晤。

相應的版本:

RecyclerView: recyclerview-v7-25.1.0.jar

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

這個頁面每行可顯示5個卡位,每個卡位的 item 布局 type 一致缅茉。開始分析回收復用機制之前嘴脾,先提幾個問題:

Q1:如果向下滑動,新一行的5個卡位的顯示會去復用緩存的 ViewHolder蔬墩,第一行的5個卡位會移出屏幕被回收译打,那么在這個過程中,是先進行復用再回收拇颅?還是先回收再復用奏司?還是邊回收邊復用?也就是說樟插,新一行的5個卡位復用的 ViewHolder 有可能是第一行被回收的5個卡位嗎韵洋?

回答問題之前竿刁,先看幾張圖片:
先向下再向上滑動


image.png

黑框表示屏幕,RecyclerView 先向下滑動搪缨,第三行卡位顯示出來食拜,再向上滑動,第三行移出屏幕副编,第一行顯示出來负甸。我們分別在 Adapter 的 onCreateViewHolder() 和 onBindViewHolder() 里打日志,下面是這個過程的日志:

image.png

紅框1是 RecyclerView 向下滑動操作的日志痹届,第三行5個卡位的顯示都是重新創(chuàng)建的 ViewHolder 呻待;紅框2是再次向上滑動時的日志,第一行5個卡位的重新顯示用的 ViewHolder 都是復用的队腐,因為沒有 create viewHolder 的日志蚕捉,然后只有后面3個卡位重新綁定數(shù)據(jù),調(diào)用了onBindViewHolder()香到;那么問題來了:

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

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

image.png

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

image.png

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

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

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

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

如果明白 RecyclerView 的回收復用機制序愚,那么這三個問題也就都知道原因了憔披;反過來,如果知道這三個問題的原因爸吮,那么理解 RecyclerView 的回收復用機制也就更簡單了芬膝;所以,帶著問題形娇,在特定的場景下去分析源碼的話锰霜,應該會比較容易。

源碼分析

其實桐早,根據(jù)問題2的日志癣缅,我們就可以回答問題1了。在目前顯示1哄酝、2行友存,ViewHolder 的個數(shù)為10個的基礎(chǔ)上,第三行的5個新卡位要顯示出來都需要重新創(chuàng)建 ViewHolder陶衅,也就是說屡立,在這個向下滑動的過程,是5個新卡位的復用機制先進行工作搀军,然后第1行的5個被移出屏幕的卡位再進行回收機制工作膨俐。那么,就先來看看復用機制的源碼罩句。

復用機制

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) {  
    //復用機制工作原理都在這里 
    //... 
}

這個方法是復用機制的入口焚刺,也就是 Recycler 開放給外部使用復用機制的api,外部調(diào)用這個方法就可以返回想要的 View门烂,而至于這個 View 是復用而來的乳愉,還是重新創(chuàng)建得來的,就都由 Recycler 內(nèi)部實現(xiàn)诅福,對外隱藏匾委。

tryGetViewHolderForPositionByDeadline()
所以,Recycler 的復用機制內(nèi)部實現(xiàn)就在這個方法里氓润。分析邏輯之前赂乐,先看一下 Recycler 的幾個結(jié)構(gòu)體,用來緩存 ViewHolder 的咖气。

 public final class Recycler { 
    final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>(); 
    ArrayList<ViewHolder> mChangedScrap = null; 
    //這個是本篇的重點 
    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; 
    //這個也是本篇的重點 
    RecycledViewPool mRecyclerPool; 

    private ViewCacheExtension mViewCacheExtension; 

    static final int DEFAULT_CACHE_SIZE = 2; 
 }
  • mAttachedScrap:用于緩存顯示在屏幕上的 item 的 ViewHolder挨措,場景好像是 RecyclerView 在 onLayout 時會先把 children 都移除掉挖滤,再重新添加進去,所以這個 List 應該是用在布局過程中臨時存放 children 的浅役,反正在 RecyclerView 滑動過程中不會在這里面來找復用的 ViewHolder 就是了

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

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

mUnmodifiableAttachedScrap: 不清楚干嘛用的,暫時跳過希柿。

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

那么接下去就看看復用的邏輯:

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() 時,那么就去 mChangedScrap 中找剩膘。那么這個 isPreLayout 表示的是什么衅斩?共5有個賦值的地方。

//只顯示相關(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 前就會置為 false奈懒,不過我還是不懂這個過程是干嘛的奠涌,滑動過程中好像 mState.mInPreLayou = false,所以并不會來這里磷杏,先暫時跳過,繼續(xù)往下溜畅。

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

跟進這個方法看看:

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() &amp;&amp; holder.getLayoutPosition() == position 
                &amp;&amp; !holder.isInvalid() &amp;&amp; (mState.mInPreLayout || !holder.isRemoved())) { 
            holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP); 
            return holder; 
        } 
    } 
    //

首先慈格,去 mAttachedScrap 中尋找 position 一致的 viewHolder怠晴,需要匹配一些條件,大致是這個 viewHolder 沒有被移除浴捆,是有效的之類的條件蒜田,滿足就返回這個 viewHolder。所以选泻,這里的關(guān)鍵就是要理解這個 mAttachedScrap 到底是什么冲粤,存的是哪些 ViewHolder。一次遙控器按鍵的操作页眯,不管有沒有發(fā)生滑動色解,都會導致 RecyclerView 的重新 onLayout,那要 layout 的話餐茵,RecyclerView 會先把所有 children 先 remove 掉科阎,然后再重新 add 上去,完成一次 layout 的過程忿族。那么這暫時性的 remove 掉的 viewHolder 要存放在哪呢锣笨,就是放在這個 mAttachedScrap 中了,這就是我的理解了道批。所以错英,感覺這個 mAttachedScrap 中存放的 viewHolder 跟回收和復用關(guān)系不大。

網(wǎng)上一些分析的文章有說隆豹,RecyclerView 在復用時會按順序去 mChangedScrap, mAttachedScrap 等等緩存里找椭岩,沒有找到再往下去找,從代碼上來看是這樣沒錯璃赡,但我覺得這樣表述有問題判哥。因為就我們這篇文章基于 RecyclerView 的滑動場景來說,新卡位的復用以及舊卡位的回收機制碉考,其實都不會涉及到 mChangedScrap 和 mAttachedScrap塌计,所以我覺得還是基于某種場景來分析相對應的回收復用機制會比較好。就像 mChangedScrap 我雖然沒理解是干嘛用的侯谁,但我猜測應該是在當數(shù)據(jù)發(fā)生變化時才會涉及到的復用場景锌仅,所以當我分析基于滑動場景時的復用時,即使我對這塊不理解墙贱,影響也不會很大热芹。繼續(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,這段也還是沒看懂惨撇,但估計應該需要一些特定的場景下所使用的復用策略吧伊脓,看名字,應該跟 hidden 有關(guān)串纺?不懂丽旅,跳過這段椰棘,應該也沒事,滑動過程中的回收復用跟這個應該也關(guān)系不大榄笙。

ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position,boolean dryRun) { 
    //...省略看過的代碼 
    // Search in our first-level recycled view cache. 
    //下面就是重點了邪狞,去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也有可能可以拿來復用,但需要我們重寫adapter的setHasStadleId并且提供一個id時茅撞,在getScrapOrCachedViewForId()里就可以再去mCachedViews里找一遍帆卓。   
        if (!holder.isInvalid() &amp;&amp; holder.getLayoutPosition() == position) { 
            if (!dryRun) { //dryRun一直為false 
                mCachedViews.remove(i);//所以,如果position匹配米丘,那么就將這個ViewHolder移除mCachedViews 
            } 
            if (DEBUG) { 
                Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position 
                        + ") found match in cache: " + holder); 
            } 
            return holder; 

    } 
    return null; 
}

這里就要畫重點啦剑令,記筆記記筆記,滑動場景中的復用會用到這里的機制拄查。mCachedViews 的大小默認為2吁津。遍歷 mCachedViews,找到 position 一致的 ViewHolder堕扶,之前說過碍脏,mCachedViews 里存放的 ViewHolder 的數(shù)據(jù)信息都保存著,所以 mCachedViews 可以理解成稍算,只有原來的卡位可以重新復用這個 ViewHolder典尾,新位置的卡位無法從 mCachedViews 里拿 ViewHolder出來用。 找到 viewholder 后:

ViewHolder tryGetViewHolderForPositionByDeadline(int position, 
                boolean dryRun, long deadlineNs) { 
    //...省略看過的代碼 
    // 1) Find by position from scrap/hidden list/cache 
    if (holder == null) { 
        //這里是第一次找可復用的ViewHolder了糊探,得跟進去看看 
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); 
        //之前分析跟進了上面那個方法钾埂,找到ViewHolder后 
        if (holder != null) { 
            //需要再次驗證一下這個ViewHodler是否可以拿來復用 
            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. 
                    //如果不能復用,就把它要么仍到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科平,還需要判斷一下這個 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 &amp;&amp; !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類型不一樣匠抗,那就不能復用 
    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印机,然后重復上面的步驟再找一遍矢腻,如下:

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() 其實差不多多柑,只不過一個是通過 position 來找 ViewHolder,一個是通過 id 來找楣责。而這個 id 并不是我們在 xml 中設(shè)置的 android:id竣灌, 而是 Adapter 持有的一個屬性聂沙,默認是不會使用這個屬性的,所以這里其實是不會執(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)的代碼... 
        //這里開始就又去另一個地方找了,ViewCacheExtension 
        if (holder == null &amp;&amp; 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ù)分析... 
    } 
}

這個就是常說擴展類了温眉,RecyclerView 提供給我們自定義實現(xiàn)的擴展類,我們可以重寫 getViewForPositionAndType() 方法來實現(xiàn)自己的復用策略翁狐。不過类溢,也沒用過,那這部分也當作不會執(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); 
        //省略看過的的代碼... 
        //這里開始就又去另一個地方找了,RecycledViewPool 
        if (holder == null) { // fallback to pool 
            if (DEBUG) { 
                Log.d(TAG, "tryGetViewHolderForPositionByDeadline("+ position + ") fetching from shared pool"); 
            } 
            //跟進這個方法看看 
            holder = getRecycledViewPool().getRecycledView(type); 
            if (holder != null) { 
                //如果在ViewPool里找到可復用的ViewHolder隐锭,那就重置ViewHolder的數(shù)據(jù)窃躲,這樣ViewHolder就可以當作全新的來使用了 
                holder.resetInternal(); 
                if (FORCE_INVALIDATE_DISPLAY_LIST) { 
                    invalidateDisplayListInt(holder); 
                } 
            } 
        } 
        //省略之后步驟,后續(xù)分析... 
    } 
}

這里也是重點了钦睡,記筆記記筆記蒂窒。這里是去 RecyclerViewPool 里取 ViewHolder,ViewPool 會根據(jù)不同的 item type 創(chuàng)建不同的 List荞怒,每個 List 默認大小為5個洒琢。看一下去 ViewPool 里是怎么找的:

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

之前說過,ViewPool 會根據(jù)不同的 viewType 創(chuàng)建不同的集合來存放 ViewHolder荧嵌,那么復用的時候呛踊,只要 ViewPool 里相同的 type 有 ViewHolder 緩存的話,就將最后一個拿出來復用啦撮,不用像 mCachedViews 需要各種匹配條件谭网,只要有就可以復用。拿到 ViewHolder 之后赃春,還會再次調(diào)用 resetInternal() 來重置 ViewHolder愉择,這樣 ViewHolder 就可以當作一個全新的 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()來新建一個ViewHolder了 
        if (holder == null) { 
            //省略無關(guān)代碼... 
            holder = mAdapter.createViewHolder(RecyclerView.this, type);//新建一個ViewHolder 
            //省略無關(guān)代碼... 
        } 
    } 
    //省略之后步驟,后續(xù)分析 
}

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

 ViewHolder tryGetViewHolderForPositionByDeadline(int position,  
                boolean dryRun, long deadlineNs) {  
    //...省略上述分析的找ViewHolder的代碼...  
    //代碼執(zhí)行到這里,ViewHolder肯定不為Null了尺栖,因為就算在各個緩存里沒找到嫡纠,最后一步也會重新創(chuàng)建一個  
    boolean bound = false;  
    if (mState.isPreLayout() &amp;&amp; holder.isBound()) {  
        // do not update unless we absolutely have to.  
        holder.mPreLayoutPosition = position;  
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {  
        if (DEBUG &amp;&amp; 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);  
    }  
    //下面是驗證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 &amp;&amp; bound;  
    return holder;  
    //結(jié)束  
}

到這里延赌,tryGetViewHolderForPositionByDeadline() 這個方法就結(jié)束了除盏。這大概就是 RecyclerView 的復用機制,中間我們跳過很多地方挫以,因為 RecyclerView 有各種場景可以刷新他的 view者蠕,比如重新 setLayoutManager(),重新 setAdapter()掐松,或者 notifyDataSetChanged()踱侣,或者滑動等等之類的場景,只要重新layout大磺,就會去回收和復用 ViewHolder抡句,所以這個復用機制需要考慮到各種各樣的場景。把代碼一行行的啃透有點吃力杠愧,所以我就只借助 RecyclerView 的滑動的這種場景來分析它涉及到的回收和復用機制待榔。下面就分析一下回收機制 。

回收機制

回收機制的入口就有很多了流济,因為 Recycler 有各種結(jié)構(gòu)體锐锣,比如mAttachedScrap,mCachedViews 等等绳瘟,不同結(jié)構(gòu)體回收的時機都不一樣刺下,入口也就多了。所以稽荧,還是基于 RecyclerView 的滑動場景下,移出屏幕的卡位回收時的入口是:

//回收入口之一 
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)部實現(xiàn),跟進看看 
    recycleViewHolderInternal(holder); 
}

本篇分析的滑動場景姨丈,在 RecyclerView 滑動時畅卓,會交由 LinearLayoutManager 的 scrollVerticallyBy() 去處理,然后 LayoutManager 會接著調(diào)用 fill() 方法去處理需要復用和回收的卡位蟋恬,最終會調(diào)用上述 recyclerView() 這個方法開始進行回收工作翁潘。

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

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

            //將最近剛剛回收的ViewHolder放在mCachedViews里 
            mCachedViews.add(targetCacheIndex, holder); 
            cached = true; 
        } 
        if (!cached) { 
            //如果設(shè)置不用mCachedViewd緩存的話沐绒,那回收時就扔進ViewPool里等待復用 
            addViewHolderToRecycledViewPool(holder, true); 
            recycled = true; 
        } 
    }  
    //省略無關(guā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取出來,扔進ViewPool里緩存 
    addViewHolderToRecycledViewPool(viewHolder, true); 
    mCachedViews.remove(cachedViewIndex); 
}
繼續(xù)跟進 addViewHolderToRecycledViewPool() 里看看,這個方法在上上代碼塊里也出現(xiàn) 

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

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

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

所以雀摘,ViewHolder 在扔進 ViewPool 前會先 reset裸删,這里的重置指的是 ViewHolder 保存的一些信息,比如 position阵赠,跟它綁定的 RecycleView 啊之類的涯塔,并不會清空 itemView,所以復用時才會經(jīng)常出現(xiàn) itemView 顯示之前卡位的圖片信息之類的情況清蚀,這點需要區(qū)分一下匕荸。

回收的邏輯比較簡單,由 LayoutManager 來遍歷移出屏幕的卡位枷邪,然后對每個卡位進行回收操作榛搔,回收時,都是把 ViewHolder 放在 mCachedViews 里面,如果 mCachedViews 滿了践惑,那就在 mCachedViews 里拿一個 ViewHolder 扔到 ViewPool 緩存里腹泌,然后 mCachedViews 就可以空出位置來放新回收的 ViewHolder 了。

總結(jié)

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

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

復用時钉稍,也是先到 mCachedViews 里找 ViewHolder涤躲,但需要各種匹配條件,概括一下就是只有原來位置的卡位可以復用存在 mCachedViews 里的 ViewHolder嫁盲,如果 mCachedViews 里沒有篓叶,那么才去 ViewPool 里找。

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

整體的流程圖如下:

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

Q1:如果向下滑動哺哼,新一行的5個卡位的顯示會去復用緩存的 ViewHolder佩抹,第一行的5個卡位會移出屏幕被回收,那么在這個過程中取董,是先進行復用再回收棍苹?還是先回收再復用?還是邊回收邊復用茵汰?也就是說枢里,新一行的5個卡位復用的 ViewHolder 有可能是第一行被回收的5個卡位嗎?

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

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

答:滑動場景下涉及到的回收和復用的結(jié)構(gòu)體是 mCachedViews 和 ViewPool桑寨,前者默認大小為2伏尼,后者為5。所以尉尾,當?shù)谌酗@示出來后,第一行的5個卡位被回收燥透,回收時先緩存在 mCachedViews沙咏,滿了再移出舊的到 ViewPool 里,所有5個卡位有2個緩存在 mCachedViews 里班套,3個緩存在 ViewPool肢藐,至于是哪2個緩存在 mCachedViews,這是由 LayoutManager 控制吱韭。上面講解的例子使用的是 GridLayoutManager吆豹,滑動時的回收邏輯則是在父類 LinearLayoutManager 里實現(xiàn),回收第一行卡位時是從后往前回收理盆,所以最新的兩個卡位是0痘煤、1,會放在 mCachedViews 里猿规,而2衷快、3、4的卡位則放在 ViewPool 里姨俩。

所以蘸拔,當再次向上滑動時,第一行5個卡位會去兩個結(jié)構(gòu)體里找復用环葵,之前說過调窍,mCachedViews 里存放的 ViewHolder 只有原本位置的卡位才能復用,所以0张遭、1兩個卡位都可以直接去 mCachedViews 里拿 ViewHolder 復用邓萨,而且這里的 ViewHolder 是不用重新綁定數(shù)據(jù)的,至于2帝璧、3先誉、4卡位則去 ViewPool 里找,剛好 ViewPool 里緩存著3個 ViewHolder的烁,所以第一行的5個卡位都是用的復用的褐耳,而從 ViewPool 里拿的復用需要重新綁定數(shù)據(jù),才會這樣只有三個卡位需要重新綁定數(shù)據(jù)渴庆。

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

答:有時一行只有3個卡位需要重新綁定的原因跟Q2一樣卓缰,因為 mCachedView 里正好緩存著當前位置的 ViewHolder,本來就是它的 ViewHolder 當然可以直接拿來用砰诵。而至于為什么會創(chuàng)建了17個 ViewHolder征唬,那是因為再第四行的卡位要顯示出來時,ViewPool 里只有3個緩存茁彭,而第四行的卡位又用不了 mCachedViews 里的2個緩存总寒,因為這兩個緩存的是6、7卡位的 ViewHolder理肺,所以就需要再重新創(chuàng)建2個 ViewHodler 來給第四行最后的兩個卡位使用摄闸。

參考鏈接
http://www.apkbus.com/blog-949976-77400.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市妹萨,隨后出現(xiàn)的幾起案子年枕,更是在濱河造成了極大的恐慌,老刑警劉巖眠副,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件画切,死亡現(xiàn)場離奇詭異,居然都是意外死亡囱怕,警方通過查閱死者的電腦和手機霍弹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來娃弓,“玉大人典格,你說我怎么就攤上這事√ù裕” “怎么了耍缴?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長挽霉。 經(jīng)常有香客問我防嗡,道長,這世上最難降的妖魔是什么侠坎? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任蚁趁,我火速辦了婚禮,結(jié)果婚禮上实胸,老公的妹妹穿的比我還像新娘他嫡。我一直安慰自己番官,他們只是感情好,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布钢属。 她就那樣靜靜地躺著徘熔,像睡著了一般。 火紅的嫁衣襯著肌膚如雪淆党。 梳的紋絲不亂的頭發(fā)上酷师,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機與錄音宁否,去河邊找鬼窒升。 笑死,一個胖子當著我的面吹牛慕匠,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播域醇,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼台谊,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了譬挚?” 一聲冷哼從身側(cè)響起锅铅,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎减宣,沒想到半個月后盐须,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡漆腌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年贼邓,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片闷尿。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡塑径,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出填具,到底是詐尸還是另有隱情统舀,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布劳景,位于F島的核電站誉简,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏盟广。R本人自食惡果不足惜闷串,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望衡蚂。 院中可真熱鬧窿克,春花似錦骏庸、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至只损,卻和暖如春一姿,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背跃惫。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工叮叹, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人爆存。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓蛉顽,卻偏偏與公主長得像,于是被迫代替她去往敵國和親先较。 傳聞我的和親對象是個殘疾皇子携冤,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容