手把手debug源碼之RecyclerView

RecyclerView的使用場景非常豐富葛峻,而本篇的源碼分析基于上下滑動(dòng)一個(gè)列表的場景來觀察它的復(fù)用-回收機(jī)制剪返。本文基于27.0.0版本進(jìn)行分析踩萎,如下是Demo展示:

Demo示例.gif

RecyclerView繼承自ViewGroup棋蚌,屬于系統(tǒng)級別的自定義控件岛抄,而它的源碼長達(dá)12000多行,還不包括抽取出去的其他輔助類月培、管理類等嘁字,可想而知其復(fù)雜性,本文的分析思路主要是集中在RecyclerView的緩存機(jī)制上杉畜,通過滑動(dòng)事件結(jié)合源碼分析它的復(fù)用-回收機(jī)制纪蜒,而RecyclerView的繪制流程、ItemDecoration此叠、LayoutManager纯续、State、Recycler等會(huì)一筆帶過灭袁。

自定義控件三部曲:onMeasure - onLayout - onDraw猬错,RecyclerView也不例外。查看源碼可以看到简卧,RecyclerView測量的一部分邏輯委托給了LayoutManager兔魂,源碼如下所示,進(jìn)來判斷是否存在LayoutManager實(shí)例举娩,不存在則調(diào)用defaultOnMeasure進(jìn)行默認(rèn)測量。然后就是一個(gè)if...else...判斷是否為AutoMeasure构罗,LinearLayoutManager和GridLayoutManager使用這種模式铜涉,而StaggerLayoutManager在一定條件下會(huì)使用自定義測量這種模式。

@Override
protected void onMeasure(int widthSpec, int heightSpec) {
    if (mLayout == null) {
        defaultOnMeasure(widthSpec, heightSpec);
        return;
    }
    //LinearLayoutManager和GridLayoutManager使用這種模式
    if (mLayout.mAutoMeasure) {
        ...
        mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
        ...
    } else {
        if (mHasFixedSize) {
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            return;
        }
        ...
        //而StaggerLayoutManager在一定條件下會(huì)使用自定義測量這種模式
    }
}

測量之后會(huì)執(zhí)行onLayout遂唧,這里我們分析采用垂直布局的LinearLayoutManager芙代,在布局的邏輯中會(huì)經(jīng)過如下三個(gè)方法:dispatchLayoutStep1 - dispatchLayoutStep2 - dispatchLayoutStep3,它們各司其職盖彭。

dispatchLayoutStep1:處理Adapter的更新和動(dòng)畫相關(guān)
dispatchLayoutStep2:真正執(zhí)行LayoutManager.onLayoutChildren纹烹,該函數(shù)的實(shí)現(xiàn)決定了ChildView將會(huì)怎樣被布局(layout)
dispatchLayoutStep3:保存動(dòng)畫相關(guān)的信息并做必要的清理工作

所以我們重點(diǎn)放到LayoutManager.onLayoutChildren上页滚,直接進(jìn)入LinearLayoutManager的onLayoutChildren,發(fā)現(xiàn)代碼很長铺呵,里面也有注釋信息裹驰,布局的邏輯如下:1 首先尋找錨點(diǎn),2 從錨點(diǎn)開始片挂,底部向上填充幻林,頂部向下填充,3 如果再有剩余空間音念,再填充一次沪饺。下面的LinearLayoutManager配合垂直布局的onLayout代碼段:

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...

    ensureLayoutState();
    mLayoutState.mRecycle = false;
    // 確定布局方向
    resolveShouldLayoutReverse();

    // 尋找錨點(diǎn)
    final View focused = getFocusedChild();
    if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION
            || mPendingSavedState != null) {
        mAnchorInfo.reset();
        mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
        // 計(jì)算錨點(diǎn)的位置和坐標(biāo)
        updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
        mAnchorInfo.mValid = true;
    } 

    ...

    detachAndScrapAttachedViews(recycler);//回收view
    
    // 下面是LinearLayoutManager配合垂直布局的代碼
    // 先向下繪制
    updateLayoutStateToFillEnd(mAnchorInfo);
    // 填充view
    fill(recycler, mLayoutState, state, false);
    
    ...

    // 再向上繪制
    updateLayoutStateToFillStart(mAnchorInfo);
    // 填充view
    fill(recycler, mLayoutState, state, false);

    //還有可用空間
    if (mLayoutState.mAvailable > 0) {
        ...
        // 再次填充view
        fill(recycler, mLayoutState, state, false);
    }

    ...
}

