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):
- 搜索changed scrap
- 搜索attached scrap
- 搜索未刪除的hidden views
- 搜索view cache
- 如果Adapter具有穩(wěn)定的ID行嗤,則會(huì)針對(duì)給定的ID再次搜索attached scrap和view cache已日。
- 搜索ViewCacheExtension
- 搜索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)景:
- 在滾動(dòng)期間,超出了RecyclerView的界限肖爵。
(不是直接放入pool中卢鹦,也可能會(huì)放入viewCache中 稍后介紹) - 數(shù)據(jù)已更改,因此不再顯示該view劝堪。當(dāng)消失動(dòng)畫(huà)結(jié)束時(shí)冀自,會(huì)添加到池中揉稚。
- 更新或刪除view cache中的item。
- 在scrap或緩存中搜到了一個(gè)我們想要位置的ViewHolder熬粗,但由于錯(cuò)誤的viewType或id(如果適配器具有固定的ID)而被判定為不合適的搀玖。[3]
- 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帶入視圖:
我們希望看到的是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)。
現(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不是被刪而只是改變了除怎么辦壮锻?
可能讓你驚訝: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)志观蓄,但ViewPropertyAnimator
(someView.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):
因此,只要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)我們向下滾動(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ì)有所不同:
在這里,我們?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ù)):
所有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)