序言
- RecyclerView有三大典型的功能,一個是Recycler的緩存機制,一個LayoutManager的布局管理,一個ItemDecoration的分割線繪制;本文將結(jié)合源碼講解其緩存機制
- 更多相關的源碼解析見RecyclerView之ItemDecoration
正文
緩存機制
(1). RecycledViewPool的緩存
- RecycledViewPool也叫第三級緩存
- 文檔中說的是: 為多個RecyclerView提供的一個共用緩存池,如果想要通過RecyclerView緩存View的話,可以自己提供一個RecycledViewPool實例,并通過RecyclerView的setRecycledViewPool()方法來設置,如果不主動提供的話,RecyclerView會為自己主動創(chuàng)建一個
- 首先來看其緩存方式: 其中有一個 SparseArray<ScrapData> 類型的mScrap來緩存ViewHolder,每一個View Type 類型的Item都會有一個該緩存(源碼如下),默認最大容量為5,但是可以通過
recyclerView.getRecycledViewPool().setMaxRecycledViews(int viewType, int max);
來設置;(作者推薦的是:如果屏幕上有很多相同類型的ItemView同時改變,那么推薦將該容量設置大一些,但是如果有一種類型的ItemView很少出現(xiàn),并且不超過一個,那么推薦將該容量設置為1,否則其遲早會被填滿而造成內(nèi)存浪費)
static class ScrapData {
ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP; //每個View Type默認容量為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()方法,那么就默認只有一種View Type,默認為-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放進RecycledViewPool之前,都會做什么處理(主要代碼如下);需要注意的是,下面的注釋中有這樣一句話:
Pass false to dispatchRecycled for views that have not been bound.
,大意為:當一個ViewHolder沒有綁定view的時候傳遞false給dispatchRecycled;換句話說就是,下面dispatchViewRecycled(holder);
的功能就是清除ViewHolder相關綁定的操作;另外我們再來看一下對于RecycledViewPool的文檔描述中有這樣一句話:RecycledViewPool lets you share Views between multiple RecyclerViews.
,即通過RecycledViewPool可以在不同的RecyclerView之間共享View(實際上是ViewHolder),所以,這里我們也就可以理解下面holder.mOwnerRecyclerView = null
清除與原來RecyclerView關聯(lián)的操作了(因為不清除的話,在多個RecyclerView之間共享就會出現(xiàn)問題);那么到這里我們對于RecycledViewPool中的ViewHolder就有了大致的了解了,總結(jié)一下就是: 當一個ViewHolder被緩存進入該pool的時候,除了其自身的View Type以外,其自身與外界的綁定關系,flag標志,與原來RecyclerView的聯(lián)系等信息都被清除了,那么理所當然的是,對于處于pool中的ViewHolder的查詢,就應該通過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)) {
//標志(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);
}
- 下面我們應該順著這條線索,繼續(xù)搜索哪種情況下會將一個ViewHolder扔進RecycledViewPool中;這里筆者找到以下幾種情況:
- 在View Cache(第一級緩存)中的Item被更新或者被刪除時(即從Cache中移出的ViewHolder會進入pool中);可以看出的時,更新和刪除操作時,將ViewHolder回收進pool中都是通過recycleCachedViewAt()方法,如下可知,其只是調(diào)用了上面的ViewHolder清除工作,同時刪除了Cache中的緩存
//當View Cache中Item更新時
//但是什么時候會更新呢: 可以想像的一種情況是當有Item緩存進入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);
}
}
//當View Cache中Item刪除時
void recycleAndClearCachedViews() {
final int count = mCachedViews.size();
for (int i = count - 1; i >= 0; i--) {
recycleCachedViewAt(i);
}
mCachedViews.clear();
...
}
//該方法中調(diào)用了上面所說的回收進pool中的清除工作,同時將Cache中的緩存刪除
void recycleCachedViewAt(int cachedViewIndex) {
....
addViewHolderToRecycledViewPool(viewHolder, true);
mCachedViews.remove(cachedViewIndex);
}
- LayoutManager在pre_layout過程中添加View,但是在post_layout過程中沒有添加該View;當然,在尋找該過程對應的源碼的時候,我們首先應該弄清楚的是pre_layout和post_layout是什么(所以在繼續(xù)講解之前,筆者打算先講一個小插曲)
(2) 一個小插曲: pre_layout和post_layout
- 關于這兩者應該看的是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). 存儲與當前View相關的信息; (4). 進行預布局(pre_layout); 這里很明顯,我們關注的重點應該放在預布局上,從下面代碼中的注釋可以看出,預布局分為兩步: 第一步是找到所有沒有被remove的Item,進行預布局準備; 第二步是進行真正的預布局,從源代碼注釋中,我們可以看出,預布局時會使用Adapter改變前的Item(包括其位置和數(shù)量)來布局,同時其使用的Layout尺寸也是改變前的尺寸(這點可以從上面onMeasure()方法中對dispatchLayoutStep2()方法的注釋可以看出(大意為: 預布局應該發(fā)生在舊的尺寸上),這是為了和正真改變后的布局相對比,來決定Item的顯示(可能這里讀者還是不清楚pre_layout的作用,不要緊,下面會詳細解釋,這里需要了解的只是在該方法中所做的事情)
/**
* 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): 預布局
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()進行的post_layout;可以看出,這里主要是對子View進行Layout,需要注意的是,在onMeasure()中,在進行dispatchLayoutStep2()操作之前,還進行了
mLayout.setMeasureSpecs(widthSpec, heightSpec);
也就是設置改變后真正的布局尺寸;但是當查看LayoutManager的onLayoutChildren()方法時,我們發(fā)現(xiàn)其是一個空方法,所以應該找其實現(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的回收方向相關); (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的填充和回收相關;其主要過程是通過下面while循環(huán)中不斷的填充(layoutChunk)和回收Item(recycleByLayoutState)完成;而在recycleByLayoutState()中分為兩種情況處理:即向上滾動和向下滾動,其中回收的條件是當Item滾動出屏幕且不可見時(在recycleViewsFromEnd()和recycleViewsFromStart()中都對滾動的邊界做了判斷),而最終回收調(diào)用的是recycleViewHolderInternal()方法;在recycleViewHolderInternal()中,其首先判斷了如果第一級緩存滿了的話,先將以前存入的Item移出,并存入Pool中,之后再緩存當前Item;這里也就是對應了RecycledViewPool緩存的第一種情況;還需要注意的是,當Item正在執(zhí)行動畫的時,會導致回收失敗,此時會在ItemAnimatorRestoreListener.onAnimationFinished()中進行回收
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); //回收進pool中 cachedViewSize--; } /存入第一級緩存 mCachedViews.add(targetCacheIndex, holder); ... }
- 在我們繼續(xù)進行下一步分析之前,筆者想先來總結(jié)一下上面我們在尋找pre_layout和post_layout區(qū)別的時候所經(jīng)過的過程: 我們主要圍繞的是RecyclerView的onMeasure()方法,經(jīng)過了dispatchLayoutStep1()和dispatchLayoutStep2()兩個主要的過程,前一個負責預布局(pre_layout),后一個負責真正的布局(post_layout);其實到這里,布局過程還沒有真正的完成,因為我們還沒有弄清楚的是Item的滾動動畫
- onMeasure過程之后,我們應該將目光聚焦在layout過程,在RecyclerView的onLayout()方法中,其關鍵的是調(diào)用了dispatchLayout(),關于該方法,源碼注釋給出了明確的說明:dispatchLayout()方法中封裝了與Item(出入)動畫相關的操作,當重新布局(可能原因比如:Adapter改變,Item滑動等)之后,Item的改變類型大概有一下幾種: (1). PERSISTENT: 即在pre_layout和post_layout中都是可見的(由animatePersistence()方法處理); (2). REMOVED: 在pre_layout中可見,但是被刪除了(對應數(shù)據(jù)的刪除)(由animateChange()方法處理);(3). ADDED: 在pre_layout中不存在,但是被添加進的Item(對應數(shù)據(jù)的添加)(由animateChange()方法處理); (4). DISAPPEARING: 數(shù)據(jù)集沒有改變,但是Item由可見變?yōu)椴豢梢?即Item滑動出屏幕)(由animateDisappearance()方法處理); (5). APPEARING: 數(shù)據(jù)集沒有改變,但是Item由不可見變?yōu)榭梢?對應Item滑動進入屏幕)(由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ū)別應該很清楚了;這里舉個例子來進一步理解一下: 考慮一種情況,如果現(xiàn)在界面上有兩個Item a,b,并且占滿了屏幕,此時如果刪除b使得c需要進入界面的話,那么我們雖然知道c的最終位置,但是我們?nèi)绾沃纁該從哪里滑入屏幕呢,很明顯,不可能默認都從底部開始滑入,因為很明顯的是還有其他情況;所以在這里Google的解決辦法是請求兩個布局: pre_layout和post_layout; 當Adapter改變即這里的b被刪除的時候,作為一個事件觸發(fā),此時pre_layout將加載c(但是此時c仍然是不可見的),然后在post_layout中去加載改變后的Adapter的正常布局,通過前后兩個布局對c位置的比較,我們就可以知道c該從哪里滑入;另外,還有一種情況是,如果b只是被改變了呢(并沒有被刪除),那么此時,pre_layout仍然會加載c,因為b的改變可能會引起b高度的改變而使得c有機會進入界面;但是,當Adapter改變完成之后,發(fā)現(xiàn)b并沒有改變高度,換句話說,就是c還是不能進入界面的時候,此時Item c將被扔進該pool,這種情況也就是上面說的RecycledViewPool進行回收的第2種情況;話不多說,繼續(xù)分析(萬里長征還未過半...)
- 我們繼續(xù)進入mViewInfoStore.process()方法,該方法屬于ViewInfoStore類,對于該類的描述是:對View進行跟蹤并運行相關動畫,進一步解釋就是執(zhí)行Item改變過程中的一些動畫;繼續(xù)看其在process()方法做了什么:其實在該方法中進行了許多的情況的判斷,這里筆者只是抽取出了對應當前情況的處理,可以看出,當
similar to appear disappear but happened between different layout passes
時,只是簡單的調(diào)用了ProcessCallback.unused(),而在unused()中,也只是對Item進行了回收(如下);但是,值得注意的是,ViewInfoStore.process()方法進行的處理,遠不止如此,實際上,我們還有意外收獲,這里只需要記住該方法就好了,具體,下面還會再分析
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);
}
- 最后筆者還想附帶提一下的是,關于Item出入屏幕動畫處理的那幾個方法(即上面的animatePersistence(),animateChange()等)都是位于ItemAnimator中,這是一個abstract的類,如果想要自定義的Item的出入動畫的話,可以繼承該類,并通過recyclerView.setItemAnimator();來進行設置
(1-). 又見RecycledViewPool緩存
- 這里插曲可能稍微長了一點,但是,筆者感覺這是值得的;現(xiàn)在,讓我們繼續(xù)最初的話題: 什么情況下一個ViewHolder會被扔進Pool中呢?這里筆者再次回顧一下:
- 在View Cache中的Item被更新或者被刪除時(存滿溢出時)
- LayoutManager在pre_layout過程中添加View,但是在post_layout過程中沒有添加該View(數(shù)據(jù)集改變,如刪除)
- 到這里RecyclerView的第三級緩存差不多就分析完了,接下來,我們再看一下與其緊密相關的第一級緩存
(3). View Cache緩存
- View Cache也叫第一級緩存,主要指的是RecyclerView.Recycler中的mCachedViews字段,它是一個ArrayList,不區(qū)分view type,默認容量是2,但是可以通過RecyclerView的setItemViewCacheSize()方法來設置
- 對于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()
,而該方法上面分析第三級緩存的時候,分析的是,當Item被移出屏幕區(qū)域時,先是緩存進了mCachedViews中,因為處于mCachedViews中的ViewHolder是希望被原樣重用的;之所以這樣說,是因為從 recycleViewHolderInternal() 的源碼中可以看出,在 mCachedViews.add() 之前并沒有像上面存入第三級緩存之前那樣進行一系列的清理工作,也就是說ViewHolder相關的和重要的position,flag等標志都一并被緩存了;那么,從mCachedViews中取出的ViewHolder就不需要再進行綁定操作而可以直接使用了(實際上所以我們期望的也是在mCachedViews中的ViewHolder能夠被重用,并且還是在它原來的位置被重用,這樣就不需要再去bind了;) - 至于mChangedScrap和mAttachedScrap緩存的話,我們也可以從其add()方法入手(如下),可以看出,一個ViewHolder是緩存進入mChangedScrap還是mAttachedScrap,取決于其狀態(tài),如果一個Item被移除或者非法(如:與其view type 類型不再相符等),那么就會被放進mAttachedScrap中,反之,則進入mChangedScrap;說的更明顯一點就是,如果如果一個Item被移除,那么就會被放進mAttachedScrap中,如果調(diào)用了notifXXX()之類的方法,那么需要改變的ViewHolder就被放進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的重復綁定,就可以使用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三級緩存相關的源碼分析就結(jié)束了;但是由于筆者能力有限,很多細節(jié)和理解可能不到位,更多的還是需要自己動手多看源碼:)