至此我們大致了解了布局的算法邏輯:先找錨點(diǎn)再多次不同方向上進(jìn)行填充,而RecyclerView的復(fù)用流程和回收流程都在該方法里面發(fā)起闷愤,所以onLayout是我們分析緩存機(jī)制的入口整葡。其中復(fù)用流程是fill,回收流程是detachAndScrapAttachedViews讥脐。到這里我們先總結(jié)下onMeasure和onLayout的內(nèi)容:

1.RecyclerView是將繪制流程交給LayoutManager處理掘宪,如果沒有設(shè)置不會(huì)測量子View
2.繪制流程是區(qū)分正向和倒置的
3.繪制是先確定錨點(diǎn),然后再多次不同方向上進(jìn)行填充攘烛,fill()至少會(huì)執(zhí)行兩次魏滚,如果繪制完還有剩余空間,則會(huì)再執(zhí)行一次fill()方法
4.LayoutManager獲得View(也可理解為復(fù)用入口)是從RecyclerView中的onLayout開始的(fill)坟漱,涉及到RecyclerView的緩存策略鼠次,如果沒有拿到緩存,則走我們自己重寫的onCreateView方法芋齿,再調(diào)用onBindViewHolder
5.LayoutManager回收View的入口也是RecyclerView中的onLayout開始的(detachAndScrapAttachedViews)腥寇,涉及到RecyclerView的緩存策略

下面就會(huì)詳細(xì)分析復(fù)用流程和回收流程,這里先確定流程的入口是onLayout方法觅捆。onDraw的代碼這里不再進(jìn)行分析赦役。這里根據(jù)源碼的執(zhí)行順序會(huì)先進(jìn)行回收再復(fù)用,所以下面先分析回收流程栅炒。

1. 回收流程

回收流程的入口方法是 LinearLayoutManager - onLayoutChildren - detachAndScrapAttachedViews -scrapOrRecycleView掂摔,最后一個(gè)方法名翻譯一下是:廢棄或者回收view,在該方法中會(huì)根據(jù)一定的策略來決定是scrap還是recycle赢赊,下面是scrapOrRecycleView的源碼:

private void scrapOrRecycleView(Recycler recycler, int index, View view) {
    //從指定的view中獲取到對應(yīng)的viewHolder
    final ViewHolder viewHolder = getChildViewHolderInt(view);
    if (viewHolder.shouldIgnore()) {
        if (DEBUG) {
            Log.d(TAG, "ignoring view " + viewHolder);
        }
        return;
    }
    //viewHolder已經(jīng)無效乙漓,并且還沒有被remove,并且沒有指定的stableId
    if (viewHolder.isInvalid() && !viewHolder.isRemoved()
            && !mRecyclerView.mAdapter.hasStableIds()) {
        //remove該項(xiàng)
        removeViewAt(index);
        //通過recycler執(zhí)行內(nèi)部回收流程释移,主要是將viewHolder放到RecycledViewPool中
        recycler.recycleViewHolderInternal(viewHolder);
    } else {
        //detach該項(xiàng)
        detachViewAt(index);
        //通過recycler將view從scrap數(shù)組中移除
        recycler.scrapView(view);
        mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
    }
}

上面的代碼中牽出了兩個(gè)比較重要的概念:remove和detach叭披。

detach: 在ViewGroup中的實(shí)現(xiàn)很簡單,只是將ChildView從ParentView的ChildView數(shù)組中移除玩讳,ChildView的mParent設(shè)置為null涩蜘,可以理解為輕量級的臨時(shí)remove嚼贡,因?yàn)閂iew此時(shí)和View樹還是藕斷絲連,這個(gè)函數(shù)被經(jīng)常用來改變ChildView在ChildView數(shù)組中的次序同诫。View被detach一般是臨時(shí)的粤策,在后面會(huì)被重新attach。

