Anatomy of RecyclerView: a Search for a ViewHolder[譯]

RecyclerView剖析:搜索ViewHolder

原文: Anatomy of RecyclerView: a Search for a ViewHolder

介紹

在本系列文章中藻雪,我將分享我對(duì)RecyclerView內(nèi)部工作原理的了解池颈。為什么?試想一下:幾乎每個(gè)現(xiàn)代Android應(yīng)用都需要使用RecyclerView豌鸡,因此開(kāi)發(fā)人員對(duì)它的使用方式會(huì)影響數(shù)百萬(wàn)用戶(hù)的體驗(yàn)疹蛉。然而活箕,我們?cè)赗ecyclerView上有什么樣的教育資料?您當(dāng)然可以找到一些關(guān)于如何使用 RecyclerView的基本教程可款,但如何工作的呢育韩?“黑匣子”方法絕對(duì)不夠好,特別是如果您正在進(jìn)行復(fù)雜的自定義或優(yōu)化性能闺鲸。[1]我推薦過(guò) “最深”的材料可能是Google I/O 2016討論的RecyclerView的來(lái)龍去脈筋讨,但是說(shuō)真的,這甚至都不是“來(lái)龍去脈”摸恍,這只是冰山一角悉罕。我的目標(biāo)是更深入。

我假設(shè)讀者具有RecyclerView的基本知識(shí)立镶,如:LayoutManager是什么壁袄,如何通知adapter更改制定數(shù)據(jù)或如何使用item的viewType。

在第一部分中媚媒,我們將理解RecyclerView內(nèi)的一個(gè)方法:getViewByPosition()(support-v7 27.0.2 源碼中為Recycler.getViewForPosition())嗜逻。這是源代碼中最重要的部分之一,通過(guò)研究缭召,我們將了解RecyclerView的許多方面变泄,例如ViewHolder回收令哟,隱藏視圖,預(yù)測(cè)動(dòng)畫(huà)和固定ID妨蛹∑粮唬看到這里的預(yù)測(cè)動(dòng)畫(huà)您可能會(huì)驚訝。嗯蛙卤,盡管Google的人們盡最大努力解耦RecyclerView不同的責(zé)任組件狠半,但它們之間仍然共享了許多“知識(shí)”,預(yù)測(cè)動(dòng)畫(huà)就是其中之一颤难。無(wú)法避免談?wù)摰剿鼈儭?/p>

因此在laying items時(shí)神年,LayoutManager會(huì)詢(xún)問(wèn)RecyclerView“請(qǐng)?jiān)?號(hào)位給我一個(gè)視圖”。以下是RecyclerView的響應(yīng):

  1. 搜索changed scrap
  2. 搜索attached scrap
  3. 搜索未刪除的hidden views
  4. 搜索view cache
  5. 如果Adapter具有穩(wěn)定的ID行嗤,則會(huì)針對(duì)給定的ID再次搜索attached scrapview cache已日。
  6. 搜索ViewCacheExtension
  7. 搜索RecycledViewPool

如果所有這些地方都無(wú)法在找到合適的視圖,它會(huì)通過(guò)調(diào)用適配器的onCreateViewHolder()方法創(chuàng)建一個(gè)栅屏。然后飘千,如果需要onBindViewHolder(),它會(huì)綁定View 栈雳,最后返回它护奈。

RecyclerView的響應(yīng)源碼:

public class RecyclerView {
    public final class Recycler {
        @Nullable
        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                                                         boolean dryRun, long deadlineNs) {
            ViewHolder holder = null;
            // 0) If there is a changed scrap, try to find from there
            if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }


            // 1) Find by position from scrap/hidden list/cache
            if (holder == null) {
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
            }


            if (holder == null) {
                final int type = mAdapter.getItemViewType(offsetPosition);
                // 2) Find from scrap/cache via stable ids, if exists
                if (mAdapter.hasStableIds()) {
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                }
                //Find from mViewCacheExtension
                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 (holder == null) { // fallback to pool
                    holder = getRecycledViewPool().getRecycledView(type);
                }

                if (holder == null) {
                    long start = getNanoTime();
                    if (deadlineNs != FOREVER_NS && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
                        // abort - we have a deadline we can't meet
                        return null;
                    }
                    //Create ViewHolder
                    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()) {
                if (DEBUG && holder.isRemoved()) {
                    throw new IllegalStateException("Removed holder should be bound and it should"
                            + " come here only in pre-layout. Holder: " + holder
                            + exceptionLabel());
                }
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                //Bind ViewHolder
                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            }


            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;
            }
            //When a ViewHolder is created, the reference to it is stored in the View’s LayoutParams
            rvLayoutParams.mViewHolder = holder;
            rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
            return holder;
        }
    }
}


