上一篇文章分析RecyclerView刷新機制知道
LayoutManager在布局子View時會向Recycler索要一個ViewHolder。但從Recycler中獲取一個ViewHolder的前提是Recycler中要有ViewHolder。那Recycler中是如何有ViewHolder的呢?
本文會分析兩個問題:
-
RecyclerView的View是在什么時候放入到Recycler中的。以及在Recycler中是如何保存的。 -
LayoutManager在向Recycler獲取ViewHolder時,Recycler尋找ViewHolder的邏輯是什么。
即何時存、怎么存和何時取、怎么取的問題。何時取已經(jīng)很明顯了:LayoutManager在布局子View時會從Recycler中獲取子View。 所以本文要理清的是其他3個問題。在文章繼續(xù)之前要知道Recycler管理的基本單元是ViewHolder,LayoutManager操作的基本單元是View,即ViewHolder的itemview。本文不會分析RecyclerView動畫時view的復(fù)用邏輯。
為了接下來的內(nèi)容更容易理解,先回顧一下Recycler的組成結(jié)構(gòu):

-
mChangedScrap: 用來保存RecyclerView做動畫時,被detach的ViewHolder。 -
mAttachedScrap: 用來保存RecyclerView做數(shù)據(jù)刷新(notify),被detach的ViewHolder -
mCacheViews:Recycler的一級ViewHolder緩存。 -
RecyclerViewPool:mCacheViews集合中裝滿時,會放到這里。
先看一下如何從Recycler中取一個ViewHolder來復(fù)用。
從Recycler中獲取一個ViewHolder的邏輯
LayoutManager會調(diào)用Recycler.getViewForPosition(pos)來獲取一個指定位置(這個位置是子View布局所在的位置)的view。getViewForPosition()會調(diào)用tryGetViewHolderForPositionByDeadline(position...), 這個方法是從Recycler中獲取一個View的核心方法。它就是如何從Recycler中獲取一個ViewHolder的邏輯,即怎么取。方法太長, 我做了很多裁剪:
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
...
if (mState.isPreLayout()) { //動畫相關(guān)
holder = getChangedScrapViewForPosition(position); //從緩存中拿嗎?不應(yīng)該不是緩存?
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); //獲取這個位置的數(shù)據(jù)的類型。 子Adapter復(fù)寫的方法
// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) { //stable id 就是標(biāo)識一個viewholder的唯一性, 即使它做動畫改變了位置
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), //根據(jù) 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) {
...
//實在沒有就會創(chuàng)建
holder = mAdapter.createViewHolder(RecyclerView.this, type);
...
}
}
...
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) { //動畫時不會想去調(diào)用 onBindData
...
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
...
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs); //調(diào)用 bindData 方法
}
final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
final LayoutParams rvLayoutParams;
...調(diào)整LayoutParams
return holder;
}
即大致步驟是:
- 如果執(zhí)行了
RecyclerView動畫的話,嘗試根據(jù)position從mChangedScrap集合中尋找一個ViewHolder - 嘗試
根據(jù)position從scrap集合、hide的view集合、mCacheViews(一級緩存)中尋找一個ViewHolder - 根據(jù)
LayoutManager的position更新到對應(yīng)的Adapter的position。 (這兩個position在大部分情況下都是相等的,不過在子view刪除或移動時可能產(chǎn)生不對應(yīng)的情況) - 根據(jù)
Adapter position,調(diào)用Adapter.getItemViewType()來獲取ViewType - 根據(jù)
stable id(用來表示ViewHolder的唯一,即使位置變化了)從scrap集合和mCacheViews(一級緩存)中尋找一個ViewHolder - 根據(jù)
position和viewType嘗試從用戶自定義的mViewCacheExtension中獲取一個ViewHolder - 根據(jù)
ViewType嘗試從RecyclerViewPool中獲取一個ViewHolder - 調(diào)用
mAdapter.createViewHolder()來創(chuàng)建一個ViewHolder - 如果需要的話調(diào)用
mAdapter.bindViewHolder來設(shè)置ViewHolder。 - 調(diào)整
ViewHolder.itemview的布局參數(shù)為Recycler.LayoutPrams,并返回Holder
雖然步驟很多,邏輯還是很簡單的,即從幾個緩存集合中獲取ViewHolder,如果實在沒有就創(chuàng)建。但比較疑惑的可能就是上述ViewHolder緩存集合中什么時候會保存ViewHolder。接下來分幾個RecyclerView的具體情形,來一點一點弄明白這些ViewHolder緩存集合的問題。
情形一 : 由無到有
即一開始RecyclerView中沒有任何數(shù)據(jù),添加數(shù)據(jù)源后adapter.notifyXXX。狀態(tài)變化如下圖:

很明顯在這種情形下Recycler中是不會存在任何可復(fù)用的ViewHolder。所以所有的ViewHolder都是新創(chuàng)建的。即會調(diào)用Adapter.createViewHolder()和Adapter.bindViewHolder()。那這些創(chuàng)建的ViewHolder會緩存起來嗎?
這時候新創(chuàng)建的這些ViewHolder是不會被緩存起來的。 即在這種情形下: Recycler只會通過Adapter創(chuàng)建ViewHolder,并且不會緩存這些新創(chuàng)建的ViewHolder
情形二 : 在原有數(shù)據(jù)的情況下進(jìn)行整體刷新
就是下面這種狀態(tài):

