前言: 生命總是要有信仰,有夢想才能一直前行,哪怕走的再慢,也是在前進。
一、概述
RecyclerView作爲官方指定的高效、高拓展性的列表控件,做了很好的封裝,靈活好用,深受我們喜歡。官方對它的介紹:爲大量數據提供有限展示窗口的靈活視圖。要想在有限的手機內存中展示大量的數據,並且保證不會OOM,它是怎麼做到的呢?
我們在adapter的onCreateViewHolder()
和onBindViewHolder()
分別打印了log,其中,onCreateViewHolder()
會在創建一個新view的時候調用,onBindViewHolder()
會在已存在view,綁定數據的時候調用。所以,如果是新創建的view,會調用onCreateViewHolder()
來創建view,調用onBindViewHolder()
來綁定數據;如果是複用的view,則不會調用onCreateViewHolder()
創建方法,只會調用onBindViewHolder()
綁定數據。
private int sum = 0;
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
Log.e("LinearVerticalAdapter", "onCreateViewHolder == " + sum);
sum += 1;
······
return null;
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
Log.e("LinearVerticalAdapter", "onBindViewHolder");
······
}
第一次進入的時候打印數據如下:
這裏發現打印了13條數據(我手機屏幕滿屏是13條數據),都走了onCreateViewHolder()
和onBindViewHolder()
方法;當我們來回滾動的時候,發現只走了onBindViewHolder()
綁定數據的方法,沒有走onCreateViewHolder()
創建ViewHolder的方法:
因爲RecyclerView能夠自動回收複用,這必須有強大的緩存機制支撐,RecyclerView的緩存機制是RecyclerView的核心部分。這裏圍繞RecyclerView的緩存機制來談一談,RecyclerView的回收複用機制是怎麼樣的?
爲了方便下面文章的理解,我們先了解幾個方法的含義:
方法 | 對應Flag | 含義 | 出現場景 |
---|---|---|---|
isInvalid() | FLAG_INVALID | ViewHolder的數據是無效的 | 1.調用adapter的setAdapter() 2.adapter調用了notifyDataSetChanged(); 3.調用RecyclerView的invalidateItemDecorations()。 |
isRemoved() | FLAG_REMOVED | ViewHolder已經被移除,源數據被移除了部分數據 | adapter調用了notifyItemRemoved() |
isUpdated() | FLAG_UPDATE | item的ViewHolder數據信息過時了,需要重新綁定數據 | 1.上述isInvalid()的三種情況都會; 2.調用adapter的onBindViewHolder(); 3.調用了adapter的notifyItemChanged()。 |
isBound() | FLAG_BOUND | ViewHolder已經綁定了某個位置的item上,數據是有效的 | 調用了onBindViewHolder()方法 |
二、Recycler的幾級緩存
RecyclerView不需要像ListView那樣if(contentView==null) {}else{}
處理複用的邏輯,它回收複用是由Recycler來負責的,Recycler是負責管理scrapped(廢棄)或者detached(分離)的視圖(ViewHolder)以便重複使用。要想了解RecyclerView的回收複用原理,那麼首先了解Recycler的幾個結構體:
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
private final List<ViewHolder>
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
int mViewCacheMax = DEFAULT_CACHE_SIZE;
RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
static final int DEFAULT_CACHE_SIZE = 2;
}
Recycler中設置了四層緩存池,按照使用的優先級順序依次是Scrap、CacheView、ViewCacheExtension、RecycledViewPool;其中Scrap包括mAttachedScrap和mChangedScrap,ViewCacheExtension是默認沒有實現的,它RecyclerView留給開發者拓展的回收池。
● mAttachedScrap: 不參與滑動時的回收複用,只保存重新佈局時從RecyclerView分離的item的無效、未移除、未更新的holder。因爲RecyclerView在onLayout的時候,會先把children全部移除掉,再重新添加進入,mAttachedScrap臨時保存這些holder複用。
● mChangedScrap: mChangedScrap和mAttachedScrap類似,不參與滑動時的回收複用,只是用作臨時保存的變量,它只會負責保存重新佈局時發生變化的item的無效、未移除的holder,那麼會重走adapter綁定數據的方法。
● mCachedViews : 用於保存最新被移除(remove)的ViewHolder,已經和RecyclerView分離的視圖;它的作用是滾動的回收複用時如果需要新的ViewHolder時,精準匹配(根據position/id判斷)是不是原來被移除的那個item;如果是,則直接返回ViewHolder使用,不需要重新綁定數據;如果不是則不返回,再去mRecyclerPool中找holder實例返回,並重新綁定數據。這一級的緩存是有容量限制的,最大數量爲2。
● mViewCacheExtension: RecyclerView給開發者預留的緩存池,開發者可以自己拓展回收池,一般不會用到,用RecyclerView系統自帶的已經足夠了。
● mRecyclerPool: 是一個終極回收站,真正存放着被標識廢棄(其他池都不願意回收)的ViewHolder的緩存池,如果上述mAttachedScrap、mChangedScrap、mCachedViews、mViewCacheExtension都找不到ViewHolder的情況下,就會從mRecyclerPool返回一個廢棄的ViewHolder實例,但是這裏的ViewHolder是已經被抹除數據的,沒有任何綁定的痕跡,需要重新綁定數據。它是根據itemType來存儲的,是以SparseArray嵌套一個ArraryList的形式保存ViewHolder的。
接着我們來詳細分析一下各個緩存池:
2.1 緩存池一 (Scrap)
Scrap是RecyclerView最輕量的緩存,包括mAttachedScrap和mChangedScrap,它不參與列表滾動時的回收複用,作爲重新佈局時的臨時緩存,它的作用是,緩存當界面重新佈局前和界面重新佈局後都出現的ViewHolder,這些ViewHolder是無效、未移除、未標記的。在這些無效、未移除、未標記的ViewHolder之中,mAttachedScrap負責保存其中沒有改變的ViewHolder;剩下的由mChangedScrap負責保存。mAttachedScrap和mChangedScrap也只是分工合作保存不同ViewHolder而已。
注意:Scrap只是作爲佈局的臨時緩存,它和滑動時的緩存沒有任何關係,它的detach和atach只是臨時存在於佈局過程中。佈局結束時,Scrap列表應該是空的,緩存的數據要麼重新佈局出來,要麼被清空;總之在佈局結束後Scrap列表不應該存在任何東西。
我們上圖分析:
在一個手機屏幕中,將itemB刪除,並且調用notifyItemRemoved()
方法,如果item是無效並且被移除的就會回收到其他的緩存,否則都是緩存到Scrap中,那麼mAttachedScrap和mChangedScrap會分別存儲itemView,itemA沒有任何的變化,存儲到mAttachedScrap中,itemB雖然被移出了,但是還有效,也被存儲到mAttachedScrap中(但是會被標記REMOVED,之後會移除);itemC和itemD發生了變化,位置往上移動了,會被存儲到mChangedScrap中。刪除時,ABCD都會進入Scrap中;刪除後,ACD都會回來,A沒有任何變化,CD只是位置發生了變化,內容沒有發生變化。
RecyclerView的局部刷新就是依賴Scrap的臨時緩存,當我們通過notifyItemRemoved()
,notifyItemChanged()
通知item發生變化的時候,通過mAttachedScrap緩存沒有發生變化的ViewHolder,其他的則由mChangedScrap緩存,添加itemView的時候快速從裏面取出,完成局部刷新。注意,如果我們使用notifyDataSetChanged()
來通知RecyclerView刷新,屏幕上的itemView被標記爲FLAG_INVALID
並且未被移除,所以不會使用Scrap緩存,而是直接扔到CacheView或者RecycledViewPool池中,回來的時候重新走一次綁定數據。
注意:itemE並沒有出現在屏幕中,它不屬於Scrap管轄的範圍,Scrap只會換在屏幕中已經加載出來的itemView的holder。
2.2 緩存池二 (CacheView)
CacheView用於RecyclerView列表位置產生變動時,對剛剛移出屏幕的view進行回收複用。根據position/id來精準匹配是不是原來的item,如果是則直接返回使用,不需要重新綁定數據;如果不是則去RecycledViewPool中找holder實例返回,並且重新綁定數據。
CacheView的最大容量爲2,緩存一個新的ViewHolder時,如果超出了最大限制,那麼會將CacheView緩存的第一個數據添加到RecycledViewPool後再移除掉,最後纔會將新的ViewHolder添加進來。我們在滑動RecyclerView的時候,Recycler會不斷地緩存剛剛移出屏幕不可見的View到CacheView中,CacheView到達上限時又會不斷替換CacheView中舊的ViewHolder,將它們扔到RecycledViewPool中。如果一直朝一個方向滾動,CacheView並沒有在效率上產生幫助,它只是把後面滑過的ViewHolder緩存起來,如果經常來回滑動,那麼從CacheView根據對應位置的item直接複用,不需要重新綁定數據,將會得到很好的利用。
用圖來看看CacheView的複用場景:
從圖中可以看出,CacheView緩存剛剛變爲不可見的view,如果當前View再次進入屏幕中的時候,進行精準匹配,這個itemView還是 之前的itemView,那麼會從CacheView中獲取ViewHolder進行復用。如果一直向某一個方向滑動,那麼CacheView將會不斷替換緩存裏面的ViewHolder(CacheView最多隻能存儲2個),將替換掉的ViewHolder先放到RecycledViewPool中。在CacheView中拿不到複用的ViewHolder,那麼最後只能去RecycledViewPool中獲取。
2.3 緩存池三 (ViewCacheExtension)
ViewCacheExtension是緩存拓展的幫助類,額外提供了一層緩存池給開發者。開發者視情況而定是否使用ViewCacheExtension增加一層緩存池,Recycler首先去scrap和CacheView中尋找複用view,如果沒有就去ViewCacheExtension中尋找View,如果還是沒有找到,那麼最後去RecycledViewPool尋找複用的View。下面的講解將會不涉及ViewCacheExtension的知識,大家知道即可。
注意:Recycler並沒有將任何的view緩存到ViewCacheExtension中。所以在ViewCacheExtension中並沒有緩存任何數據。
2.4 緩存池四 (RecycledViewPool)
在Scrap、CacheView、ViewCacheExtension都不願意回收的時候,都會丟到RecycledViewPool中回收,所以RecycledViewPool是Recycler的終極回收站。
RecycledViewPool實際上是以SparseArray嵌套一個ArraryList的形式保存ViewHolder的,因爲RecycledViewPool保存的ViewHolder是以itemType來區分的。這樣方便不同的itemType保存不同的ViewHolder。它在回收的時候只是回收該viewType的ViewHolder對象,並沒有保存原來的數據信息,在複用的時候需要重新走onBindViewHolder()
方法重新綁定數據。
我們來看看RecycledViewPool的結構:
public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 5;
static class ScrapData {
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
}
SparseArray<ScrapData> mScrap = new SparseArray<>();
}
可以看出,RecycledViewPool中定義了SparseArray<ScrapData> mScrap
,它是一個根據不同itemType來保存靜態類ScrapData對象的SparseArray,ScrapData中包含了ArrayList<ViewHolder> mScrapHeap
,mScrapHeap是保存該itemType類型下ViewHolder的ArrayList。
緩存池定義了默認的緩存大小DEFAULT_MAX_SCRAP = 5
,這個數量不是說整個緩存池只能緩存這多個ViewHolder,而是不同itemType的ViewHolder的list的緩存數量,即mScrap的數量,說明最多隻有5組不同類型的mScrapHeap。mMaxScrap = DEFAULT_MAX_SCRAP
說明每種不同類型的ViewHolder默認保存5個,當然mMaxScrap的值是可以設置的。這樣RecycledViewPool就把不同ViewType的ViewHolder按類型分類緩存起來。
其實,Scrap緩存池不參與滾動的回收複用,CacheView緩存池被稱爲一級緩存,又因爲ViewCacheExtension緩存池是給開發者定義的緩存池,一般不用到,所以RecycledViewPool緩存池被稱爲二級緩存,那麼這樣來說實際只有兩層緩存。
三、源碼解析(回收和複用)
單單看上面的解釋可能比較抽象、生硬,不明白這段話所表達的意思。這裏我們結合源碼來分析一下RecyclerView的回收複用流程,跟着源碼走你會明白RecyclerView的緩存整體結構。以LinearLayoutManager爲例,在RecyclerView<6>對RecyclerView的佈局流程進行了分析,但是沒有涉及到RecyclerView的回收複用機制,我們知道RecyclerView的佈局和回收複用都是在RecyclerView.LayoutManager處理的。
3.1 回收流程
在LinearLayoutManager中,來到itemView佈局入口的方法onLayoutChildren()
:
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
if (state.getItemCount() == 0) {
removeAndRecycleAllViews(recycler);//移除所有子View
return;
}
}
ensureLayoutState();
mLayoutState.mRecycle = false;//禁止回收
//顛倒繪製佈局
resolveShouldLayoutReverse();
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
//暫時分離已經附加的view,即將所有child detach並通過Scrap回收
detachAndScrapAttachedViews(recycler);
}
在onLayoutChildren()
佈局的時候,先根據實際情況是否需要removeAndRecycleAllViews()
移除所有的子View,那些ViewHolder不可用;然後通過detachAndScrapAttachedViews()
暫時分離已經附加的ItemView,緩存到List中。
試想我們插入了item或者刪除了item亦或者打亂了列表的順序,怎麼重新佈局這些item呢?如何將屏幕上現有的item佈局到新的位置呢?最簡單的方法就是把每個item從屏幕中分離下來,保存着,然後按照位置要求重新排列上去。
detachAndScrapAttachedViews()
的作用就是把當前屏幕所有的item與屏幕分離,將他們從RecyclerView的佈局中拿下來,保存到list中,在重新佈局時,再將ViewHolder重新一個個放到新的位置上去。將屏幕上的ViewHolder從RecyclerView的佈局中拿下來後,存放在Scrap中,Scrap包括mAttachedScrap和mChangedScrap,它們是一個list,用來保存從RecyclerView佈局中拿下來ViewHolder列表,detachAndScrapAttachedViews()
只會在onLayoutChildren()
中調用,只有在佈局的時候,纔會把ViewHolder detach掉,然後再add進來重新佈局,但是大家需要注意,Scrap只是保存從RecyclerView佈局中當前屏幕顯示的item的ViewHolder,不參與回收複用,單純是爲了現從RecyclerView中拿下來再重新佈局上去。對於沒有保存到的item,會放到mCachedViews或者RecycledViewPool緩存中參與回收複用。
public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
遍歷所有view,分離所有已經添加到RecyclerView的itemView,Recycler先廢棄它們,然後再在緩存列表中拿出來複用。
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);//移除VIew
recycler.recycleViewHolderInternal(viewHolder);//緩存到CacheView或者RecycledViewPool中
} else {
detachViewAt(index);//分離View
recycler.scrapView(view);//scrap緩存
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
進入else分支,可以看到先detachViewAt()
分離視圖,然後再通過scrapView()
緩存到scrap中:
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);//保存到mAttachedScrap中
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);//保存到mChangedScrap中
}
}
進入if()分支的ViewHolder保存到mAttachedScrap中,else分支的保存到mChangedScrap中。
回到scrapOrRecycleView()
中,進入if()分支如果viewHolder是無效、未被移除、未被標記的則放到recycleViewHolderInternal()
緩存起來,同時removeViewAt()
移除了viewHolder,
void recycleViewHolderInternal(ViewHolder holder) {
·····
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();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {//如果超出容量限制,把第一個移除
recycleCachedViewAt(0);
cachedViewSize--;
}
·····
mCachedViews.add(targetCacheIndex, holder);//mCachedViews回收
cached = true;
}
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);//放到RecycledViewPool回收
recycled = true;
}
}
}
如果符合條件,會優先緩存到mCachedViews中時,如果超出了mCachedViews的最大限制,通過recycleCachedViewAt()
將CacheView緩存的第一個數據添加到終極回收池RecycledViewPool後再移除掉,最後纔會add()
新的ViewHolder添加到mCachedViews中。
剩下不符合條件的則通過addViewHolderToRecycledViewPool()
緩存到RecycledViewPool中。
void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
clearNestedRecyclerViewIfNotNested(holder);
View itemView = holder.itemView;
······
holder.mOwnerRecyclerView = null;
getRecycledViewPool().putRecycledView(holder);//將holder添加到RecycledViewPool中
}
還有一個就是在填充佈局fill()
的時候,它會回收移出屏幕的view到mCachedViews或者RecycledViewPool中:
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
recycleByLayoutState(recycler, layoutState);//回收移出屏幕的view
}
}
在recycleByLayoutState()
層層追查下去,會來到recycler.recycleView(view)
Recycler的公共回收方法中,:
public void recycleView(@NonNull View view) {
ViewHolder holder = getChildViewHolderInt(view);
if (holder.isTmpDetached()) {
removeDetachedView(view, false);
}
recycleViewHolderInternal(holder);
}
回收分離的視圖到緩存池中,方便以後重新綁定和複用,這裏又來到了recycleViewHolderInternal(holder)
,和上面的一樣,按照優先級緩存 mCachedViews > RecycledViewPool。
那麼回收流程就到這裏結束了。
3.2 複用流程
itemView的回收流程分析完了,那麼這些回收的ViewHolder到底在什麼時候,什麼地方拿出來使用呢?回到LinearLayoutManager的佈局入口的方法onLayoutChildren()
:
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
if (state.getItemCount() == 0) {
removeAndRecycleAllViews(recycler);//移除所有子View
return;
}
}
//暫時分離已經附加的view,即將所有child detach並通過Scrap回收
detachAndScrapAttachedViews(recycler);
if (mAnchorInfo.mLayoutFromEnd) {
//描點位置從start位置開始填充ItemView佈局
updateLayoutStateToFillStart(mAnchorInfo);
fill(recycler, mLayoutState, state, false);//填充所有itemView
//描點位置從end位置開始填充ItemView佈局
updateLayoutStateToFillEnd(mAnchorInfo);
fill(recycler, mLayoutState, state, false);//填充所有itemView
endOffset = mLayoutState.mOffset;
}else {
//描點位置從end位置開始填充ItemView佈局
updateLayoutStateToFillEnd(mAnchorInfo);
fill(recycler, mLayoutState, state, false);
//描點位置從start位置開始填充ItemView佈局
updateLayoutStateToFillStart(mAnchorInfo);
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
}
}
回收view後,緊接着就是填充view了,上面提到,在重新佈局的時候會臨時將view緩存起來,再一個個把ViewHolder按照正確的位置填充上去。fill()
就是填充由layoutState定義的給定佈局:
int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
recycleByLayoutState(recycler, layoutState);//回收滑出屏幕的view
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {//一直循環,知道沒有數據
layoutChunkResult.resetInternal();
layoutChunk(recycler, state, layoutState, layoutChunkResult);//添加一個child
······
if (layoutChunkResult.mFinished) {//佈局結束,退出循環
break;
}
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;//根據添加的child高度偏移計算
}
······
return start - layoutState.mAvailable;//返回這次填充的區域大小
}
判斷當前可見區域還有沒有剩餘空間,如果有則填充view上去,核心是通過while()
循環執行layoutChunk()
填充一個itemView到屏幕, layoutChunk()
完成佈局工作:
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);//獲取複用的view
······
}
該方法通過layoutState.next(recycler)
拿到視圖,我們看看它是怎麼拿到視圖的:
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
@NonNull
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
tryGetViewHolderForPositionByDeadline()
纔是獲取view的方法,它會根據給出的position/id去scrap、cache、RecycledViewPool、或者創建獲取一個ViewHolder:
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
ViewHolder holder = null;
// 0) 如果它是改變的廢棄的ViewHolder,在scrap的mChangedScrap找
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1)根據position分別在scrap的mAttachedScrap、mChildHelper、mCachedViews中查找
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}
if (holder == null) {
final int type = mAdapter.getItemViewType(offsetPosition);
// 2)根據id在scrap的mAttachedScrap、mCachedViews中查找
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
}
if (holder == null && mViewCacheExtension != null) {
//3)在ViewCacheExtension中查找,一般不用到,所以沒有緩存
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
}
}
//4)在RecycledViewPool中查找
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
//5)到最後如果還沒有找到複用的ViewHolder,則新建一個
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
這個方法確實做了不少事情,分別去scrap、CacheView、ViewCacheExtension、RecycledViewPool中獲取ViewHolder,如果沒有則創建一個新的ViewHolder返回,我們一步步來分析:
(1)第一步:如果是廢棄的發生改變的ViewHolder,則在scrap的mChangedScrap查找視圖,通過position和id分別查找;
這個一般在我們調用adapter的notifyItemChanged()
方法時,數據發生變化,item緩存在mChangedScrap中,後續拿到的ViewHolder需要重新綁定數據。
ViewHolder getChangedScrapViewForPosition(int position) {
//通過position
for (int i = 0; i < changedScrapSize; i++) {
final ViewHolder holder = mChangedScrap.get(i);
return holder;
}
// 通過id
if (mAdapter.hasStableIds()) {
final long id = mAdapter.getItemId(offsetPosition);
for (int i = 0; i < changedScrapSize; i++) {
final ViewHolder holder = mChangedScrap.get(i);
return holder;
}
}
return null;
}
(2)第二步:如果沒有找到視圖,根據position分別在scrap的mAttachedScrap、mChildHelper、mCachedViews中查找。在getScrapOrHiddenOrCachedHolderForPosition(position, dryRun)
這個方法按照以下順序查找:
- 首先從mAttachedScrap中查找,精準匹配有效的ViewHolder;
- 接着在mChildHelper中mHiddenViews查找隱藏的ViewHolder;
- 最後從我們的一級緩存中mCachedViews查找。
//根據position分別在scrap的mAttachedScrap、mChildHelper、mCachedViews中查找
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();
// 首先從mAttachedScrap中查找,精準匹配有效的ViewHolder
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
return holder;
}
//接着在mChildHelper中mHiddenViews查找隱藏的ViewHolder
if (!dryRun) {
View view = mChildHelper.findHiddenNonRemovedView(position);
if (view != null) {
final ViewHolder vh = getChildViewHolderInt(view);
scrapView(view);
return vh;
}
}
//最後從我們的一級緩存中mCachedViews查找。
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
return holder;
}
}
(3)第三步:如果沒有找到視圖,通過id在scrap的mAttachedScrap、mCachedViews中查找。在getScrapOrCachedViewForId()
這個方法按照以下順序:
- 首先從mAttachedScrap中查找,精準匹配有效的ViewHolder;
- 接着從我們的一級緩存中mCachedViews查找;
注意:這一步是跟id來查找的,與上一步根據position查找類似。
ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {
//在Scrap的mAttachedScrap中查找
final int count = mAttachedScrap.size();
for (int i = count - 1; i >= 0; i--) {
final ViewHolder holder = mAttachedScrap.get(i);
return holder;
}
//在一級緩存mCachedViews中查找
final int cacheSize = mCachedViews.size();
for (int i = cacheSize - 1; i >= 0; i--) {
final ViewHolder holder = mCachedViews.get(i);
return holder;
}
}
(4)第四步:在mViewCacheExtension中查找,前面提到這個緩存池是由開發者定義的一層緩存策略,Recycler並沒有將任何view緩存到這裏。這裏沒有定義過,所有找不到對應的view。
if (holder == null && mViewCacheExtension != null) {
final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
}
}
(5)第五步:從RecycledViewPool中查找,上面講過RecycledViewPool是通過itemType把ViewHolder的List緩存到SparseArray中的,在getRecycledViewPool().getRecycledView(type)
根據itemType從SparseArray獲取ScrapData ,然後再從裏面獲取ArrayList<ViewHolder>
,從而獲取到ViewHolder。
@Nullable
public ViewHolder getRecycledView(int viewType) {
final ScrapData scrapData = mScrap.get(viewType);//根據viewType獲取對應的ScrapData
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
for (int i = scrapHeap.size() - 1; i >= 0; i--) {
if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
return scrapHeap.remove(i);
}
}
}
return null;
}
(6)第六步:如果還沒有獲取到ViewHolder,則通過mAdapter.createViewHolder()
創建一個新的ViewHolder返回。
//5)到最後如果還沒有找到複用的ViewHolder,則新建一個
holder = mAdapter.createViewHolder(RecyclerView.this, type);
那麼複用流程到這裏也完畢了。
四、RecyclerVIew的回收複用原理
4.1 RecyclerVIew的回收原理
在RecyclerView重新佈局onLayoutChildren()
或者填充佈局fill()
的時候,會先把必要的item與屏幕分離或者移除,並做好標記,保存到list中,在重新佈局時,再將ViewHolde拿出來重新一個個放到新的位置上去。
(1)如果是RecyclerView不滾動情況下緩存(比如刪除item),重新佈局時,把屏幕上的ViewHolder與屏幕分離下來,存放到Scrap中,即發生改變的ViewHolder緩存到mChangedScrap中,不發生改變的ViewHolder存放到mAttachedScrap中;剩下ViewHolder的會按照mCachedViews>RecycledViewPool的優先級緩存到mCachedViews或者RecycledViewPool中。
(2)如果是RecyclerVIew滾動情況下緩存(比如滑動列表),在滑動時填充佈局,先移除滑出屏幕的item,第一級緩存mCachedViews優先緩存這些ViewHolder,但是mCachedViews最大容量爲2,當mCachedViews滿了以後,會利用先進先出原則,把舊的ViewHolder存放到RecycledViewPool中後移除掉,騰出空間,再將新的ViewHolder添加到mCachedViews中,最後剩下的ViewHolder都會緩存到終極回收池RecycledViewPool中,它是根據itemType來緩存不同類型的ArrayList<ViewHolder>
,最大容量爲5。
4.2 RecyclerVIew的複用原理
至此,已經有五個緩存RecyclerView的池子,mChangedScrap、mAttachedScrap、mCachedViews、mViewCacheExtension、mRecyclerPool,除了mViewCacheExtension是系統提供給開發者拓展的沒有用到之外,還有四個池子是參與到複用流程中的。
當RecyclerView要拿一個複用的ViewHolder時,如果是預加載,則會先去mChangedScrap中精準查找(分別根據position和id)對應的ViewHolder,如果有就返回,如果沒有就再去mAttachedScrap和mCachedViews中精確查找(先position後id)是不是原來的ViewHolder,如果是說明ViewHolder是剛剛被移除的,如果不是,則最終去mRecyclerPool找,如果itemType類型匹配對應的ViewHolder,那麼返回實例,讓它重新綁定數據,如果mRecyclerPool也沒有返回ViewHolder纔會調用createViewHolder()
重新去創建一個。
這裏需要注意:在mChangedScrap、mAttachedScrap、mCachedViews中拿到的ViewHolder都是精準匹配,但是mChangedScrap的是發生了變化的,需要調用onBindViewHolder()
重新綁定數據,mAttachedScrap和mCachedViews沒有發生變化,是直接使用的,不需要重新綁定數據,而mRecyclerPool中的ViewHolder的內容信息已經被抹除,需要重新綁定數據。所以在RecyclerView來回滾動時,mCachedViews緩存池的使用效率最高。
總的來說:RecyclerView着重在兩個場景緩存和回收的優化,一是:在數據更新時,使用Scrap進行局部更新,儘可能複用原來viewHolder,減少綁定數據的工作;二是:在滑動的時候,重複利用原來的ViewHolder,儘可能減少重複創建ViewHolder和綁定數據的工作。最終思想就是,能不創建就不創建,能不重新綁定就不重新綁定,儘可能減少重複不必要的工作。
整個過程大致如下:
至此!本文結束。
請尊重原創者版權,轉載請標明出處:https://blog.csdn.net/m0_37796683/article/details/105141373 謝謝!
相關文章:
理解RecyclerView(五)
● RecyclerView的繪製流程
理解RecyclerView(六)
● RecyclerView的滑動原理
理解RecyclerView(七)
● RecyclerView的嵌套滑動機制
理解RecyclerView(八)
● RecyclerView的回收複用緩存機制詳解
理解RecyclerView(九)
● RecyclerView的自定義LayoutManager