RecycledViewPool

對(duì)每種緩存,我們希望找到以下答案:它的后備數(shù)據(jù)結(jié)構(gòu)是什么哥纫,存儲(chǔ)和檢索ViewHolders的條件霉旗,最重要的是,它的目的是什么蛀骇。

您可能非常了解池的用途:向下滾動(dòng)時(shí)厌秒,向上消失的視圖將被回收到池中,以便從底部出現(xiàn)的視圖重用擅憔。ViewHolders進(jìn)入池中的其他場(chǎng)景鸵闪,我們將稍后討論。但首先讓我們來(lái)看看一些RecycledViewPool的代碼(這是RecyclerView.Recycler的內(nèi)部類(lèi)):

public static class RecycledViewPool {
    private SparseArray<ArrayList<ViewHolder>> mScrap =
                   new SparseArray<>();
    private SparseIntArray mMaxScrap = new SparseIntArray();
    …
    public ViewHolder getRecycledView(int viewType) {
        ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);
        …

首先雕欺,不要為mScrap這個(gè)變量名感到困惑 - 這與上面列表中提到的attached scrap或changed scrap無(wú)關(guān)岛马。

我們看到每個(gè)viewType都有自己的ViewHolders池(mScrap:key為viewType)棉姐。當(dāng)RecyclerView在搜索ViewHolder期間用完所有其他可能性時(shí)屠列,它會(huì)要求池根據(jù)viewType提供ViewHolder;在這一點(diǎn)上伞矩,viewType是唯一重要的笛洛。

現(xiàn)在,每種viewType都有自己的容量乃坤。它默認(rèn)為5苛让,但您可以像這樣更改它:

recyclerView.getRecycledViewPool()
            .setMaxRecycledViews(SOME_VIEW_TYPE, POOL_CAPACITY);

這對(duì)靈活性是非常重要的沟蔑。如果屏幕上有許多相同類(lèi)型的items(通常會(huì)同時(shí)更改),請(qǐng)使該viewType的池更大狱杰。如果您知道瘦材,某些viewType的項(xiàng)目非常罕見(jiàn),它們出現(xiàn)在屏幕上的數(shù)量永遠(yuǎn)不會(huì)超過(guò)一個(gè)仿畸,請(qǐng)?jiān)O(shè)置該viewType池大小為1食棕。否則,池遲早會(huì)被5個(gè)同樣viewType的item填滿(mǎn)错沽,其中4個(gè)就會(huì)閑置在那里簿晓,這是浪費(fèi)內(nèi)存憔儿。

方法getRecycledView()放可,putRecycledView()clear()是公開(kāi)的吴侦,所以你可以操縱池的內(nèi)容。但是手動(dòng)使用putRecycledView()是個(gè)壞主意劫樟,例如:預(yù)先準(zhǔn)備一些ViewHolders织堂。您應(yīng)該onCreateViewHolder()適配器的方法中創(chuàng)建ViewHolder 叠艳,否則ViewHolders可以出現(xiàn)在RecyclerView不期望的狀態(tài)中。[2]

另一個(gè)很酷的功能是易阳,除了getRecycledViewPool()之外還有一個(gè)setRecycledViewPool()附较,因此您可以為多個(gè)RecyclerView重用單個(gè)池。

最后潦俺,我會(huì)注意到每個(gè)viewType的池都是一個(gè)堆棧(后進(jìn)先出)拒课。為什么使用棧更好,我們稍后會(huì)介紹事示。

匯集方式