remove: 真正的移除剩辟,不光被從ChildView數(shù)組中除名掐场,其他和View樹各項(xiàng)聯(lián)系也會(huì)被徹底斬?cái)?不考慮Animation/LayoutTransition這種特殊情況),比如焦點(diǎn)被清除贩猎,從TouchTarget中被移除等熊户。

所以我們可以將scrapOrRecycleView對應(yīng)起來:scrap-detach,recycler-remove吭服。同時(shí)滿足下面的3個(gè)條件會(huì)被recycler嚷堡,其余情況下viewHolder都會(huì)被scrap:

1、viewHolder本身已經(jīng)完全無效
2艇棕、viewHolder對應(yīng)的項(xiàng)還沒有被remove(這個(gè)判斷是考慮到預(yù)加載的原因蝌戒,先不具體說)
3、adapter沒有指定stableId沼琉,因?yàn)槿绻付ū惫叮筒淮嬖赩iew綁定內(nèi)容無效的可能了

Demo案例實(shí)操過程中,上下滑動(dòng)時(shí)基本上都是觸發(fā)recycler打瘪;當(dāng)插入一個(gè)元素或者刪除一個(gè)元素友鼻,或者本質(zhì)上說調(diào)用notifyDataSetChanged后,就會(huì)觸發(fā)scrap闺骚。

下面再看看recycler執(zhí)行內(nèi)部回收流程彩扔,大致邏輯是先判斷viewHolder的一些標(biāo)志位,達(dá)到回收條件后僻爽,先將viewHolder緩存到mCachedViews中虫碉,如果mCachedViews已滿,則刪除mCachedViews中最老的一個(gè)元素胸梆,并將該元素放到RecycledViewPool中敦捧;再接著將本次要回收的元素放到mCachedViews中。 如果未達(dá)到條件乳绕,則直接將viewHolder放到RecycledViewPool中绞惦。下面這段代碼是整理之后的源碼,描述了上述邏輯:

void recycleViewHolderInternal(ViewHolder holder) {
    //進(jìn)行必要的校驗(yàn)洋措,否則拋出異常
    if (holder.isScrap() || holder.itemView.getParent() != null) {
        throw new IllegalArgumentException(
                "Scrapped or attached views may not be recycled. isScrap:"
                        + holder.isScrap() + " isAttached:"
                        + (holder.itemView.getParent() != null) + exceptionLabel());
    }

    //進(jìn)行必要的校驗(yàn),否則拋出異常
    if (holder.isTmpDetached()) {
        throw new IllegalArgumentException("Tmp detached view should be removed "
                + "from RecyclerView before it can be recycled: " + holder
                + exceptionLabel());
    }

    //進(jìn)行必要的校驗(yàn)杰刽,否則拋出異常
    if (holder.shouldIgnore()) {
        throw new IllegalArgumentException("Trying to recycle an ignored view holder. You"
                + " should first call stopIgnoringView(view) before calling recycle."
                + exceptionLabel());
    }
    
    ...

    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();
            //判斷cachedViewSize是否大于最大緩存數(shù)量
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                //回收最老的元素菠发,即第0號元素
                recycleCachedViewAt(0);
                cachedViewSize--;
            }

            int targetCacheIndex = cachedViewSize;
            if (ALLOW_THREAD_GAP_WORK
                    && cachedViewSize > 0
                    && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                int cacheIndex = cachedViewSize - 1;
                while (cacheIndex >= 0) {
                    int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                    if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                        break;
                    }
                    cacheIndex--;
                }
                //計(jì)算出緩存元素的index值
                targetCacheIndex = cacheIndex + 1;
            }
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
        }
        //未到達(dá)條件王滤,又沒被緩存,則直接放到RecycledViewPool
        if (!cached) {
            addViewHolderToRecycledViewPool(holder, true);
            recycled = true;
        }
    } 
    
    ...

}

