上一篇文章分析RecyclerView刷新機(jī)制知道
LayoutManager
在布局子View
時(shí)會(huì)向Recycler
索要一個(gè)ViewHolder
咆畏。但從Recycler
中獲取一個(gè)ViewHolder
的前提是Recycler
中要有ViewHolder
。那Recycler
中是如何有ViewHolder
的呢?
本文會(huì)分析兩個(gè)問(wèn)題:
-
RecyclerView
的View
是在什么時(shí)候放入到Recycler
中的秕重。以及在Recycler
中是如何保存的迎变。 -
LayoutManager
在向Recycler
獲取ViewHolder
時(shí)贯城,Recycler
尋找ViewHolder
的邏輯是什么玩敏。
即何時(shí)存、怎么存
和何時(shí)取闯第、怎么取
的問(wèn)題市栗。何時(shí)取
已經(jīng)很明顯了:LayoutManager
在布局子View
時(shí)會(huì)從Recycler
中獲取子View
。 所以本文要理清的是其他3個(gè)問(wèn)題乡括。在文章繼續(xù)之前要知道Recycler
管理的基本單元是ViewHolder
肃廓,LayoutManager
操作的基本單元是View
,即ViewHolder
的itemview
诲泌。本文不會(huì)分析RecyclerView
動(dòng)畫(huà)時(shí)view
的復(fù)用邏輯盲赊。
為了接下來(lái)的內(nèi)容更容易理解,先回顧一下Recycler
的組成結(jié)構(gòu):
-
mChangedScrap
: 用來(lái)保存RecyclerView
做動(dòng)畫(huà)時(shí)敷扫,被detach的ViewHolder
哀蘑。 -
mAttachedScrap
: 用來(lái)保存RecyclerView
做數(shù)據(jù)刷新(notify
),被detach的ViewHolder
-
mCacheViews
:Recycler
的一級(jí)ViewHolder
緩存葵第。 -
RecyclerViewPool
:mCacheViews
集合中裝滿時(shí)绘迁,會(huì)放到這里。
先看一下如何從Recycler
中取一個(gè)ViewHolder
來(lái)復(fù)用卒密。
從Recycler中獲取一個(gè)ViewHolder的邏輯
LayoutManager
會(huì)調(diào)用Recycler.getViewForPosition(pos)
來(lái)獲取一個(gè)指定位置(這個(gè)位置是子View布局所在的位置)的view
缀台。getViewForPosition()
會(huì)調(diào)用tryGetViewHolderForPositionByDeadline(position...)
, 這個(gè)方法是從Recycler
中獲取一個(gè)View
的核心方法。它就是如何從Recycler中獲取一個(gè)ViewHolder
的邏輯哮奇,即怎么取
膛腐。方法太長(zhǎng), 我做了很多裁剪:
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
...
if (mState.isPreLayout()) { //動(dòng)畫(huà)相關(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) {
... //校驗(yàn)這個(gè)holder是否可用
}
}
if (holder == null) {
...
final int type = mAdapter.getItemViewType(offsetPosition); //獲取這個(gè)位置的數(shù)據(jù)的類型哲身。 子Adapter復(fù)寫(xiě)的方法
// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) { //stable id 就是標(biāo)識(shí)一個(gè)viewholder的唯一性辩涝, 即使它做動(dòng)畫(huà)改變了位置
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); //把它包裝成一個(gè)ViewHolder
...
}
}
if (holder == null) { // 從 RecyclerViewPool中獲取
holder = getRecycledViewPool().getRecycledView(type);
...
}
if (holder == null) {
...
//實(shí)在沒(méi)有就會(huì)創(chuàng)建
holder = mAdapter.createViewHolder(RecyclerView.this, type);
...
}
}
...
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) { //動(dòng)畫(huà)時(shí)不會(huì)想去調(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
動(dòng)畫(huà)的話,嘗試根據(jù)position
從mChangedScrap集合
中尋找一個(gè)ViewHolder
- 嘗試
根據(jù)position
從scrap集合
勘天、hide的view集合
怔揩、mCacheViews(一級(jí)緩存)
中尋找一個(gè)ViewHolder
- 根據(jù)
LayoutManager
的position
更新到對(duì)應(yīng)的Adapter
的position
。 (這兩個(gè)position
在大部分情況下都是相等的脯丝,不過(guò)在子view刪除或移動(dòng)
時(shí)可能產(chǎn)生不對(duì)應(yīng)的情況) - 根據(jù)
Adapter position
,調(diào)用Adapter.getItemViewType()
來(lái)獲取ViewType
- 根據(jù)
stable id(用來(lái)表示ViewHolder的唯一商膊,即使位置變化了)
從scrap集合
和mCacheViews(一級(jí)緩存)
中尋找一個(gè)ViewHolder
- 根據(jù)
position和viewType
嘗試從用戶自定義的mViewCacheExtension
中獲取一個(gè)ViewHolder
- 根據(jù)
ViewType
嘗試從RecyclerViewPool
中獲取一個(gè)ViewHolder
- 調(diào)用
mAdapter.createViewHolder()
來(lái)創(chuàng)建一個(gè)ViewHolder
- 如果需要的話調(diào)用
mAdapter.bindViewHolder
來(lái)設(shè)置ViewHolder
。 - 調(diào)整
ViewHolder.itemview
的布局參數(shù)為Recycler.LayoutPrams
宠进,并返回Holder
雖然步驟很多翘狱,邏輯還是很簡(jiǎn)單的,即從幾個(gè)緩存集合中獲取ViewHolder
,如果實(shí)在沒(méi)有就創(chuàng)建砰苍。但比較疑惑的可能就是上述ViewHolder緩存集合
中什么時(shí)候會(huì)保存ViewHolder
。接下來(lái)分幾個(gè)RecyclerView
的具體情形阱高,來(lái)一點(diǎn)一點(diǎn)弄明白這些ViewHolder緩存集合
的問(wèn)題赚导。
情形一 : 由無(wú)到有
即一開(kāi)始RecyclerView
中沒(méi)有任何數(shù)據(jù),添加數(shù)據(jù)源后adapter.notifyXXX
赤惊。狀態(tài)變化如下圖:
很明顯在這種情形下Recycler
中是不會(huì)存在任何可復(fù)用的ViewHolder
吼旧。所以所有的ViewHolder
都是新創(chuàng)建的。即會(huì)調(diào)用Adapter.createViewHolder()和Adapter.bindViewHolder()
未舟。那這些創(chuàng)建的ViewHolder
會(huì)緩存起來(lái)嗎圈暗?
這時(shí)候新創(chuàng)建的這些ViewHolder
是不會(huì)被緩存起來(lái)的。 即在這種情形下: Recycler只會(huì)通過(guò)Adapter創(chuàng)建ViewHolder,并且不會(huì)緩存這些新創(chuàng)建的ViewHolder
情形二 : 在原有數(shù)據(jù)的情況下進(jìn)行整體刷新
就是下面這種狀態(tài):
其實(shí)就是相當(dāng)于用戶在feed中做了下拉刷新裕膀。實(shí)現(xiàn)中的偽代碼如下:
dataSource.clear()
dataSource.addAll(newList)
adapter.notifyDatasetChanged()
在這種情形下猜想Recycler
肯定復(fù)用了老的卡片(卡片的類型不變)员串,那么問(wèn)題是 : 在用戶刷新時(shí)舊ViewHolder
保存在哪里? 如何調(diào)用舊ViewHolder
的Adapter.bindViewHolder()
來(lái)重新設(shè)置數(shù)據(jù)的昼扛?
其實(shí)在上一篇文章Recycler刷新機(jī)制
中寸齐,LinearLayoutManager
在確定好布局錨點(diǎn)View
之后就會(huì)把當(dāng)前attach
在RecyclerView
上的子View
全部設(shè)置為scrap狀態(tài)
:
void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection); // RecyclerView指定錨點(diǎn),要準(zhǔn)備正式布局了
detachAndScrapAttachedViews(recycler); // 在開(kāi)始布局時(shí)抄谐,把所有的View都設(shè)置為 scrap 狀態(tài)
...
}
什么是scrap狀態(tài)呢渺鹦? 在前面的文章其實(shí)已經(jīng)解釋過(guò): 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這個(gè)位置的view的parent為null毅厚, 并標(biāo)記ViewHolder為FLAG_TMP_DETACHED
recycler.scrapView(view); //添加到mAttachedScrap集合中
...
}
所以在這種情形下LinearLayoutManager
在真正擺放子View
之前,會(huì)把所有舊的子View
按順序保存到Recycler
的mAttachedScrap集合
中
接下來(lái)繼續(xù)看,LinearLayoutManager
在布局時(shí)如何復(fù)用mAttachedScrap集合
中的ViewHolder
浦箱。
前面已經(jīng)說(shuō)了LinearLayoutManager
會(huì)當(dāng)前布局子View的位置向Recycler
要一個(gè)子View吸耿,即調(diào)用到tryGetViewHolderForPositionByDeadline(position..)
祠锣。我們上面已經(jīng)列出了這個(gè)方法的邏輯,其實(shí)在前面的第二步:
嘗試根據(jù)position
從scrap集合
珍语、hide的view集合
锤岸、mCacheViews(一級(jí)緩存)
中尋找一個(gè)ViewHolder
即從mAttachedScrap
中就可以獲得一個(gè)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
是有效的話這個(gè)holder
就是可以復(fù)用的板乙。所以綜上所述是偷,在情形二下所有的ViewHolder
幾乎都是復(fù)用Recycler中mAttachedScrap集合
中的。
并且重新布局完畢后Recycler
中是不存在可復(fù)用的ViewHolder
的募逞。
情形三 : 滾動(dòng)復(fù)用
這個(gè)情形分析是在情形二
的基礎(chǔ)上向下滑動(dòng)時(shí)ViewHolder
的復(fù)用情況以及Recycler
中ViewHolder
的保存情況, 如下圖:
在這種情況下滾出屏幕的View會(huì)優(yōu)先保存到mCacheViews
, 如果mCacheViews
中保存滿了蛋铆,就會(huì)保存到RecyclerViewPool
中。
在前一篇文章RecyclerView刷新機(jī)制
中分析過(guò)放接,RecyclerView
在滑動(dòng)時(shí)會(huì)調(diào)用LinearLayoutManager.fill()
方法來(lái)根據(jù)滾動(dòng)的距離來(lái)向RecyclerView
填充子View刺啦,其實(shí)在個(gè)方法在填充完子View之后就會(huì)把滾動(dòng)出屏幕的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); //填充一個(gè)子View
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState); //根據(jù)滾動(dòng)的距離來(lái)回收View
}
}
}
即fill
每填充一個(gè)子View
都會(huì)調(diào)用recycleByLayoutState()
來(lái)回收一個(gè)舊的子View
,這個(gè)方法在層層調(diào)用之后會(huì)調(diào)用到Recycler.recycleViewHolderInternal()
。這個(gè)方法是ViewHolder
回收的核心方法纠脾,不過(guò)邏輯很簡(jiǎn)單:
- 檢查
mCacheViews集合
中是否還有空位玛瘸,如果有空位,則直接放到mCacheViews集合
- 如果沒(méi)有的話就把
mCacheViews集合
中最前面的ViewHolder
拿出來(lái)放到RecyclerViewPool
中苟蹈,然后再把最新的這個(gè)ViewHolder放到mCacheViews集合
- 如果沒(méi)有成功緩存到
mCacheViews集合
中糊渊,就直接放到RecyclerViewPool
mCacheViews集合
為什么要這樣緩存? 看一下下面這張圖 :
我是這樣認(rèn)為的,如上圖慧脱,往上滑動(dòng)一段距離渺绒,被滑動(dòng)出去的ViewHolder
會(huì)被緩存在mCacheViews集合
,并且位置是被記錄的。如果用戶此時(shí)再下滑的話菱鸥,可以參考文章開(kāi)頭的從Recycler
中獲取ViewHolder的邏輯:
- 先按照位置從
mCacheViews集合
中獲取 - 按照
viewType
從mCacheViews集合
中獲取
上面對(duì)于mCacheViews集合
兩步操作宗兼,其實(shí)第一步就已經(jīng)命中了緩存的ViewHolder
。并且這時(shí)候都不需要調(diào)用Adapter.bindViewHolder()
方法的氮采。即是十分高效的殷绍。
所以在普通的滾動(dòng)復(fù)用的情況下,ViewHolder
的復(fù)用主要來(lái)自于mCacheViews集合
, 舊的ViewHolder
會(huì)被放到mCacheViews集合
, mCacheViews集合
擠出來(lái)的更老的ViewHolder
放到了RecyclerViewPool
中
到這里基本的復(fù)用情形都覆蓋了扳抽,其他的就涉及到RecyclerView動(dòng)畫(huà)
了篡帕。這些點(diǎn)在下一篇文章繼續(xù)看。
歡迎關(guān)注我的Android進(jìn)階計(jì)劃贸呢×眨看更多干貨