現(xiàn)在讓我們解決ViewHolders何時(shí)被拋入池中的問(wèn)題早像。有5種場(chǎng)景:

  1. 在滾動(dòng)期間,超出了RecyclerView的界限肖爵。
    (不是直接放入pool中卢鹦,也可能會(huì)放入viewCache中 稍后介紹)
  2. 數(shù)據(jù)已更改,因此不再顯示該view劝堪。當(dāng)消失動(dòng)畫(huà)結(jié)束時(shí)冀自,會(huì)添加到池中揉稚。
  3. 更新或刪除view cache中的item。
  4. 在scrap或緩存中搜到了一個(gè)我們想要位置的ViewHolder熬粗,但由于錯(cuò)誤的viewType或id(如果適配器具有固定的ID)而被判定為不合適的搀玖。[3]
  5. LayoutManager在pre-layout中添加了一個(gè)視圖,但沒(méi)有在post-layout中添加該視圖驻呐。

前兩個(gè)場(chǎng)景非常明顯巷怜。然而,有一點(diǎn)需要注意的是暴氏,場(chǎng)景2不僅在刪除有問(wèn)題的item時(shí)會(huì)觸發(fā)延塑,在插入其他item時(shí)也可能被觸發(fā),例如其他item插入后关带,被推出界限的item不顯示時(shí)。
場(chǎng)景1
LinearLayoutManager.scrollBy() -->
LinearLayoutManager.fill() -->
LinearLayoutManager.recycleByLayoutState() -->
LinearLayoutManager.recycleViewsFromStart() -->
LinearLayoutManager.recycleChildren()-->
RecyclerView.LayoutManager.removeAndRecycleViewAt()-->
RecyclerView.Recycler.recycleView()-->
RecyclerView.Recycler.recycleViewHolderInternal() (存入viewCache磨总、pool的邏輯)-->
RecyclerView.Recycler.addViewHolderToRecycledViewPool-->
RecycledViewPool.putRecycledView(ViewHolder scrap)

class LinearLayoutManager {
     /**
     * Recycles views that went out of bounds after scrolling towards the end of the layout.
     * <p>
     * Checks both layout position and visible position to guarantee that the view is not visible.
     */
     private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt){
         
        final int childCount = getChildCount();
        if (mShouldReverseLayout) {
            for (int i = childCount - 1; i >= 0; i--) {
                View child = getChildAt(i);
                if (mOrientationHelper.getDecoratedEnd(child) > limit
                        || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
                    // stop here
                    recycleChildren(recycler, childCount - 1, i);
                    return;
                }
            }
        } else {
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                if (mOrientationHelper.getDecoratedEnd(child) > limit
                        || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
                    // stop here
                    recycleChildren(recycler, 0, i);
                    return;
                }
            }
        }
     }
}

其他方案需要一些說(shuō)明。我們還沒(méi)有涵蓋view cache和scrap馆纳,但方案3和4背后的想法很簡(jiǎn)單鲁驶。池保存的是“dirty” views和需要重新綁定的view钥弯。除了池之外脆霎,所有緩存中的ViewHolders都保留了一些狀態(tài)(最重要的是位置)辨泳。所有這些緩存都按位置搜索玖院,希望一些ViewHolder可以按原樣重用。相反蔑滓,當(dāng)視圖進(jìn)入池時(shí)燎窘,它的狀態(tài)(所有標(biāo)志褐健,位置等)被清除蚜迅。唯一剩下的就是關(guān)聯(lián)視圖和view type谁不。正如我們所知,池時(shí)根據(jù)view type搜索的谎替,當(dāng)在池中找到ViewHolder時(shí)亡蓉,ViewHolder會(huì)開(kāi)始新的生命周期砍濒。

鑒于該情況爸邢,場(chǎng)景3和場(chǎng)景4應(yīng)該不難理解:例如杠河,如果視圖緩存中的某個(gè)項(xiàng)被刪除唾戚,那么將其保留在該緩存中是沒(méi)有意義的叹坦,因?yàn)闊o(wú)法在原有的位置重用(原有的位置已經(jīng)被刪除)。但是把它扔掉是不好的测蹲,所以我們把它扔進(jìn)池道盏。(見(jiàn)RecyclerView.Recycler.recycleViewHolderInternal())

最后一個(gè)場(chǎng)景要求我們知道pre-layout和post-layout的內(nèi)容荷逞。好吧种远,讓我們繼續(xù)吧坠敷!雖然pre-layout/post-layout不是很重要,但這種機(jī)制一般在RecyclerView的每個(gè)部分都有所體現(xiàn)限次,所以我們無(wú)論如何都要知道它卖漫。