根據(jù)上面的源碼滓鸠,我們debug的方法調(diào)用路徑是:recycleViewHolderInternal - recycleCachedViewAt - addViewHolderToRecycledViewPool雁乡。下面就是addViewHolderToRecycledViewPool中最關(guān)鍵的源碼,將元素放到RecycledViewPool中糜俗,可以看到這里區(qū)分了type踱稍,每個(gè)type對應(yīng)一個(gè)ArrayList,同時(shí)進(jìn)入到這里的viewHolder會(huì)被重置悠抹,主要是重置position以及flags珠月。

public void putRecycledView(ViewHolder scrap) {
    //拿到type
    final int viewType = scrap.getItemViewType();
    //拿到type對應(yīng)的ViewHolder集合
    final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
    if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
        return;
    }
    if (DEBUG && scrapHeap.contains(scrap)) {
        throw new IllegalArgumentException("this scrap item already exists");
    }
    //重置viewHolder
    scrap.resetInternal();
    //將viewHolder添加到集合中
    scrapHeap.add(scrap);
}

看完recycler的流程之后,還有一種回收場景scrap楔敌,scrap的場景中會(huì)涉及到比較多的全局變量啤挎,如mChildHelper,mAttachedScrap卵凑,mChangedScrap庆聘。首先mAttachedScrap和mChangedScrap都是ArrayList類型的緩存viewHolder變量的。mChildHelper是ChildHelper的實(shí)例對象勺卢,RecyclerView盡管本身是一個(gè)ViewGroup伙判,但是將ChildView管理職責(zé)全權(quán)委托給了ChildHelper,所有關(guān)于ChildView的操作都要通過ChildHelper來間接進(jìn)行黑忱,ChildHelper成為了一個(gè)ChildView操作的中間層宴抚,getChildCount/getChildAt等函數(shù)經(jīng)由ChildHelper的攔截處理再下發(fā)給RecyclerView的對應(yīng)函數(shù),其參數(shù)或者返回結(jié)果會(huì)根據(jù)實(shí)際的ChildView信息進(jìn)行改寫杨何。了解了基本的概念之后酱塔,看看scrapOrRecycleView中的detach分支,下面是detach分支的關(guān)鍵源碼:

//detach下標(biāo)為index的view
detachViewAt(index);
//在recycler中維護(hù)下這個(gè)scrapView
recycler.scrapView(view);

detachViewAt中是通過mChildHelper處理view和parentView的關(guān)系危虱;而在scrapView中羊娃,則通過判斷viewHolder是否被removed,是否invalid埃跷,是否canReuseUpdatedViewHolder條件來決定是放到mAttachedScrap中還是mChangedScrap中蕊玷。源碼如下所示:

void scrapView(View view) {
    //拿到viewHolder
    final ViewHolder holder = getChildViewHolderInt(view);
    //是否被removed,或者invalid弥雹,或者canReuseUpdatedViewHolder
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
            || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
        ...
        holder.setScrapContainer(this, false);
        //放到mAttachedScrap中
        mAttachedScrap.add(holder);
    } else {
        if (mChangedScrap == null) {
            mChangedScrap = new ArrayList<ViewHolder>();
        }
        holder.setScrapContainer(this, true);
        //否則放到mChangedScrap中
        mChangedScrap.add(holder);
    }
}

