本文是RecyclerView
徹底解析系列第三篇文章,內容是緊跟前兩篇:RecyclerView基本設計結構和 RecyclerView刷新機制。git
經過前面分析知道
LayoutManager
在佈局子View
時會向Recycler
索要一個ViewHolder
。但從Recycler
中獲取一個ViewHolder
的前提是Recycler
中要有ViewHolder
。那Recycler
中是如何有ViewHolder
的呢? 本文會分析兩個問題:github
RecyclerView
的View
是在何時放入到Recycler
中的。以及在Recycler
中是如何保存的。LayoutManager
在向Recycler
獲取ViewHolder
時,Recycler
尋找ViewHolder
的邏輯是什麼。即什麼時候存、怎麼存
和什麼時候取、怎麼取
的問題。什麼時候取
已經很明顯了:LayoutManager
在佈局子View
時會從Recycler
中獲取子View
。 因此本文要理清的是其餘3個問題。在文章繼續以前要知道Recycler
管理的基本單元是ViewHolder
,LayoutManager
操做的基本單元是View
,即ViewHolder
的itemview
。本文不會分析RecyclerView
動畫時view
的複用邏輯。緩存
爲了接下來的內容更容易理解,先回顧一下Recycler
的組成結構:bash
mChangedScrap
: 用來保存RecyclerView
作動畫時,被detach的ViewHolder
。mAttachedScrap
: 用來保存RecyclerView
作數據刷新(notify
),被detach的ViewHolder
mCacheViews
: Recycler
的一級ViewHolder
緩存。RecyclerViewPool
: mCacheViews
集合中裝滿時,會放到這裏。先看一下如何從Recycler
中取一個ViewHolder
來複用。ide
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;
}
複製代碼
即大體步驟是:post
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
。狀態變化以下圖:ui
很明顯在這種情形下Recycler
中是不會存在任何可複用的ViewHolder
。因此全部的ViewHolder
都是新建立的。即會調用Adapter.createViewHolder()和Adapter.bindViewHolder()
。那這些建立的ViewHolder
會緩存起來嗎?this
這時候新建立的這些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進階計劃。看更多幹貨。