Offtopic:預(yù)布局羊始,布局后和預(yù)測(cè)動(dòng)畫(huà)

考慮一個(gè)場(chǎng)景突委,我們有a匀油,b和c項(xiàng),其中a和b適合屏幕钝侠。我們刪除b帅韧,它將c帶入視圖:


預(yù)布局_1.png

我們希望看到的是c從底部順利滑動(dòng)到它的新位置忽舟。但怎么做呢?我們知道新布局中c的最終位置泣特,但如何知道它應(yīng)該從何處滑過(guò)來(lái)勒叠?通過(guò)查看c應(yīng)該來(lái)自底部的新布局來(lái)假設(shè)RecyclerView或ItemAnimator是錯(cuò)誤的眯分。我們可能有一些自定義的LayoutManager弊决,讓c從側(cè)面或其他地方進(jìn)來(lái)丢氢。所以我們需要LayoutManager的更多幫助。我們可以使用以前的布局嗎貌嫡?不行,因?yàn)槟抢餂](méi)有с别惦。那時(shí)沒(méi)人知道b將被刪除掸掸,所以L(fǎng)ayoutManager認(rèn)為布局c是浪費(fèi)資源。

谷歌的解決方案提供如下羽莺。在適配器發(fā)生更改后盐固,RecyclerView會(huì)從LayoutManager請(qǐng)求兩個(gè)而不是一個(gè)布局刁卜。第一個(gè) - 預(yù)布局长酗,在先前的適配器中布置項(xiàng)目的狀態(tài),但使用適配器更改作為提示茉继,布置一些額外的視圖菲茬,這可能是個(gè)好主意婉弹。在我們的例子中镀赌,因?yàn)槲覀儸F(xiàn)在知道b被刪除了喉钢,所以我們額外列出了c肠虽,盡管它已經(jīng)超出界限税课。第二個(gè) - 后布局韩玩,只是一個(gè)正常的布局侍匙,對(duì)應(yīng)于更改后的適配器狀態(tài)。


預(yù)測(cè)動(dòng)畫(huà)_1.png

現(xiàn)在,通過(guò)比較預(yù)布局和布局后c的位置说莫,我們可以正確地為其外觀設(shè)置動(dòng)畫(huà)。

這種動(dòng)畫(huà) - 當(dāng)動(dòng)畫(huà)視圖在先前的布局中或在新的布局中都不存在時(shí) - 被稱(chēng)為預(yù)測(cè)動(dòng)畫(huà)互婿,這是RecyclerView中最重要的概念之一。我們將在本系列的后續(xù)部分中更詳細(xì)地討論它刮萌。但現(xiàn)在讓我們快速看一下另一個(gè)例子:如果b不是被刪而只是改變了除怎么辦壮锻?

預(yù)布局_2.png

可能讓你驚訝:LayoutManager仍然在預(yù)布局階段布局c猜绣。為什么途事?因?yàn)閎的改變可能會(huì)使它變得更小尸变,誰(shuí)知道呢召烂?如果b變小怕篷,c可能會(huì)從底部彈出酗昼,所以我們最好在預(yù)先布局中將其布局蒸痹。但后來(lái)叠荠,在后期布局中榛鼎,似乎并非如此者娱,我們只是在b中更改了一些TextView 黄鳍。因此不需要c,并將其扔進(jìn)池中街望。這就是進(jìn)入pool的場(chǎng)景5中描述的≡智埃現(xiàn)在我們可以重新回到RecycledViewPool哎甲。


RecycledViewPool炭玫,續(xù)

當(dāng)我們遇到ViewHolder應(yīng)該進(jìn)入池的場(chǎng)景時(shí),還有兩個(gè)障礙:它可能不是可回收的裙犹;它的View可能處于臨時(shí)狀態(tài)叶圃。

可回收

可回收性只是ViewHolder中的一個(gè)標(biāo)志掺冠,您可以使用RecyclerView.ViewHolder.setIsRecyclable()方法進(jìn)行操作德崭。RecycleView也通過(guò)此方法讓ViewHolders在動(dòng)畫(huà)期間不可回收接癌。