這里還需要對比下remove和scrap在復(fù)用性上的不同垃帅,只被detach的View要比被remove的View高,detach的View一般來說代表可以直接復(fù)用(其ViewHolder對應(yīng)于Data的Position還是有效的剪勿,只需要重新綁定數(shù)據(jù)贸诚,如果數(shù)據(jù)也沒變化的話,甚至都不用重新綁定數(shù)據(jù);View還是有效的酱固,View綁定的數(shù)據(jù)可能有效的, 比如一個(gè)列表有N項(xiàng)械念,現(xiàn)在刪除了其中一項(xiàng),那么在沒有其他變化的前提下运悲,剩余的N-1個(gè)項(xiàng)對應(yīng)的ViewHolder是可以直接復(fù)用的)龄减,這一點(diǎn)非常關(guān)鍵,避免了不必要的綁定(和ListView等相比)班眯,項(xiàng)處理的粒度從整體細(xì)化到了單個(gè)項(xiàng)希停,即包含了對View的復(fù)用,也包含了對View當(dāng)前綁定內(nèi)容的復(fù)用署隘。被remove的View復(fù)用性上則要差一些宠能,其對應(yīng)的Position已經(jīng)無效,這種復(fù)用層級和Scrap相比只有View層級的復(fù)用(稍帶可以復(fù)用ViewHolder,只不過里面的信息要重新設(shè)置定踱,但起碼不用new一個(gè))棍潘。

至此回收機(jī)制的流程基本完成,回顧一下崖媚,首先在RecyclerView的onLayout方法中會(huì)在dispatchLayoutStep2中將布局的權(quán)利移交給LayoutManger亦歉,Demo中對應(yīng)就是LinearLayoutManager。LinearLayoutManager接管之后畅哑,調(diào)用自身的onLayoutChildren肴楷,然后就會(huì)對view進(jìn)行回收(detachAndScrapAttachedViews)和填充(fill)。detachAndScrapAttachedViews中會(huì)根據(jù)一定的條件決定該view是被scrap(對應(yīng)detach)還是被recycler(對應(yīng)remove)荠呐。被recycler的view會(huì)先經(jīng)過mCachedViews再根據(jù)條件進(jìn)入到RecyclerViewPool中赛蔫,而被scrap的元素會(huì)根據(jù)具體條件看是放到mAttachedScrap還是mChangedScrap中緩存起來。

2. 復(fù)用流程

上面根據(jù)LinearLayoutManager的onLayoutChildren中代碼的執(zhí)行順序泥张,先分析了回收機(jī)制的流程呵恢,接下來繼續(xù)分析復(fù)用機(jī)制的流程,還是遵循上文的思路媚创,先確定入口方法渗钉,再確定一條方法調(diào)用流程,然后再細(xì)細(xì)分析钞钙。上文提到過LinearLayoutManager配合垂直布局的onLayout代碼段鳄橘,找到錨點(diǎn),先向下繪制-再填充-再向上繪制-再填充的流程芒炼,這里的fill方法便是我們分析復(fù)用機(jī)制的入口方法了瘫怜。

// 下面是LinearLayoutManager配合垂直布局的代碼
// 先向下繪制
updateLayoutStateToFillEnd(mAnchorInfo);
// 填充view
fill(recycler, mLayoutState, state, false);

...

// 再向上繪制
updateLayoutStateToFillStart(mAnchorInfo);
// 填充view
fill(recycler, mLayoutState, state, false);

//還有可用空間
if (mLayoutState.mAvailable > 0) {
    ...
    // 再次填充view
    fill(recycler, mLayoutState, state, false);
}

進(jìn)入fill后,會(huì)根據(jù)layoutState是否還有更多項(xiàng)要填充本刽,來循環(huán)調(diào)用layoutChunk方法鲸湃,根據(jù)layoutChunk這個(gè)方法名猜測其作用就是布局塊用的赠涮,一塊一塊對應(yīng)就是一項(xiàng)一項(xiàng)的item。在layoutChunk中唤锉,先通過next方法找到view世囊,然后對該view進(jìn)行再測量和布局别瞭,以及邊框的確定窿祥。這篇文章的重點(diǎn)是關(guān)注緩存機(jī)制,所以繪制布局這塊一筆帶過蝙寨,我們將重點(diǎn)放到next方法上晒衩。下面是next方法的源碼:

View next(RecyclerView.Recycler recycler) {
    if (mScrapList != null) {
        return nextViewFromScrapList();
    }
    //通過recycler對象找到一個(gè)合適的view
    final View view = recycler.getViewForPosition(mCurrentPosition);
    mCurrentPosition += mItemDirection;
    return view;
}

