本文是RecyclerView
完全解析系列第三篇文章,內容是緊跟前兩篇:RecyclerView基本設計結構和 RecyclerView重新整理機制。
通過前面分析知道
LayoutManager
在佈局子View
時會向Recycler
索要一個ViewHolder
。但從Recycler
中獲取一個ViewHolder
的前提是Recycler
中要有ViewHolder
。那Recycler
中是如何有ViewHolder
的呢? 本文會分析兩個問題:
RecyclerView
的View
是在什麼時候放入到Recycler
中的。以及在Recycler
中是如何儲存的。LayoutManager
在向Recycler
獲取ViewHolder
時,Recycler
尋找ViewHolder
的邏輯是什麼。
即何時存、怎麼存
和何時取、怎麼取
的問題。何時取
已經很明顯了:LayoutManager
在佈局子View
時會從Recycler
中獲取子View
。 所以本文要理清的是其他3個問題。在文章繼續之前要知道Recycler
管理的基本單元是ViewHolder
,LayoutManager
操作的基本單元是View
,即ViewHolder
的itemview
。本文不會分析RecyclerView
動畫時view
的複用邏輯。
為了接下來的內容更容易理解,先回顧一下Recycler
的組成結構:
mChangedScrap
: 用來儲存RecyclerView
做動畫時,被detach的ViewHolder
。mAttachedScrap
: 用來儲存RecyclerView
做資料重新整理(notify
),被detach的ViewHolder
mCacheViews
:Recycler
的一級ViewHolder
快取。RecyclerViewPool
:mCacheViews
集合中裝滿時,會放到這裡。
先看一下如何從Recycler
中取一個ViewHolder
來複用。
從Recycler中獲取一個ViewHolder的邏輯
LayoutManager
會呼叫Recycler.getViewForPosition(pos)
來獲取一個指定位置(這個位置是子View佈局所在的位置)的view
。getViewForPosition()
會呼叫tryGetViewHolderForPositionByDeadline(position...)
, 這個方法是從Recycler
中獲取一個View
的核心方法。它就是如何從Recycler中獲取一個ViewHolder
的邏輯,即怎麼取
, 方法太長, 我做了很多裁剪:
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
...
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); //從 attach 和 mCacheViews 中獲取
if (holder != null) {
... //校驗這個holder是否可用
}
}
if (holder == null) {
...
final int type = mAdapter.getItemViewType(offsetPosition); //獲取這個位置的資料的型別。 子Adapter複寫的方法
// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) { //stable id 就是標識一個viewholder的唯一性, 即使它做動畫改變了位置
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), //根據 stable id 從 scrap 和 mCacheViews中獲取
type, dryRun);
....
}
if (holder == null && mViewCacheExtension != null) { // 從使用者自定義的快取集合中獲取
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type); //你返回的View要是RecyclerView.LayoutParams屬性的
if (view != null) {
holder = getChildViewHolder(view); //把它包裝成一個ViewHolder
...
}
}
if (holder == null) { // 從 RecyclerViewPool中獲取
holder = getRecycledViewPool().getRecycledView(type);
...
}
if (holder == null) {
...
//實在沒有就會建立
holder = mAdapter.createViewHolder(RecyclerView.this, type);
...
}
}
...
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) { //動畫時不會想去呼叫 onBindData
...
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
...
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs); //呼叫 bindData 方法
}
final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
final LayoutParams rvLayoutParams;
...調整LayoutParams
return holder;
}
複製程式碼
即大致步驟是:
- 如果執行了
RecyclerView
動畫的話,嘗試根據position
從mChangedScrap集合
中尋找一個ViewHolder
- 嘗試
根據position
從scrap集合
、hide的view集合
、mCacheViews(一級快取)
中尋找一個ViewHolder
- 根據
LayoutManager
的position
更新到對應的Adapter
的position
。 (這兩個position
在大部分情況下都是相等的,不過在子view刪除或移動
時可能產生不對應的情況) - 根據
Adapter position
,呼叫Adapter.getItemViewType()
來獲取ViewType
- 根據
stable id(用來表示ViewHolder的唯一,即使位置變化了)
從scrap集合
和mCacheViews(一級快取)
中尋找一個ViewHolder
- 根據
position和viewType
嘗試從使用者自定義的mViewCacheExtension
中獲取一個ViewHolder
- 根據
ViewType
嘗試從RecyclerViewPool
中獲取一個ViewHolder
- 呼叫
mAdapter.createViewHolder()
來建立一個ViewHolder
- 如果需要的話呼叫
mAdapter.bindViewHolder
來設定ViewHolder
。 - 調整
ViewHolder.itemview
的佈局引數為Recycler.LayoutPrams
,並返回Holder
雖然步驟很多,邏輯還是很簡單的,即從幾個快取集合中獲取ViewHolder
,如果實在沒有就建立。但比較疑惑的可能就是上述ViewHolder快取集合
中什麼時候會儲存ViewHolder
。接下來分幾個RecyclerView
的具體情形,來一點一點弄明白這些ViewHolder快取集合
的問題。
情形一 : 由無到有
即一開始RecyclerView
中沒有任何資料,新增資料來源後adapter.notifyXXX
。狀態變化如下圖:
很明顯在這種情形下Recycler
中是不會存在任何可複用的ViewHolder
。所以所有的ViewHolder
都是新建立的。即會呼叫Adapter.createViewHolder()和Adapter.bindViewHolder()
。那這些建立的ViewHolder
會快取起來嗎?
這時候新建立的這些ViewHolder
是不會被快取起來的。 即在這種情形下: Recycler只會通過Adapter建立ViewHolder,並且不會快取這些新建立的ViewHolder
情形二 : 在原有資料的情況下進行整體重新整理
就是下面這種狀態:
其實就是相當於使用者在feed中做了下拉重新整理。實現中的虛擬碼如下:
dataSource.clear()
dataSource.addAll(newList)
adapter.notifyDatasetChanged()
複製程式碼
在這種情形下猜想Recycler
肯定複用了老的卡片(卡片的型別不變),那麼問題是 : 在使用者重新整理時舊ViewHolder
儲存在哪裡? 如何呼叫舊ViewHolder
的Adapter.bindViewHolder()
來重新設定資料的?
其實在上一篇文章Recycler重新整理機制
中,LinearLayoutManager
在確定好佈局錨點View
之後就會把當前attach
在RecyclerView
上的子View
全部設定為scrap狀態
:
void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection); // RecyclerView指定錨點,要準備正式佈局了
detachAndScrapAttachedViews(recycler); // 在開始佈局時,把所有的View都設定為 scrap 狀態
...
}
複製程式碼
什麼是scrap狀態呢? 在前面的文章其實已經解釋過: ViewHolder被標記為FLAG_TMP_DETACHED
狀態,並且其itemview
的parent
被設定為null
。
detachAndScrapAttachedViews
就是把所有的view儲存到Recycler
的mAttachedScrap
集合中:
public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
for (int i = getChildCount() - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
...刪去了一些判斷邏輯
detachViewAt(index); //設定RecyclerView這個位置的view的parent為null, 並標記ViewHolder為FLAG_TMP_DETACHED
recycler.scrapView(view); //新增到mAttachedScrap集合中
...
}
複製程式碼
所以在這種情形下LinearLayoutManager
在真正擺放子View
之前,會把所有舊的子View
按順序儲存到Recycler
的mAttachedScrap集合
中
接下來繼續看,LinearLayoutManager
在佈局時如何複用mAttachedScrap集合
中的ViewHolder
。
前面已經說了LinearLayoutManager
會當前佈局子View的位置向Recycler
要一個子View,即呼叫到tryGetViewHolderForPositionByDeadline(position..)
。我們上面已經列出了這個方法的邏輯,其實在前面的第二步:
嘗試根據position
從scrap集合
、hide的view集合
、mCacheViews(一級快取)
中尋找一個ViewHolder
即從mAttachedScrap
中就可以獲得一個ViewHolder
:
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
...
}
複製程式碼
即如果mAttachedScrap中holder
的位置和入參position
相等,並且holder
是有效的話這個holder
就是可以複用的。所以綜上所述,在情形二下所有的ViewHolder
幾乎都是複用Recycler中mAttachedScrap集合
中的。
並且重新佈局完畢後Recycler
中是不存在可複用的ViewHolder
的。
情形三 : 滾動複用
這個情形分析是在情形二
的基礎上向下滑動時ViewHolder
的複用情況以及Recycler
中ViewHolder
的儲存情況, 如下圖:
在這種情況下滾出螢幕的View會優先儲存到mCacheViews
, 如果mCacheViews
中儲存滿了,就會儲存到RecyclerViewPool
中。
在前一篇文章RecyclerView重新整理機制
中分析過,RecyclerView
在滑動時會呼叫LinearLayoutManager.fill()
方法來根據滾動的距離來向RecyclerView
填充子View,其實在個方法在填充完子View之後就會把滾動出螢幕的View做回收:
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {
...
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
...
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
...
layoutChunk(recycler, state, layoutState, layoutChunkResult); //填充一個子View
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState); //根據滾動的距離來回收View
}
}
}
複製程式碼
即fill
每填充一個子View
都會呼叫recycleByLayoutState()
來回收一個舊的子View
,這個方法在層層呼叫之後會呼叫到Recycler.recycleViewHolderInternal()
。這個方法是ViewHolder
回收的核心方法,不過邏輯很簡單:
- 檢查
mCacheViews集合
中是否還有空位,如果有空位,則直接放到mCacheViews集合
- 如果沒有的話就把
mCacheViews集合
中最前面的ViewHolder
拿出來放到RecyclerViewPool
中,然後再把最新的這個ViewHolder放到mCacheViews集合
- 如果沒有成功快取到
mCacheViews集合
中,就直接放到RecyclerViewPool
mCacheViews集合
為什麼要這樣快取? 看一下下面這張圖 :
我是這樣認為的,如上圖,往上滑動一段距離,被滑動出去的ViewHolder
會被快取在mCacheViews集合
,並且位置是被記錄的。如果使用者此時再下滑的話,可以參考文章開頭的從Recycler
中獲取ViewHolder的邏輯:
- 先按照位置從
mCacheViews集合
中獲取 - 按照
viewType
從mCacheViews集合
中獲取
上面對於mCacheViews集合
兩步操作,其實第一步就已經命中了快取的ViewHolder
。並且這時候都不需要呼叫Adapter.bindViewHolder()
方法的。即是十分高效的。
所以在普通的滾動複用的情況下,ViewHolder
的複用主要來自於mCacheViews集合
, 舊的ViewHolder
會被放到mCacheViews集合
, mCacheViews集合
擠出來的更老的ViewHolder
放到了RecyclerViewPool
中
到這裡基本的複用情形都覆蓋了,其他的就涉及到RecyclerView動畫
了。這些點在下一篇文章繼續看。
歡迎關注我的Android進階計劃。看更多幹貨。