從不同地方操縱同一個(gè)標(biāo)志通常是一個(gè)壞主意缨叫。例如耻姥,當(dāng)動(dòng)畫(huà)結(jié)束時(shí)蒸健,RecyclerView會(huì)調(diào)用setIsRecyclable(true)似忧,因?yàn)槌绦虻哪承┨囟ㄔ蚨疲阆M豢苫厥战戎5窃谶@種情況下事情并沒(méi)有真正打破靴跛,因?yàn)檎{(diào)用setIsRecyclable()是配對(duì)的。也就是說(shuō)严拒,如果你調(diào)用setIsRecyclable(false)兩次挤牛,那么setIsRecyclable(true) 只調(diào)用一次不會(huì)使ViewHolder可回收墓赴,你也需要調(diào)用setIsRecyclable(true)兩次诫硕。

臨時(shí)狀態(tài)

View的臨時(shí)狀態(tài)也類(lèi)似。它是View中的一個(gè)標(biāo)志藕届,由setHasTransientState()方法操縱亭饵,并且也是配對(duì)調(diào)用的踏兜。View類(lèi)本身不使用該標(biāo)志,只是保留它山橄。它可以作為L(zhǎng)istView和RecyclerView等控件的提示睡雇,在新內(nèi)容中最好不要重用臨時(shí)狀態(tài)下的View。

您可以自己設(shè)置此標(biāo)志观蓄,但ViewPropertyAnimatorsomeView.animate()…被調(diào)用時(shí))會(huì)在動(dòng)畫(huà)開(kāi)始時(shí)自動(dòng)將其設(shè)置為true,并在動(dòng)畫(huà)結(jié)束時(shí)自動(dòng)設(shè)置為false亲茅。[4]請(qǐng)注意,如果您使用ValueAnimator為視圖設(shè)置動(dòng)畫(huà)腔长,則必須自行管理臨時(shí)狀態(tài)袭祟。

關(guān)于臨時(shí)狀態(tài)的最后一點(diǎn)需要注意的是,它是從子節(jié)點(diǎn)傳播到父節(jié)點(diǎn)捞附,一直傳播到根視圖巾乳。因此,如果您為列表中的item的某個(gè)內(nèi)部view設(shè)置動(dòng)畫(huà)故俐,不僅僅是該item的內(nèi)部view想鹰,就連ViewHolder引用root view也會(huì)進(jìn)入臨時(shí)狀態(tài)紊婉。

OnFailedToRecycleView

如果要回收的ViewHolder無(wú)法通過(guò)可回收性或臨時(shí)狀態(tài)檢查,則Adapter的onFailedToRecycleView()方法會(huì)觸發(fā)。這是非常重要的一點(diǎn):這種方法不僅僅是一個(gè)事件的通知,而且是一個(gè)如何處理的問(wèn)題慨蛙。

onFailedToRecycledView()中直接return true的意思是“無(wú)論如何都回收它”。其中一個(gè)適用的場(chǎng)景是,在綁定新項(xiàng)目時(shí)清除所有動(dòng)畫(huà)和其他此類(lèi)問(wèn)題的來(lái)源泡徙≈或者泉孩,您可以在onFailedToRecycledView()方法中處理這些事情。

你不該完全忽略onFailedToRecycledView()。否則會(huì)給您帶來(lái)?yè)p失,比如以下情況:想象一下,當(dāng)item進(jìn)入視野時(shí)悠咱,其中的圖像淡入顯示眼坏。如果用戶(hù)滾動(dòng)列表足夠地快闯第,則當(dāng)圖像離開(kāi)視圖時(shí)咙好,圖像還沒(méi)有完成淡入合溺,導(dǎo)致ViewHolders無(wú)法進(jìn)行回收鼎俘。因此,滾動(dòng)會(huì)滯后,最重要的是实幕,新的ViewHolders不停的創(chuàng)建,使內(nèi)存變得緊張昼扛。

ViewHolder回收成功時(shí)會(huì)調(diào)用onViewRecycled()方法浦箱,這是釋放大量資源(如圖像)的好地方糕珊。請(qǐng)記住,一些ViewHolder實(shí)例可能會(huì)在沒(méi)有使用的情況下長(zhǎng)時(shí)間留在池中,這可能會(huì)浪費(fèi)大量?jī)?nèi)存鹊漠。

現(xiàn)在我們進(jìn)入下一種緩存 - view cache度陆。


View Cache