上述代碼最關(guān)鍵的一句是recycler.getViewForPosition(mCurrentPosition),通過給定的position獲取一個(gè)view的實(shí)例對象墙歪,最終會(huì)通過tryGetViewHolderForPositionByDeadline方法得到一個(gè)viewHolder听系,再通過viewHolder里面的itemView屬性將view實(shí)例對象返回。如下源碼所示:

public View getViewForPosition(int position) {
    return getViewForPosition(position, false);
}

View getViewForPosition(int position, boolean dryRun) {
    //先獲取viewHolder虹菲,再通過itemView屬性得到view的實(shí)例
    return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}

Recycler一般不會(huì)直接作用于View靠胜,其操作的對象一般是ViewHolder。如果你有自己debug代碼毕源,留意了view和viewHolder之間的關(guān)系浪漠,你會(huì)發(fā)現(xiàn)它們之間是雙向綁定的,view中持有viewHolder是通過LayoutParams的mViewHolder屬性霎褐;而viewHolder中持有view是通過itemView屬性址愿。在tryGetViewHolderForPositionByDeadline中總的思路是,依次經(jīng)過RecyclerView中的四級緩存冻璃,一級一級找响谓,找到了就返回viewHolder,沒有的話省艳,就回調(diào)用戶的onCreateViewHolder和onBindViewHolder娘纷。RecyclerView中的四級緩存更細(xì)致的說應(yīng)該是Recycler中的四級緩存,分別是:mAttachedScrap - mCachedViews - mViewCacheExtension - mRecyclerPool跋炕。

mAttachedScrap:對應(yīng)上述回收機(jī)制中的Scrap View赖晶,保存在mAttachedScrap或者mChangedScrap中,用于屏幕內(nèi)的itemView快速復(fù)用枣购。

mCachedViews:對應(yīng)上述回收機(jī)制中的remove view嬉探,默認(rèn)上線為2個(gè)。

mViewCacheExtension:供使用者自行擴(kuò)展棉圈,讓使用者可以控制緩存涩堤。

mRecyclerPool:對應(yīng)于上述回收機(jī)制中remove view放到mCachedViews后溢出的view,同時(shí)可以用與RecyclerView之間共享ViewHolder的緩存池分瘾。

了解了上面四級緩存后胎围,接著看tryGetViewHolderForPositionByDeadline的代碼會(huì)輕松很多,如下源碼:

@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    ...

    ViewHolder holder = null;
    
    // 1) Find by position from scrap/hidden list/cache
    if (holder == null) {
        //從scrap或hidden或cache中找viewHolder
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        if (holder != null) {
            //檢查找到的holder是不是能夠被當(dāng)前的位置使用,不行的話就要對該viewHolder進(jìn)行回收
            if (!validateViewHolderForOffsetPosition(holder)) {
                // dryRun一般為false白魂,表示可以從scrap或者cache中移除
                if (!dryRun) {
                    holder.addFlags(ViewHolder.FLAG_INVALID);
                    if (holder.isScrap()) {
                        removeDetachedView(holder.itemView, false);
                        holder.unScrap();
                    } else if (holder.wasReturnedFromScrap()) {
                        holder.clearReturnedFromScrapFlag();
                    }
                    //執(zhí)行內(nèi)部回收邏輯
                    recycleViewHolderInternal(holder);
                }
                holder = null;
            } else {
                fromScrapOrHiddenOrCache = true;
            }
        }
    }
    if (holder == null) {
        ...
        //獲取type
        final int type = mAdapter.getItemViewType(offsetPosition);
        // 2) Find from scrap/cache via stable ids, if exists
        if (mAdapter.hasStableIds()) {
            //有設(shè)置stableId汽纤,則嘗試從scrap或者cache中獲取
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
            if (holder != null) {
                // update position
                holder.mPosition = offsetPosition;
                fromScrapOrHiddenOrCache = true;
            }
        }
        //判斷是否設(shè)置了外部擴(kuò)展
        if (holder == null && mViewCacheExtension != null) {
            // 從外部擴(kuò)展中找
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
                ...
            }
        }
        if (holder == null) { // fallback to pool
            ...
            //根據(jù)tyep從RecycledViewPool中找
            holder = getRecycledViewPool().getRecycledView(type);
            ...
        }
        if (holder == null) {
            //回調(diào)用戶的onCreateViewHolder方法
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
            ...
        }
    }

    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()) {
        ...
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        //回調(diào)用戶的onBindViewHolder方法
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }

    //獲取LayoutParams
    final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
    final LayoutParams rvLayoutParams;
    if (lp == null) {
        //轉(zhuǎn)成RecyclerView所需類型的LayoutParams
        rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
        holder.itemView.setLayoutParams(rvLayoutParams);
    } else if (!checkLayoutParams(lp)) {
        rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
        holder.itemView.setLayoutParams(rvLayoutParams);
    } else {
        rvLayoutParams = (LayoutParams) lp;
    }
    //將viewHolder保存到mViewHolder屬性中
    rvLayoutParams.mViewHolder = holder;
    rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
    return holder;
}