其實就是相當(dāng)于用戶在feed中做了下拉刷新。實現(xiàn)中的偽代碼如下:
dataSource.clear()
dataSource.addAll(newList)
adapter.notifyDatasetChanged()
在這種情形下猜想Recycler肯定復(fù)用了老的卡片(卡片的類型不變),那么問題是 : 在用戶刷新時舊ViewHolder保存在哪里? 如何調(diào)用舊ViewHolder的Adapter.bindViewHolder()來重新設(shè)置數(shù)據(jù)的?
其實在上一篇文章Recycler刷新機制中,LinearLayoutManager在確定好布局錨點View之后就會把當(dāng)前attach在RecyclerView上的子View全部設(shè)置為scrap狀態(tài):
void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection); // RecyclerView指定錨點,要準(zhǔn)備正式布局了
detachAndScrapAttachedViews(recycler); // 在開始布局時,把所有的View都設(shè)置為 scrap 狀態(tài)
...
}
什么是scrap狀態(tài)呢? 在前面的文章其實已經(jīng)解釋過: ViewHolder被標(biāo)記為FLAG_TMP_DETACHED狀態(tài),并且其itemview的parent被設(shè)置為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); //設(shè)置RecyclerView這個位置的view的parent為null, 并標(biāo)記ViewHolder為FLAG_TMP_DETACHED
recycler.scrapView(view); //添加到mAttachedScrap集合中
...
}
所以在這種情形下LinearLayoutManager在真正擺放子View之前,會把所有舊的子View按順序保存到Recycler的mAttachedScrap集合中
接下來繼續(xù)看,LinearLayoutManager在布局時如何復(fù)用mAttachedScrap集合中的ViewHolder。
前面已經(jīng)說了LinearLayoutManager會當(dāng)前布局子View的位置向Recycler要一個子View,即調(diào)用到tryGetViewHolderForPositionByDeadline(position..)。我們上面已經(jīng)列出了這個方法的邏輯,其實在前面的第二步:
嘗試根據(jù)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的位置和入?yún)osition相等,并且holder是有效的話這個holder就是可以復(fù)用的。所以綜上所述,在情形二下所有的ViewHolder幾乎都是復(fù)用Recycler中mAttachedScrap集合中的。
并且重新布局完畢后Recycler中是不存在可復(fù)用的ViewHolder的。
情形三 : 滾動復(fù)用
這個情形分析是在情形二的基礎(chǔ)上向下滑動時ViewHolder的復(fù)用情況以及Recycler中ViewHolder的保存情況, 如下圖:

在這種情況下滾出屏幕的View會優(yōu)先保存到mCacheViews, 如果mCacheViews中保存滿了,就會保存到RecyclerViewPool中。
在前一篇文章RecyclerView刷新機制中分析過,RecyclerView在滑動時會調(diào)用LinearLayoutManager.fill()方法來根據(jù)滾動的距離來向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); //根據(jù)滾動的距離來回收View
}
}
}
即fill每填充一個子View都會調(diào)用recycleByLayoutState()來回收一個舊的子View,這個方法在層層調(diào)用之后會調(diào)用到Recycler.recycleViewHolderInternal()。這個方法是ViewHolder回收的核心方法,不過邏輯很簡單:
- 檢查
mCacheViews集合中是否還有空位,如果有空位,則直接放到mCacheViews集合 - 如果沒有的話就把
mCacheViews集合中最前面的ViewHolder拿出來放到RecyclerViewPool中,然后再把最新的這個ViewHolder放到mCacheViews集合 - 如果沒有成功緩存到
mCacheViews集合中,就直接放到RecyclerViewPool
mCacheViews集合為什么要這樣緩存? 看一下下面這張圖 :

我是這樣認(rèn)為的,如上圖,往上滑動一段距離,被滑動出去的ViewHolder會被緩存在mCacheViews集合,并且位置是被記錄的。如果用戶此時再下滑的話,可以參考文章開頭的從Recycler中獲取ViewHolder的邏輯:
- 先按照位置從
mCacheViews集合中獲取 - 按照
viewType從mCacheViews集合中獲取
上面對于mCacheViews集合兩步操作,其實第一步就已經(jīng)命中了緩存的ViewHolder。并且這時候都不需要調(diào)用Adapter.bindViewHolder()方法的。即是十分高效的。
所以在普通的滾動復(fù)用的情況下,ViewHolder的復(fù)用主要來自于mCacheViews集合, 舊的ViewHolder會被放到mCacheViews集合, mCacheViews集合擠出來的更老的ViewHolder放到了RecyclerViewPool中
到這里基本的復(fù)用情形都覆蓋了,其他的就涉及到RecyclerView動畫了。這些點在下一篇文章繼續(xù)看。
歡迎關(guān)注我的Android進(jìn)階計劃??锤喔韶?/p>