當(dāng)我說(shuō)“view Cache”(視圖緩存)或只是“cache”(緩存),所指的都是RecyclerView.Recycler類(lèi)中的mCachedViews字段。它在代碼中的一些注釋中也稱(chēng)為“第一級(jí)緩存”状囱。

這只是ViewHolders的ArrayList冻晤,這里沒(méi)有按view type拆分。默認(rèn)容量為2练俐,您可以通過(guò)RecyclerView.setItemViewCacheSize()的方法進(jìn)行調(diào)整。

正如我之前提到的哈误,pool和其他緩存(包括view cache)之間最重要的區(qū)別是婆殿,在pool中搜索ViewHolder是根據(jù)view type被啼,而在其他緩存中搜索是根據(jù)關(guān)聯(lián)的position。當(dāng)ViewHolder在view cache中時(shí)碍讯,它進(jìn)入緩存后與進(jìn)入緩存前的位置相同钦无,我們希望“原樣”重用它而不需要重新綁定。所以讓我們明確這個(gè)區(qū)別:

  • 如果ViewHolder找不到倍踪,它將被創(chuàng)建和綁定。
  • 如果在pool中找到ViewHolder ,它將被綁定钓株。
  • 如果在cache中找到ViewHolder ,則無(wú)需執(zhí)行任何操作陌僵。

這時(shí)轴合,有一個(gè)重要的事情變得很清楚:一個(gè)ViewHolder的綁定、回收到pool中(onViewRecycled())和它進(jìn)入碗短、移出列表的可視范圍是不一樣的東西受葛。當(dāng)ViewHolder進(jìn)入可視范圍時(shí),ViewHolder有時(shí)會(huì)從view cache中檢索到并且沒(méi)有重新綁定;當(dāng)它從可視范圍移出時(shí)总滩,它的ViewHolder可以緩存到view cache中而不是pool中(參考RecyclerView.Recycler.recycleViewHolderInternal() )纲堵。如果您需要在屏幕上跟蹤item的存在,請(qǐng)使用適配器的onViewAttachedToWindow()onViewDetachedFromWindow()回調(diào)闰渔。

填充pool和cache

現(xiàn)在席函,回到下一個(gè)問(wèn)題:ViewHolders如何在view cache中結(jié)束?當(dāng)我談到viewholder緩存到pool的場(chǎng)景時(shí)冈涧,我實(shí)際上欺騙了你一點(diǎn)點(diǎn)茂附。在這些情況下(第三個(gè)除外),ViewHolder會(huì)轉(zhuǎn)到緩存或池中督弓。[5]

讓我舉例說(shuō)明選擇cache或pool的規(guī)則营曼。比如說(shuō),我們最初有空cache和pool咽筋,items逐個(gè)被回收溶推。這是cache和pool的填充方式(假設(shè)容量為默認(rèn)且只有一種view type):


View_Cache_Pool_1.png

因此,只要cache未滿(mǎn)奸攻,ViewHolders就會(huì)存到那里。如果它已滿(mǎn)虱痕,則新的ViewHolder將緩存中已有的ViewHolder從緩存的“另一端”推送到池中睹耐。如果一個(gè)池已經(jīng)滿(mǎn)了,那么ViewHolder會(huì)被遺忘到垃圾收集器部翘。[6]


Cache和Pool的運(yùn)轉(zhuǎn)方式

現(xiàn)在讓我們看看cache和pool在RecyclerView的幾個(gè)實(shí)際使用場(chǎng)景硝训。

滾動(dòng)中:


滾動(dòng)中_1.png

當(dāng)我們向下滾動(dòng)時(shí),在當(dāng)前看到的items后面有一個(gè)“尾巴”新思,包括cache中的item窖梁,然后是一個(gè)pool中的item。當(dāng)item8出現(xiàn)在屏幕上時(shí)夹囚,在緩存中找不到合適的ViewHolder:沒(méi)有與位置8相關(guān)聯(lián)的ViewHolder纵刘。所以我們使用一個(gè)pool中的ViewHolder,它先前位于第3位荸哟。當(dāng)?shù)?項(xiàng)消失在頂部時(shí)假哎,它進(jìn)入緩存,將4推入池中鞍历。

當(dāng)我們開(kāi)始向相反方向滾動(dòng)時(shí)舵抹,圖片會(huì)有所不同:


滾動(dòng)_2.png