上述源碼我對其進(jìn)行了刪減,保留了核心流程代碼福荸,從上倒下就是從四級緩存中逐個(gè)查找蕴坪,實(shí)在沒有則創(chuàng)建一個(gè),最后將viewHolder綁定到view上敬锐,完成最后的雙向綁定背传。

至此復(fù)用機(jī)制的流程基本完成,總結(jié)一下台夺,方法的調(diào)用流程是:LinearLayoutManager - onLayoutChildren - fill() - layoutChunk() - layoutState.next() - getViewForPosition() - tryGetViewHolderForPositionByDeadline() - 四級緩存 or onCreateViewHolder - onBindViewHolder(未綁定的情況下會(huì)觸發(fā)綁定回調(diào))径玖。這套流程下來要關(guān)注兩個(gè)地方,一個(gè)是fill方法颤介,它會(huì)被調(diào)用多次梳星;一個(gè)是tryGetViewHolderForPositionByDeadline方面,里面涉及到RecyclerView復(fù)用機(jī)制的核心邏輯:四級緩存滚朵。

3. RecyclerView的優(yōu)勢

3.1. RecyclerView與ListView對比

RecyclerView強(qiáng)制使用ViewHolder冤灾,當(dāng)然在使用ListView的時(shí)候都是自定義ViewHolder配合使用,避免每次createView時(shí)調(diào)用findViewById始绍。但是RecyclerView在ViewHolder基礎(chǔ)上定義了很多flag標(biāo)識表明當(dāng)前ViewHolder的可用性狀態(tài)瞳购,這點(diǎn)比ListView中自定義ViewHolder要更加豐富。

在處理離屏緩存這一場景時(shí)亏推,RecyclerView與ListView的處理也有很大的不同学赛。RecyclerView會(huì)從mCachedViews中獲取到一個(gè)viewHolder,然后會(huì)判斷這個(gè)viewHolder是否已被綁定吞杭,是否不需要更新盏浇,是否有效,如果滿足其中任何一個(gè)條件就不會(huì)觸發(fā)onBindViewHolder芽狗。源碼如下所示:

//處理預(yù)加載的情況
if (mState.isPreLayout() && holder.isBound()) {
    // do not update unless we absolutely have to.
    holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {//如果已被綁定绢掰,或者不需要更新或者是有效的,就不會(huì)觸發(fā)tryBindViewHolderByDeadline方法了
    ...
    final int offsetPosition = mAdapterHelper.findPositionOffset(position);
    bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}

而ListView的處理則是mRecycler得到一個(gè)緩存的view童擎,然后重新getView滴劲,此處勢必會(huì)調(diào)用onBind觸發(fā)重新綁定的邏輯。AbsListView源碼如下所示:

View obtainView(int position, boolean[] outMetadata) {
    ...

    //拿到緩存的view
    final View scrapView = mRecycler.getScrapView(position);
    //每次都調(diào)用getView顾复,也就意味著每次都調(diào)用onBind
    final View child = mAdapter.getView(position, scrapView, this);
    if (scrapView != null) {
        if (child != scrapView) {
            // Failed to re-bind the data, return scrap to the heap.
            mRecycler.addScrapView(scrapView, position);
        } else if (child.isTemporarilyDetached()) {
            outMetadata[0] = true;

            // Finish the temporary detach started in addScrapView().
            child.dispatchFinishTemporaryDetach();
        }
    }

    ...
}

下面的gif動(dòng)圖展示了RecyclerView中處理離屏緩存時(shí)班挖,onBind方法的執(zhí)行情況,當(dāng)用戶輕微的來回滑入滑出item時(shí)芯砸,此時(shí)是從mCachedViews中拿到緩存的viewHolder直接復(fù)用萧芙,不會(huì)觸發(fā)onBind操作给梅。

離屏緩存示例.gif

3.2. 局部刷新功能

處理局部刷新時(shí),ListView是一鍋端双揪,將所有的mActiveViews都移入了二級緩存mScrapViews动羽,而RecyclerView則是更加靈活地對每個(gè)View修改標(biāo)志位,區(qū)分是否重新bindView渔期。通過局部刷新能避免調(diào)用許多無用的bindView运吓,下面的gif動(dòng)圖展示了局部刷新position位置為4的場景,我們可以觀察第二個(gè)透明框中的onBind的情況擎场。

局部刷新示例.gif

參考:
RecyclerView機(jī)制分析: Recycler
Android ListView與RecyclerView對比淺析--緩存機(jī)制

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末羽德,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子迅办,更是在濱河造成了極大的恐慌,老刑警劉巖章蚣,帶你破解...
    沈念sama閱讀 219,110評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件站欺,死亡現(xiàn)場離奇詭異,居然都是意外死亡纤垂,警方通過查閱死者的電腦和手機(jī)矾策,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來峭沦,“玉大人贾虽,你說我怎么就攤上這事『鹩悖” “怎么了蓬豁?”我有些...
    開封第一講書人閱讀 165,474評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長菇肃。 經(jīng)常有香客問我地粪,道長,這世上最難降的妖魔是什么琐谤? 我笑而不...
    開封第一講書人閱讀 58,881評論 1 295
  • 正文 為了忘掉前任蟆技,我火速辦了婚禮,結(jié)果婚禮上斗忌,老公的妹妹穿的比我還像新娘质礼。我一直安慰自己,他們只是感情好织阳,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評論 6 392
  • 文/花漫 我一把揭開白布眶蕉。 她就那樣靜靜地躺著,像睡著了一般陈哑。 火紅的嫁衣襯著肌膚如雪妻坝。 梳的紋絲不亂的頭發(fā)上伸眶,一...
    開封第一講書人閱讀 51,698評論 1 305
  • 那天,我揣著相機(jī)與錄音刽宪,去河邊找鬼厘贼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛圣拄,可吹牛的內(nèi)容都是我干的嘴秸。 我是一名探鬼主播,決...
    沈念sama閱讀 40,418評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼庇谆,長吁一口氣:“原來是場噩夢啊……” “哼岳掐!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起饭耳,我...
    開封第一講書人閱讀 39,332評論 0 276
  • 序言:老撾萬榮一對情侶失蹤串述,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后寞肖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體纲酗,經(jīng)...
    沈念sama閱讀 45,796評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評論 3 337
  • 正文 我和宋清朗相戀三年新蟆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了觅赊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,110評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡琼稻,死狀恐怖吮螺,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情帕翻,我是刑警寧澤鸠补,帶...
    沈念sama閱讀 35,792評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站熊咽,受9級特大地震影響莫鸭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜横殴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評論 3 331
  • 文/蒙蒙 一被因、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧衫仑,春花似錦梨与、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至瞄崇,卻和暖如春呻粹,著一層夾襖步出監(jiān)牢的瞬間壕曼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評論 1 272
  • 我被黑心中介騙來泰國打工等浊, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留腮郊,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,348評論 3 373
  • 正文 我出身青樓筹燕,卻偏偏與公主長得像轧飞,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子撒踪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評論 2 355