序言
- RecyclerView有三大典型的功能,一個是Recycler的緩存機(jī)制,一個LayoutManager的布局管理,一個ItemDecoration的分割線繪制;本文將結(jié)合源碼講解其緩存機(jī)制
- 更多相關(guān)的源碼解析見RecyclerView之ItemDecoration
正文
緩存機(jī)制
(1). RecycledViewPool的緩存
- RecycledViewPool也叫第三級緩存
- 文檔中說的是: 為多個RecyclerView提供的一個共用緩存池,如果想要通過RecyclerView緩存View的話,可以自己提供一個RecycledViewPool實例,并通過RecyclerView的setRecycledViewPool()方法來設(shè)置,如果不主動提供的話,RecyclerView會為自己主動創(chuàng)建一個
- 首先來看其緩存方式: 其中有一個 SparseArray<ScrapData> 類型的mScrap來緩存ViewHolder,每一個View Type 類型的Item都會有一個該緩存(源碼如下),默認(rèn)最大容量為5,但是可以通過
recyclerView.getRecycledViewPool().setMaxRecycledViews(int viewType, int max);
來設(shè)置;(作者推薦的是:如果屏幕上有很多相同類型的ItemView同時改變,那么推薦將該容量設(shè)置大一些,但是如果有一種類型的ItemView很少出現(xiàn),并且不超過一個,那么推薦將該容量設(shè)置為1,否則其遲早會被填滿而造成內(nèi)存浪費)
static class ScrapData {
ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP; //每個View Type默認(rèn)容量為5
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
SparseArray<ScrapData> mScrap = new SparseArray<>();
至于這里的SparseArray,它是Android中的一個工具類,因為Android內(nèi)存限制,所以產(chǎn)生了這樣一個比HashMap輕量的類(具體可以參考博客)
接下來看一下RecycledViewPool的存取方法;從這兩個方法中,我們可以看出,在RecycledViewPool中緩存的ViewHolder之間是依靠 View Type 來區(qū)分的,也就是說,同一個View Type之間的ViewHolder緩存在RecycledViewPool中是沒有區(qū)別的;如果我們沒有重寫ViewHolder的getItemViewType()方法,那么就默認(rèn)只有一種View Type,默認(rèn)為-1
public ViewHolder getRecycledView(int viewType) {
...
return scrapHeap.remove(scrapHeap.size() - 1);
}
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
...
scrap.resetInternal();
scrapHeap.add(scrap);
}
- 下面我們看一下在將一個ViewHolder放進(jìn)RecycledViewPool之前,都會做什么處理(主要代碼如下);需要注意的是,下面的注釋中有這樣一句話:
Pass false to dispatchRecycled for views that have not been bound.
,大意為:當(dāng)一個ViewHolder沒有綁定view的時候傳遞false給dispatchRecycled;換句話說就是,下面dispatchViewRecycled(holder);
的功能就是清除ViewHolder相關(guān)綁定的操作;另外我們再來看一下對于RecycledViewPool的文檔描述中有這樣一句話:RecycledViewPool lets you share Views between multiple RecyclerViews.
,即通過RecycledViewPool可以在不同的RecyclerView之間共享View(實際上是ViewHolder),所以,這里我們也就可以理解下面holder.mOwnerRecyclerView = null
清除與原來RecyclerView關(guān)聯(lián)的操作了(因為不清除的話,在多個RecyclerView之間共享就會出現(xiàn)問題);那么到這里我們對于RecycledViewPool中的ViewHolder就有了大致的了解了,總結(jié)一下就是: 當(dāng)一個ViewHolder被緩存進(jìn)入該pool的時候,除了其自身的View Type以外,其自身與外界的綁定關(guān)系,flag標(biāo)志,與原來RecyclerView的聯(lián)系等信息都被清除了,那么理所當(dāng)然的是,對于處于pool中的ViewHolder的查詢,就應(yīng)該通過View Type來確定了,也就是上面我們所說的
/**
* Pass false to dispatchRecycled for views that have not been bound.
* @param dispatchRecycled True to dispatch View recycled callbacks.
*/
void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
...
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE)) {
//標(biāo)志(flag)清除
holder.setFlags(0, ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE);
ViewCompat.setAccessibilityDelegate(holder.itemView, null);
}
if (dispatchRecycled) {
//綁定清除
dispatchViewRecycled(holder);
}
//與RecyclerView的聯(lián)系清除
holder.mOwnerRecyclerView = null;
//緩存入pool
getRecycledViewPool().putRecycledView(holder);
}
- 下面我們應(yīng)該順著這條線索,繼續(xù)搜索哪種情況下會將一個ViewHolder扔進(jìn)RecycledViewPool中;這里筆者找到以下幾種情況:
- 在View Cache(第一級緩存)中的Item被更新或者被刪除時(即從Cache中移出的ViewHolder會進(jìn)入pool中);可以看出的時,更新和刪除操作時,將ViewHolder回收進(jìn)pool中都是通過recycleCachedViewAt()方法,如下可知,其只是調(diào)用了上面的ViewHolder清除工作,同時刪除了Cache中的緩存
//當(dāng)View Cache中Item更新時
//但是什么時候會更新呢: 可以想像的一種情況是當(dāng)有Item緩存進(jìn)入View Cache中時
void updateViewCacheSize() {
...
// first, try the views that can be recycled
for (int i = mCachedViews.size() - 1;
i >= 0 && mCachedViews.size() > mViewCacheMax; i--) {
recycleCachedViewAt(i);
}
}
//當(dāng)View Cache中Item刪除時
void recycleAndClearCachedViews() {
final int count = mCachedViews.size();
for (int i = count - 1; i >= 0; i--) {
recycleCachedViewAt(i);
}
mCachedViews.clear();
...
}
//該方法中調(diào)用了上面所說的回收進(jìn)pool中的清除工作,同時將Cache中的緩存刪除
void recycleCachedViewAt(int cachedViewIndex) {
....
addViewHolderToRecycledViewPool(viewHolder, true);
mCachedViews.remove(cachedViewIndex);
}
- LayoutManager在pre_layout過程中添加View,但是在post_layout過程中沒有添加該View;當(dāng)然,在尋找該過程對應(yīng)的源碼的時候,我們首先應(yīng)該弄清楚的是pre_layout和post_layout是什么(所以在繼續(xù)講解之前,筆者打算先講一個小插曲)
(2) 一個小插曲: pre_layout和post_layout
- 關(guān)于這兩者應(yīng)該看的是RecyclerView的onMeasure()方法;如下可知,onMeasure中主要是分為兩步,即dispatchLayoutStep1()和dispatchLayoutStep2();
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout.mAutoMeasure) {
...
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
// set dimensions in 2nd step. Pre-layout should happen with old dimensions for
// consistency
mLayout.setMeasureSpecs(widthSpec, heightSpec);
dispatchLayoutStep2();
...
} else {
...
}
}
- 我們先來看即dispatchLayoutStep1()中做的事情;該方法的注釋中我們知道其做的事情: (1). 處理Adapter的更新; (2). 決定是否是否使用動畫; (3). 存儲與當(dāng)前View相關(guān)的信息; (4). 進(jìn)行預(yù)布局(pre_layout); 這里很明顯,我們關(guān)注的重點應(yīng)該放在預(yù)布局上,從下面代碼中的注釋可以看出,預(yù)布局分為兩步: 第一步是找到所有沒有被remove的Item,進(jìn)行預(yù)布局準(zhǔn)備; 第二步是進(jìn)行真正的預(yù)布局,從源代碼注釋中,我們可以看出,預(yù)布局時會使用Adapter改變前的Item(包括其位置和數(shù)量)來布局,同時其使用的Layout尺寸也是改變前的尺寸(這點可以從上面onMeasure()方法中對dispatchLayoutStep2()方法的注釋可以看出(大意為: 預(yù)布局應(yīng)該發(fā)生在舊的尺寸上),這是為了和正真改變后的布局相對比,來決定Item的顯示(可能這里讀者還是不清楚pre_layout的作用,不要緊,下面會詳細(xì)解釋,這里需要了解的只是在該方法中所做的事情)
/**
* The first step of a layout where we;
* - process adapter updates
* - decide which animation should run
* - save information about current views
* - If necessary, run predictive layout and save its information
*/
private void dispatchLayoutStep1() {
...
//情況(1)和(2)
processAdapterUpdatesAndSetAnimationFlags();
//情況(3)
...
//情況(4): 預(yù)布局
if (mState.mRunSimpleAnimations) {
// Step 0: Find out where all non-removed items are, pre-layout
}
if (mState.mRunPredictiveAnimations) {
/**
Step 1: run prelayout: This will use the old positions of items. The layout manager is expected to layout everything, even removed items (though not to add removed items back to the container). This gives the pre-layout position of APPEARING views which come into existence as part of the real layout.
*/
}
}
- 接下來是實現(xiàn)真正的布局,即dispatchLayoutStep2()進(jìn)行的post_layout;可以看出,這里主要是對子View進(jìn)行Layout,需要注意的是,在onMeasure()中,在進(jìn)行dispatchLayoutStep2()操作之前,還進(jìn)行了
mLayout.setMeasureSpecs(widthSpec, heightSpec);
也就是設(shè)置改變后真正的布局尺寸;但是當(dāng)查看LayoutManager的onLayoutChildren()方法時,我們發(fā)現(xiàn)其是一個空方法,所以應(yīng)該找其實現(xiàn)類(這里以LinearLayoutManager為例)
/**
* The second layout step where we do the actual layout of the views for the final state.
* This step might be run multiple times if necessary (e.g. measure).
*/
private void dispatchLayoutStep2() {
...
// Step 2: Run layout
mLayout.onLayoutChildren(mRecycler, mState);
}
- LinearLayoutManager的onLayoutChildren()過程: 在其源碼中介紹了Layout算法: (1). 首先找到獲得焦點的ItemView; (2). 從后往前布局或者從前往后布局(這個主要是與滾動出屏幕的Item的回收方向相關(guān)); (3). 滾動; 其中最主要的是一個fill()方法
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// layout algorithm:
// 1) by checking children and other variables, find an anchor coordinate and an anchor item position.
// 2) fill towards start, stacking from bottom
// 3) fill towards end, stacking from top
// 4) scroll to fulfill requirements like stack from bottom.
...
fill(recycler, mLayoutState, state, false);
}
- fill()方法: 從其參數(shù)可以猜測的是,該方法與Item的填充和回收相關(guān);其主要過程是通過下面while循環(huán)中不斷的填充(layoutChunk)和回收Item(recycleByLayoutState)完成;而在recycleByLayoutState()中分為兩種情況處理:即向上滾動和向下滾動,其中回收的條件是當(dāng)Item滾動出屏幕且不可見時(在recycleViewsFromEnd()和recycleViewsFromStart()中都對滾動的邊界做了判斷),而最終回收調(diào)用的是recycleViewHolderInternal()方法;在recycleViewHolderInternal()中,其首先判斷了如果第一級緩存滿了的話,先將以前存入的Item移出,并存入Pool中,之后再緩存當(dāng)前Item;這里也就是對應(yīng)了RecycledViewPool緩存的第一種情況;還需要注意的是,當(dāng)Item正在執(zhí)行動畫的時,會導(dǎo)致回收失敗,此時會在ItemAnimatorRestoreListener.onAnimationFinished()中進(jìn)行回收
int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) { while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { ... layoutChunk(recycler, state, layoutState, layoutChunkResult); ... recycleByLayoutState(recycler, layoutState); } } private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) { if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { recycleViewsFromEnd(recycler, layoutState.mScrollingOffset); } else { recycleViewsFromStart(recycler, layoutState.mScrollingOffset); } } void recycleViewHolderInternal(ViewHolder holder) { ... int cachedViewSize = mCachedViews.size(); if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) { recycleCachedViewAt(0); //回收進(jìn)pool中 cachedViewSize--; } /存入第一級緩存 mCachedViews.add(targetCacheIndex, holder); ... }
- 在我們繼續(xù)進(jìn)行下一步分析之前,筆者想先來總結(jié)一下上面我們在尋找pre_layout和post_layout區(qū)別的時候所經(jīng)過的過程: 我們主要圍繞的是RecyclerView的onMeasure()方法,經(jīng)過了dispatchLayoutStep1()和dispatchLayoutStep2()兩個主要的過程,前一個負(fù)責(zé)預(yù)布局(pre_layout),后一個負(fù)責(zé)真正的布局(post_layout);其實到這里,布局過程還沒有真正的完成,因為我們還沒有弄清楚的是Item的滾動動畫
- onMeasure過程之后,我們應(yīng)該將目光聚焦在layout過程,在RecyclerView的onLayout()方法中,其關(guān)鍵的是調(diào)用了dispatchLayout(),關(guān)于該方法,源碼注釋給出了明確的說明:dispatchLayout()方法中封裝了與Item(出入)動畫相關(guān)的操作,當(dāng)重新布局(可能原因比如:Adapter改變,Item滑動等)之后,Item的改變類型大概有一下幾種: (1). PERSISTENT: 即在pre_layout和post_layout中都是可見的(由animatePersistence()方法處理); (2). REMOVED: 在pre_layout中可見,但是被刪除了(對應(yīng)數(shù)據(jù)的刪除)(由animateChange()方法處理);(3). ADDED: 在pre_layout中不存在,但是被添加進(jìn)的Item(對應(yīng)數(shù)據(jù)的添加)(由animateChange()方法處理); (4). DISAPPEARING: 數(shù)據(jù)集沒有改變,但是Item由可見變?yōu)椴豢梢?即Item滑動出屏幕)(由animateDisappearance()方法處理); (5). APPEARING: 數(shù)據(jù)集沒有改變,但是Item由不可見變?yōu)榭梢?對應(yīng)Item滑動進(jìn)入屏幕)(由animateAppearance()方法處理);
- 但是我們最終追尋下去,可以看出的是在dispatchLayout()中,又將一系列處理完全交給了dispatchLayoutStep3()方法來處理;從下面代碼中可以看出,其最終通過回調(diào)ViewInfoStore.ProcessCallback來處理上面的四種動畫
private void dispatchLayoutStep3() {
...
// Step 4: Process view info lists and trigger animations
mViewInfoStore.process(mViewInfoProcessCallback);
}
- 到這里為止,我們對于pre_layout和post_layout的區(qū)別應(yīng)該很清楚了;這里舉個例子來進(jìn)一步理解一下: 考慮一種情況,如果現(xiàn)在界面上有兩個Item a,b,并且占滿了屏幕,此時如果刪除b使得c需要進(jìn)入界面的話,那么我們雖然知道c的最終位置,但是我們?nèi)绾沃纁該從哪里滑入屏幕呢,很明顯,不可能默認(rèn)都從底部開始滑入,因為很明顯的是還有其他情況;所以在這里Google的解決辦法是請求兩個布局: pre_layout和post_layout; 當(dāng)Adapter改變即這里的b被刪除的時候,作為一個事件觸發(fā),此時pre_layout將加載c(但是此時c仍然是不可見的),然后在post_layout中去加載改變后的Adapter的正常布局,通過前后兩個布局對c位置的比較,我們就可以知道c該從哪里滑入;另外,還有一種情況是,如果b只是被改變了呢(并沒有被刪除),那么此時,pre_layout仍然會加載c,因為b的改變可能會引起b高度的改變而使得c有機(jī)會進(jìn)入界面;但是,當(dāng)Adapter改變完成之后,發(fā)現(xiàn)b并沒有改變高度,換句話說,就是c還是不能進(jìn)入界面的時候,此時Item c將被扔進(jìn)該pool,這種情況也就是上面說的RecycledViewPool進(jìn)行回收的第2種情況;話不多說,繼續(xù)分析(萬里長征還未過半...)
- 我們繼續(xù)進(jìn)入mViewInfoStore.process()方法,該方法屬于ViewInfoStore類,對于該類的描述是:對View進(jìn)行跟蹤并運行相關(guān)動畫,進(jìn)一步解釋就是執(zhí)行Item改變過程中的一些動畫;繼續(xù)看其在process()方法做了什么:其實在該方法中進(jìn)行了許多的情況的判斷,這里筆者只是抽取出了對應(yīng)當(dāng)前情況的處理,可以看出,當(dāng)
similar to appear disappear but happened between different layout passes
時,只是簡單的調(diào)用了ProcessCallback.unused(),而在unused()中,也只是對Item進(jìn)行了回收(如下);但是,值得注意的是,ViewInfoStore.process()方法進(jìn)行的處理,遠(yuǎn)不止如此,實際上,我們還有意外收獲,這里只需要記住該方法就好了,具體,下面還會再分析
void process(ProcessCallback callback) {
...
// similar to appear disappear but happened between different layout passes.
// this can happen when the layout manager is using auto-measure
callback.unused(viewHolder);
...
}
@Override
public void unused(ViewHolder viewHolder) {
mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler);
}
- 最后筆者還想附帶提一下的是,關(guān)于Item出入屏幕動畫處理的那幾個方法(即上面的animatePersistence(),animateChange()等)都是位于ItemAnimator中,這是一個abstract的類,如果想要自定義的Item的出入動畫的話,可以繼承該類,并通過recyclerView.setItemAnimator();來進(jìn)行設(shè)置
(1-). 又見RecycledViewPool緩存
- 這里插曲可能稍微長了一點,但是,筆者感覺這是值得的;現(xiàn)在,讓我們繼續(xù)最初的話題: 什么情況下一個ViewHolder會被扔進(jìn)Pool中呢?這里筆者再次回顧一下:
- 在View Cache中的Item被更新或者被刪除時(存滿溢出時)
- LayoutManager在pre_layout過程中添加View,但是在post_layout過程中沒有添加該View(數(shù)據(jù)集改變,如刪除)
- 到這里RecyclerView的第三級緩存差不多就分析完了,接下來,我們再看一下與其緊密相關(guān)的第一級緩存
(3). View Cache緩存
- View Cache也叫第一級緩存,主要指的是RecyclerView.Recycler中的mCachedViews字段,它是一個ArrayList,不區(qū)分view type,默認(rèn)容量是2,但是可以通過RecyclerView的setItemViewCacheSize()方法來設(shè)置
- 對于Recycler類的第一級緩存,我們需要注意的是以下三個字段
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
}
- 現(xiàn)在,我們需要看的是什么時候,mCachedViews會緩存ViewHolder,通過追蹤,可以發(fā)現(xiàn),只有在
recycleViewHolderInternal()
中調(diào)用了mCachedViews.add()
,而該方法上面分析第三級緩存的時候,分析的是,當(dāng)Item被移出屏幕區(qū)域時,先是緩存進(jìn)了mCachedViews中,因為處于mCachedViews中的ViewHolder是希望被原樣重用的;之所以這樣說,是因為從 recycleViewHolderInternal() 的源碼中可以看出,在 mCachedViews.add() 之前并沒有像上面存入第三級緩存之前那樣進(jìn)行一系列的清理工作,也就是說ViewHolder相關(guān)的和重要的position,flag等標(biāo)志都一并被緩存了;那么,從mCachedViews中取出的ViewHolder就不需要再進(jìn)行綁定操作而可以直接使用了(實際上所以我們期望的也是在mCachedViews中的ViewHolder能夠被重用,并且還是在它原來的位置被重用,這樣就不需要再去bind了;) - 至于mChangedScrap和mAttachedScrap緩存的話,我們也可以從其add()方法入手(如下),可以看出,一個ViewHolder是緩存進(jìn)入mChangedScrap還是mAttachedScrap,取決于其狀態(tài),如果一個Item被移除或者非法(如:與其view type 類型不再相符等),那么就會被放進(jìn)mAttachedScrap中,反之,則進(jìn)入mChangedScrap;說的更明顯一點就是,如果如果一個Item被移除,那么就會被放進(jìn)mAttachedScrap中,如果調(diào)用了notifXXX()之類的方法,那么需要改變的ViewHolder就被放進(jìn)mChangedScrap中
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
...
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}
- 第二級緩存相對來說比較簡單,所以就暫時分析到這里
(4). ViewCacheExtension
- 交由用戶決定的緩存,也是第二級緩存
- 從文檔對其的描述中可以看出的是,這是一個用戶自定義邏輯的緩存類,在查找一個緩存的ViewHolder的時候,會按照mCachedViews -> ViewCacheExtension -> RecycledViewPool的順序來查找
- 這是一個abstract的類,使用的時候,只需要實現(xiàn)一個
View getViewForPositionAndType(Recycler recycler, int position, int type);
方法 - 下面,我們通過一個例子來看一下什么時候可以使用該緩存:(注: 下面的例子來源于文末的參考文章)考慮現(xiàn)在有這樣的一些Item
- 其position固定(比如廣告之類)
- 不會改變(view type等)
- 數(shù)量合理,以便可以保存在內(nèi)存中
現(xiàn)在,為了避免這些Item的重復(fù)綁定,就可以使用ViewCacheExtension(需要注意的是,這里不能使用RecycledViewPool,因為其緩存的ViewHolder需要重新綁定,同時也能使用View Cache,因為其中的ViewHolder是不區(qū)分view type的),比如下面的示例代碼
SparseArray<View> specials = new SparseArray<>();
...
recyclerView.getRecycledViewPool().setMaxRecycledViews(SPECIAL, 0);
recyclerView.setViewCacheExtension(new RecyclerView.ViewCacheExtension() {
@Override
public View getViewForPositionAndType(RecyclerView.Recycler recycler,
int position, int type) {
return type == SPECIAL ? specials.get(position) : null;
}
});
...
class SpecialViewHolder extends RecyclerView.ViewHolder {
...
public void bindTo(int position) {
...
specials.put(position, itemView);
}
}
(5). 小結(jié)
- 到這里為止,RecyclerView三級緩存相關(guān)的源碼分析就結(jié)束了;但是由于筆者能力有限,很多細(xì)節(jié)和理解可能不到位,更多的還是需要自己動手多看源碼:)