在這里,我們?cè)谝晥D緩存中找到位置5的ViewHolder,并立即重用它,無(wú)需重新綁定甚垦。這似乎是緩存的主要用例 - 反方向滾動(dòng)查看剛剛看到的item西乖,此時(shí)效率更高妈嘹。因此国旷,如果您有新聞源及汉,則緩存可能無(wú)用钢悲,因?yàn)橛脩?hù)不會(huì)經(jīng)常返回陶缺。但是如果它是一個(gè)可供選擇的列表钾挟,比如一個(gè)壁紙庫(kù),你可能想要擴(kuò)展緩存的容量饱岸。

這里有幾點(diǎn)需要注意掺出。首先,如果我們向上滾動(dòng)查看3怎么辦苫费?請(qǐng)記住汤锨,池的工作方式就像一個(gè)堆棧,所以如果我們上次看到3之后只是滾動(dòng)百框,除此之外沒(méi)有做任何事情闲礼,那么ViewHolder 3將是最后一個(gè)放入池中的,因此現(xiàn)在在第3位重新綁定铐维。實(shí)際上如果數(shù)據(jù)沒(méi)有改變柬泽,我們?cè)诮壎〞r(shí)不需要做任何事。您應(yīng)該始終檢查onBindViewHolder()是否確實(shí)需要更改此TextView或ImageView等嫁蛇,此處不需要做更改锨并。

其次,請(qǐng)注意滾動(dòng)時(shí)池中總是不超過(guò)一個(gè)項(xiàng)目(每種視圖類(lèi)型)2桥铩(當(dāng)然第煮,如果你有一個(gè)包含n列的多列網(wǎng)格,那么你將在池中有n個(gè)項(xiàng)目抑党。)通過(guò)場(chǎng)景2-5在池中結(jié)束的其他項(xiàng)目包警,只是在滾動(dòng)期間無(wú)用地停留在那里。

現(xiàn)在讓我們看一個(gè)場(chǎng)景底靠,相比之下害晦,很多項(xiàng)目都會(huì)進(jìn)入池中:調(diào)用notifyDataSetChanged() (或者notifyItemRangeChanged() 使用一些范圍參數(shù)):

池中包含多個(gè)Items.png

所有ViewHolders都變得無(wú)效,緩存不適合他們苛骨,他們都試圖存入池中篱瞎。池中可能沒(méi)有足夠的空間,因此一些不幸的item將被作為垃圾收集然后再次創(chuàng)建痒芝。與滾動(dòng)相比俐筋,在這種情況下您可能需要更大的池。更大的池另一個(gè)有用的情況是通過(guò)調(diào)用scrollToPosition()從一個(gè)位置跳到另一個(gè)位置严衬。

那么池的最佳大小如何選擇呢澄者?似乎最佳策略是在你需要池之??前擴(kuò)充它,并在之后縮小它。實(shí)現(xiàn)此目的粱挡,以下是一種簡(jiǎn)單粗暴的方式:

recyclerView.getRecycledViewPool().setMaxRecycledViews(0, 20);
adapter.notifyDataSetChanged();
new Handler().post(new Runnable() {
    @Override
    public void run() {
        recyclerView.getRecycledViewPool()
                    .setMaxRecycledViews(0, 1);
    }
});

接下來(lái):
Anatomy of RecyclerView: a Search for a ViewHolder (continued)

[1]事實(shí)上赠幕,即使了解RecyclerView的公共API,也需要了解一些內(nèi)部工作原理询筏。例如榕堰,javadoc to setHasStableIds()方法不會(huì)告訴您為什么要使用它。

[2]例如嫌套,createViewHolder()在適配器調(diào)用之后的方法中設(shè)置了正確的視圖類(lèi)型逆屡,并且該字段是包本地的,因此您無(wú)法自己設(shè)置它踱讨。

[3]發(fā)生這種情況時(shí)的示例:更改項(xiàng)目魏蔗,以便更改視圖類(lèi)型,調(diào)用notifyItemChanged()痹筛。此外莺治,禁用ItemAnimator中的更改動(dòng)畫(huà),否則將發(fā)生方案2帚稠。

[4]ViewView處于臨時(shí)狀態(tài)的另一個(gè)例子是EditText谣旁,其中選擇了一些文本或正在編輯過(guò)程中。

[5]在緩存和池之間進(jìn)行選擇之前檢查可回收性和臨時(shí)狀態(tài)翁锡,老實(shí)說(shuō)對(duì)我沒(méi)有多大意義蔓挖,因?yàn)榫彺嬷械囊晥D應(yīng)該完全以消失時(shí)的狀態(tài)重新出現(xiàn)。

[6]在support版本23中馆衔,這種機(jī)制被一個(gè)簡(jiǎn)單的逐個(gè)索引錯(cuò)誤打破。當(dāng)我們逐個(gè)回收ViewHolders時(shí)怨绣,緩存中ViewHolders的數(shù)量在1和2之間交替變化角溃。

結(jié)合log 看布局過(guò)程中從Recycler.mCachedViews 獲取viewHolder

 class Recycler{        
        /**
         * Returns a view for the position either from attach scrap, hidden children, or cache.
         *
         * @param position Item position
         * @param dryRun  Does a dry run, finds the ViewHolder but does not remove
         * @return a ViewHolder that can be re-used for this position.
         */
        ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
            

            //...


            // Search in our first-level recycled view cache.
            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
                if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
                    if (!dryRun) {
                        mCachedViews.remove(i);
                    }
                    if (DEBUG) {
                        Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
                                + ") found match in cache: " + holder);
                    }
                    return holder;
                }
            }
            return null;
        }
}
at android.support.v7.widget.RecyclerView$Recycler.getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        at android.support.v7.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:5750)
        at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5589)
        at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5585)
        at android.support.v7.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2231)
        at android.support.v7.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1558)
        at android.support.v7.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1518)
        at android.support.v7.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:610)


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市篮撑,隨后出現(xiàn)的幾起案子减细,更是在濱河造成了極大的恐慌,老刑警劉巖赢笨,帶你破解...
    沈念sama閱讀 212,718評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件未蝌,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡茧妒,警方通過(guò)查閱死者的電腦和手機(jī)萧吠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)桐筏,“玉大人纸型,你說(shuō)我怎么就攤上這事。” “怎么了狰腌?”我有些...
    開(kāi)封第一講書(shū)人閱讀 158,207評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵除破,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我琼腔,道長(zhǎng)瑰枫,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,755評(píng)論 1 284
  • 正文 為了忘掉前任丹莲,我火速辦了婚禮光坝,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘圾笨。我一直安慰自己教馆,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,862評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布擂达。 她就那樣靜靜地躺著土铺,像睡著了一般。 火紅的嫁衣襯著肌膚如雪板鬓。 梳的紋絲不亂的頭發(fā)上悲敷,一...
    開(kāi)封第一講書(shū)人閱讀 50,050評(píng)論 1 291
  • 那天,我揣著相機(jī)與錄音俭令,去河邊找鬼后德。 笑死,一個(gè)胖子當(dāng)著我的面吹牛抄腔,可吹牛的內(nèi)容都是我干的瓢湃。 我是一名探鬼主播,決...
    沈念sama閱讀 39,136評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼赫蛇,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼绵患!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起悟耘,我...
    開(kāi)封第一講書(shū)人閱讀 37,882評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤落蝙,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后暂幼,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體筏勒,經(jīng)...
    沈念sama閱讀 44,330評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,651評(píng)論 2 327
  • 正文 我和宋清朗相戀三年旺嬉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了管行。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,789評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鹰服,死狀恐怖病瞳,靈堂內(nèi)的尸體忽然破棺而出揽咕,到底是詐尸還是另有隱情,我是刑警寧澤套菜,帶...
    沈念sama閱讀 34,477評(píng)論 4 333
  • 正文 年R本政府宣布亲善,位于F島的核電站,受9級(jí)特大地震影響逗柴,放射性物質(zhì)發(fā)生泄漏蛹头。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,135評(píng)論 3 317
  • 文/蒙蒙 一戏溺、第九天 我趴在偏房一處隱蔽的房頂上張望渣蜗。 院中可真熱鬧,春花似錦旷祸、人聲如沸耕拷。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,864評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)骚烧。三九已至,卻和暖如春闰围,著一層夾襖步出監(jiān)牢的瞬間赃绊,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,099評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工羡榴, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留碧查,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,598評(píng)論 2 362
  • 正文 我出身青樓校仑,卻偏偏與公主長(zhǎng)得像忠售,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子迄沫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評(píng)論 2 351

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