開始
最近在研究 RecyclerView 的回收復(fù)用機(jī)制,順便記錄一下。我們知道,RecyclerView 在 layout 子 View 時(shí),都通過回收復(fù)用機(jī)制來管理。網(wǎng)上關(guān)于回收復(fù)用機(jī)制的分析講解的文章也有一大堆了,分析得也都很詳細(xì),什么四級(jí)緩存啊,先去 mChangedScrap 取再去哪里取啊之類的;但其實(shí),我想說的是,RecyclerView 的回收復(fù)用機(jī)制確實(shí)很完善,覆蓋到各種場景中,但并不是每種場景的回收復(fù)用時(shí)都會(huì)將機(jī)制的所有流程走一遍的。舉個(gè)例子說,在 setLayoutManager、setAdapter、notifyDataSetChanged 或者滑動(dòng)時(shí)等等這些場景都會(huì)觸發(fā)回收復(fù)用機(jī)制的工作。但是如果只是 RecyclerView 滑動(dòng)的場景觸發(fā)的回收復(fù)用機(jī)制工作時(shí),其實(shí)并不需要四級(jí)緩存都參與的。
問題
假設(shè)有一個(gè)20個(gè)item的RecyclerView,每五個(gè)占滿一個(gè)屏幕,在從頭滑到尾的過程中,onCreatViewHolder會(huì)調(diào)用多少次?
正題
RecyclerView 的回收復(fù)用機(jī)制的內(nèi)部實(shí)現(xiàn)都是由 Recycler 內(nèi)部類實(shí)現(xiàn),下面就都以這樣一種頁面的滑動(dòng)場景來講解 RecyclerView 的回收復(fù)用機(jī)制。
相應(yīng)的版本:
RecyclerView: recyclerview-v7-25.1.0.jar
LayoutManager: GridLayoutManager extends LinearLayoutManager (recyclerview-v7-25.1.0.jar)
這個(gè)頁面每行可顯示5個(gè)卡位,每個(gè)卡位的 item 布局 type 一致。開始分析回收復(fù)用機(jī)制之前,先提幾個(gè)問題:
Q1:如果向下滑動(dòng),新一行的5個(gè)卡位的顯示會(huì)去復(fù)用緩存的 ViewHolder,第一行的5個(gè)卡位會(huì)移出屏幕被回收,那么在這個(gè)過程中,是先進(jìn)行復(fù)用再回收?還是先回收再復(fù)用?還是邊回收邊復(fù)用?也就是說,新一行的5個(gè)卡位復(fù)用的 ViewHolder 有可能是第一行被回收的5個(gè)卡位嗎?
回答問題之前,先看幾張圖片:
先向下再向上滑動(dòng)
黑框表示屏幕,RecyclerView 先向下滑動(dòng),第三行卡位顯示出來,再向上滑動(dòng),第三行移出屏幕,第一行顯示出來。我們分別在 Adapter 的 onCreateViewHolder() 和 onBindViewHolder() 里打日志,下面是這個(gè)過程的日志:
紅框1是 RecyclerView 向下滑動(dòng)操作的日志,第三行5個(gè)卡位的顯示都是重新創(chuàng)建的 ViewHolder ;紅框2是再次向上滑動(dòng)時(shí)的日志,第一行5個(gè)卡位的重新顯示用的 ViewHolder 都是復(fù)用的,因?yàn)闆]有 create viewHolder 的日志,然后只有后面3個(gè)卡位重新綁定數(shù)據(jù),調(diào)用了onBindViewHolder();那么問題來了:
Q2: 在這個(gè)過程中,為什么當(dāng) RecyclerView 再次向上滑動(dòng)重新顯示第一行的5個(gè)卡位時(shí),只有后面3個(gè)卡位觸發(fā)了 onBindViewHolder() 方法,重新綁定數(shù)據(jù)呢?明明5個(gè)卡位都是復(fù)用的。
在上面的操作基礎(chǔ)上,我們繼續(xù)往下操作:先向下再向下
在第二個(gè)問題操作的基礎(chǔ)上,目前已經(jīng)創(chuàng)建了15個(gè) ViewHolder,此時(shí)顯示的是第1、2行的卡位,那么繼續(xù)向下滑動(dòng)兩次,這個(gè)過程的日志如下:
紅框1是第二個(gè)問題操作的日志,在這里截出來只是為了顯示接下去的日志是在上面的基礎(chǔ)上繼續(xù)操作的;
紅框2就是第一次向下滑時(shí)的日志,對(duì)比問題2的日志,這次第三行的5個(gè)卡位用的 ViewHolder 也都是復(fù)用的,而且也只有后面3個(gè)卡位觸發(fā)了 onBindViewHolder() 重新綁定數(shù)據(jù);
紅框3是第二次向下滑動(dòng)時(shí)的日志,這次第四行的5個(gè)卡位,前3個(gè)的卡位用的 ViewHolder 是復(fù)用的,后面2個(gè)卡位的 ViewHolder 則是重新創(chuàng)建的,而且5個(gè)卡位都調(diào)用了 onBindViewHolder() 重新綁定數(shù)據(jù);
Q3:接下去不管是向上滑動(dòng)還是向下滑動(dòng),滑動(dòng)幾次,都不會(huì)再有 onCreateViewHolder() 的日志了,也就是說 RecyclerView 總共創(chuàng)建了17個(gè) ViewHolder,但有時(shí)一行的5個(gè)卡位只有3個(gè)卡位需要重新綁定數(shù)據(jù),有時(shí)卻又5個(gè)卡位都需要重新綁定數(shù)據(jù),這是為什么呢?
如果明白 RecyclerView 的回收復(fù)用機(jī)制,那么這三個(gè)問題也就都知道原因了;反過來,如果知道這三個(gè)問題的原因,那么理解 RecyclerView 的回收復(fù)用機(jī)制也就更簡單了;所以,帶著問題,在特定的場景下去分析源碼的話,應(yīng)該會(huì)比較容易。
源碼分析
其實(shí),根據(jù)問題2的日志,我們就可以回答問題1了。在目前顯示1、2行,ViewHolder 的個(gè)數(shù)為10個(gè)的基礎(chǔ)上,第三行的5個(gè)新卡位要顯示出來都需要重新創(chuàng)建 ViewHolder,也就是說,在這個(gè)向下滑動(dòng)的過程,是5個(gè)新卡位的復(fù)用機(jī)制先進(jìn)行工作,然后第1行的5個(gè)被移出屏幕的卡位再進(jìn)行回收機(jī)制工作。那么,就先來看看復(fù)用機(jī)制的源碼。
復(fù)用機(jī)制
getViewForPosition()
//入口在這里
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
//復(fù)用機(jī)制工作原理都在這里
//...
}
這個(gè)方法是復(fù)用機(jī)制的入口,也就是 Recycler 開放給外部使用復(fù)用機(jī)制的api,外部調(diào)用這個(gè)方法就可以返回想要的 View,而至于這個(gè) View 是復(fù)用而來的,還是重新創(chuàng)建得來的,就都由 Recycler 內(nèi)部實(shí)現(xiàn),對(duì)外隱藏。
tryGetViewHolderForPositionByDeadline()
所以,Recycler 的復(fù)用機(jī)制內(nèi)部實(shí)現(xiàn)就在這個(gè)方法里。分析邏輯之前,先看一下 Recycler 的幾個(gè)結(jié)構(gòu)體,用來緩存 ViewHolder 的。
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
//這個(gè)是本篇的重點(diǎn)
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;
//這個(gè)也是本篇的重點(diǎn)
RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
static final int DEFAULT_CACHE_SIZE = 2;
}
mAttachedScrap:用于緩存顯示在屏幕上的 item 的 ViewHolder,場景好像是 RecyclerView 在 onLayout 時(shí)會(huì)先把 children 都移除掉,再重新添加進(jìn)去,所以這個(gè) List 應(yīng)該是用在布局過程中臨時(shí)存放 children 的,反正在 RecyclerView 滑動(dòng)過程中不會(huì)在這里面來找復(fù)用的 ViewHolder 就是了
mChangedScrap: 這個(gè)沒理解是干嘛用的,看名字應(yīng)該跟 ViewHolder 的數(shù)據(jù)發(fā)生變化時(shí)有關(guān)吧,在 RecyclerView 滑動(dòng)的過程中,也沒有發(fā)現(xiàn)到這里找復(fù)用的 ViewHolder,所以這個(gè)可以先暫時(shí)放一邊。
mCachedViews:這個(gè)就重要得多了,滑動(dòng)過程中的回收和復(fù)用都是先處理的這個(gè) List,這個(gè)集合里存的 ViewHolder 的原本數(shù)據(jù)信息都在,所以可以直接添加到 RecyclerView 中顯示,不需要再次重新 onBindViewHolder()。
mUnmodifiableAttachedScrap: 不清楚干嘛用的,暫時(shí)跳過。
mRecyclerPool:這個(gè)也很重要,但存在這里的 ViewHolder 的數(shù)據(jù)信息會(huì)被重置掉,相當(dāng)于 ViewHolder 是一個(gè)重創(chuàng)新建的一樣,所以需要重新調(diào)用 onBindViewHolder 來綁定數(shù)據(jù)。
mViewCacheExtension:這個(gè)是留給我們自己擴(kuò)展的,好像也沒怎么用,就暫時(shí)不分析了。
那么接下去就看看復(fù)用的邏輯:
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
if (position < 0 || position >= mState.getItemCount()) {
throw new IndexOutOfBoundsException("Invalid item position " + position
+ "(" + position + "). Item count:" + mState.getItemCount());
}
//...省略代碼
}
第一步很簡單,position 如果在 item 的范圍之外的話,那就拋異常吧。繼續(xù)往下看:
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
//...省略看過的代碼
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 0) If there is a changed scrap, try to find from there
//上面是Google留的注釋,大意是...(emmm,這里我也沒理解)
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
}
如果是在 isPreLayout() 時(shí),那么就去 mChangedScrap 中找。那么這個(gè) isPreLayout 表示的是什么?共5有個(gè)賦值的地方。
//只顯示相關(guān)代碼,無關(guān)代碼省略
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout.mAutoMeasure) {
//...
} else {
// custom onMeasure
if (mAdapterUpdateDuringMeasure) {
if (mState.mRunPredictiveAnimations) {
mState.mInPreLayout = true;
} else {
// consume remaining updates to provide a consistent state with the layout pass.
mAdapterHelper.consumeUpdatesInOnePass();
mState.mInPreLayout = false;
}
}
}
//...
mState.mInPreLayout = false; // clear
}
private void dispatchLayoutStep1() {
//...
mState.mInPreLayout = mState.mRunPredictiveAnimations;
//...
}
private void dispatchLayoutStep2() {
//...
mState.mInPreLayout = mState.mRunPredictiveAnimations;
mLayout.onLayoutChildren(mRecycler, mState);
//...
}
emmm,看樣子,在 LayoutManager 的 onLayoutChildren 前就會(huì)置為 false,不過我還是不懂這個(gè)過程是干嘛的,滑動(dòng)過程中好像 mState.mInPreLayou = false,所以并不會(huì)來這里,先暫時(shí)跳過,繼續(xù)往下。
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
//...省略看過的代碼
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
//這里是第一次找可復(fù)用的ViewHolder了,得跟進(jìn)去看看
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
//...
}
}
跟進(jìn)這個(gè)方法看看:
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position,boolean dryRun) {
final int scrapCount = mAttachedScrap.size();
// Try first for an exact, non-invalid match from scrap.
for (int i = 0; i < scrapCount; i++) {
//首先去mAttachedScrap中遍歷尋找,匹配條件也很多
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 中尋找 position 一致的 viewHolder,需要匹配一些條件,大致是這個(gè) viewHolder 沒有被移除,是有效的之類的條件,滿足就返回這個(gè) viewHolder。所以,這里的關(guān)鍵就是要理解這個(gè) mAttachedScrap 到底是什么,存的是哪些 ViewHolder。一次遙控器按鍵的操作,不管有沒有發(fā)生滑動(dòng),都會(huì)導(dǎo)致 RecyclerView 的重新 onLayout,那要 layout 的話,RecyclerView 會(huì)先把所有 children 先 remove 掉,然后再重新 add 上去,完成一次 layout 的過程。那么這暫時(shí)性的 remove 掉的 viewHolder 要存放在哪呢,就是放在這個(gè) mAttachedScrap 中了,這就是我的理解了。所以,感覺這個(gè) mAttachedScrap 中存放的 viewHolder 跟回收和復(fù)用關(guān)系不大。
網(wǎng)上一些分析的文章有說,RecyclerView 在復(fù)用時(shí)會(huì)按順序去 mChangedScrap, mAttachedScrap 等等緩存里找,沒有找到再往下去找,從代碼上來看是這樣沒錯(cuò),但我覺得這樣表述有問題。因?yàn)榫臀覀冞@篇文章基于 RecyclerView 的滑動(dòng)場景來說,新卡位的復(fù)用以及舊卡位的回收機(jī)制,其實(shí)都不會(huì)涉及到 mChangedScrap 和 mAttachedScrap,所以我覺得還是基于某種場景來分析相對(duì)應(yīng)的回收復(fù)用機(jī)制會(huì)比較好。就像 mChangedScrap 我雖然沒理解是干嘛用的,但我猜測應(yīng)該是在當(dāng)數(shù)據(jù)發(fā)生變化時(shí)才會(huì)涉及到的復(fù)用場景,所以當(dāng)我分析基于滑動(dòng)場景時(shí)的復(fù)用時(shí),即使我對(duì)這塊不理解,影響也不會(huì)很大。繼續(xù)向下看:
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position,boolean dryRun) {
//...省略看過的代碼
if (!dryRun) {//dryRun一直為false
//這段代碼可看可不看
View view = mChildHelper.findHiddenNonRemovedView(position);
if (view != null) {
// This View is good to be used. We just need to unhide, detach and move to the
// scrap list.
final ViewHolder vh = getChildViewHolderInt(view);
mChildHelper.unhide(view);
int layoutIndex = mChildHelper.indexOfChild(view);
if (layoutIndex == RecyclerView.NO_POSITION) {
throw new IllegalStateException("layout index should not be -1 after "
+ "unhiding a view:" + vh);
}
mChildHelper.detachViewFromParent(layoutIndex);
scrapView(view);
vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
| ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
return vh;
}
}
}
emmm,這段也還是沒看懂,但估計(jì)應(yīng)該需要一些特定的場景下所使用的復(fù)用策略吧,看名字,應(yīng)該跟 hidden 有關(guān)?不懂,跳過這段,應(yīng)該也沒事,滑動(dòng)過程中的回收復(fù)用跟這個(gè)應(yīng)該也關(guān)系不大。
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position,boolean dryRun) {
//...省略看過的代碼
// Search in our first-level recycled view cache.
//下面就是重點(diǎn)了,去mCachedViews里遍歷
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
// invalid view holders may be in cache if adapter has stable ids as they can be
// retrieved via getScrapOrCachedViewForId
// 上面的大意是即使是失效的holser也有可能可以拿來復(fù)用,但需要我們重寫adapter的setHasStadleId并且提供一個(gè)id時(shí),在getScrapOrCachedViewForId()里就可以再去mCachedViews里找一遍。
if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
if (!dryRun) { //dryRun一直為false
mCachedViews.remove(i);//所以,如果position匹配,那么就將這個(gè)ViewHolder移除mCachedViews
}
if (DEBUG) {
Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
+ ") found match in cache: " + holder);
}
return holder;
}
return null;
}
這里就要畫重點(diǎn)啦,記筆記記筆記,滑動(dòng)場景中的復(fù)用會(huì)用到這里的機(jī)制。mCachedViews 的大小默認(rèn)為2。遍歷 mCachedViews,找到 position 一致的 ViewHolder,之前說過,mCachedViews 里存放的 ViewHolder 的數(shù)據(jù)信息都保存著,所以 mCachedViews 可以理解成,只有原來的卡位可以重新復(fù)用這個(gè) ViewHolder,新位置的卡位無法從 mCachedViews 里拿 ViewHolder出來用。 找到 viewholder 后:
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
//...省略看過的代碼
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
//這里是第一次找可復(fù)用的ViewHolder了,得跟進(jìn)去看看
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
//之前分析跟進(jìn)了上面那個(gè)方法,找到ViewHolder后
if (holder != null) {
//需要再次驗(yàn)證一下這個(gè)ViewHodler是否可以拿來復(fù)用
if (!validateViewHolderForOffsetPosition(holder)) {
// recycle holder (and unscrap if relevant) since it can't be used
if (!dryRun) {
// we would like to recycle this but need to make sure it is not used by
// animation logic etc.
//如果不能復(fù)用,就把它要么仍到mAttachedScrap或者扔到ViewPool里
holder.addFlags(ViewHolder.FLAG_INVALID);
if (holder.isScrap()) {
removeDetachedView(holder.itemView, false);
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
}
holder = null;
} else {
fromScrapOrHiddenOrCache = true;
}
}
}
}
就算 position 匹配找到了 ViewHolder,還需要判斷一下這個(gè) ViewHolder 是否已經(jīng)被 remove 掉,type 類型一致不一致,如下:
boolean validateViewHolderForOffsetPosition(ViewHolder holder) {
// if it is a removed holder, nothing to verify since we cannot ask adapter anymore
// if it is not removed, verify the type and id.
if (holder.isRemoved()) {
if (DEBUG && !mState.isPreLayout()) {
throw new IllegalStateException("should not receive a removed view unless it"
+ " is pre layout");
}
return mState.isPreLayout();
}
if (holder.mPosition < 0 || holder.mPosition >= mAdapter.getItemCount()) {
throw new IndexOutOfBoundsException("Inconsistency detected. Invalid view holder "
+ "adapter position" + holder);
}
//如果type類型不一樣,那就不能復(fù)用
if (!mState.isPreLayout()) {
// don't check type if it is pre-layout.
final int type = mAdapter.getItemViewType(holder.mPosition);
if (type != holder.getItemViewType()) {
return false;
}
}
if (mAdapter.hasStableIds()) {
return holder.getItemId() == mAdapter.getItemId(holder.mPosition);
}
return true;
}
以上是在 mCachedViews 中尋找,沒有找到的話,就繼續(xù)再找一遍,剛才是通過 position 來找,那這次就換成id,然后重復(fù)上面的步驟再找一遍,如下:
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
//...省略看過的代碼
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
throw new IndexOutOfBoundsException("http://省略...");
}
final int type = mAdapter.getItemViewType(offsetPosition);
// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) {//如果有設(shè)置stableIs,就再從Scrap和cached里根據(jù)id找一次
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
//省略之后步驟,后續(xù)分析...
}
}
getScrapOrCachedViewForId() 做的事跟 getScrapOrHiddenOrCacheHolderForPosition() 其實(shí)差不多,只不過一個(gè)是通過 position 來找 ViewHolder,一個(gè)是通過 id 來找。而這個(gè) id 并不是我們在 xml 中設(shè)置的 android:id, 而是 Adapter 持有的一個(gè)屬性,默認(rèn)是不會(huì)使用這個(gè)屬性的,所以這里其實(shí)是不會(huì)執(zhí)行的,除非我們重寫了 Adapter 的 setHasStableIds(),既然不是常用的場景,那就先略過吧,那就繼續(xù)往下。
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
//...省略看過的代碼
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
//省略無關(guān)代碼...
final int type = mAdapter.getItemViewType(offsetPosition);
//省略上述步驟跟getScrapOrCachedViewForId()相關(guān)的代碼...
//這里開始就又去另一個(gè)地方找了,ViewCacheExtension
if (holder == null && mViewCacheExtension != null) {
// We are NOT sending the offsetPosition because LayoutManager does not
// know it.
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
if (holder == null) {
throw new IllegalArgumentException("getViewForPositionAndType returned" + " a view which does not have a ViewHolder");
} else if (holder.shouldIgnore()) {
throw new IllegalArgumentException("getViewForPositionAndType returned" + " a view that is ignored. You must call stopIgnoring before" + " returning this view.");
}
}
}
//省略之后步驟,后續(xù)分析...
}
}
這個(gè)就是常說擴(kuò)展類了,RecyclerView 提供給我們自定義實(shí)現(xiàn)的擴(kuò)展類,我們可以重寫 getViewForPositionAndType() 方法來實(shí)現(xiàn)自己的復(fù)用策略。不過,也沒用過,那這部分也當(dāng)作不會(huì)執(zhí)行,略過。繼續(xù)往下:
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
//...省略看過的代碼
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
//省略無關(guān)代碼...
final int type = mAdapter.getItemViewType(offsetPosition);
//省略看過的的代碼...
//這里開始就又去另一個(gè)地方找了,RecycledViewPool
if (holder == null) { // fallback to pool
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline("+ position + ") fetching from shared pool");
}
//跟進(jìn)這個(gè)方法看看
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
//如果在ViewPool里找到可復(fù)用的ViewHolder,那就重置ViewHolder的數(shù)據(jù),這樣ViewHolder就可以當(dāng)作全新的來使用了
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
//省略之后步驟,后續(xù)分析...
}
}
這里也是重點(diǎn)了,記筆記記筆記。這里是去 RecyclerViewPool 里取 ViewHolder,ViewPool 會(huì)根據(jù)不同的 item type 創(chuàng)建不同的 List,每個(gè) List 默認(rèn)大小為5個(gè)??匆幌氯?ViewPool 里是怎么找的:
public ViewHolder getRecycledView(int viewType) {
//根據(jù)type,只要不為空,就將最后一個(gè)ViewHolder移出來復(fù)用
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
return scrapHeap.remove(scrapHeap.size() - 1);
}
return null;
}
之前說過,ViewPool 會(huì)根據(jù)不同的 viewType 創(chuàng)建不同的集合來存放 ViewHolder,那么復(fù)用的時(shí)候,只要 ViewPool 里相同的 type 有 ViewHolder 緩存的話,就將最后一個(gè)拿出來復(fù)用,不用像 mCachedViews 需要各種匹配條件,只要有就可以復(fù)用。拿到 ViewHolder 之后,還會(huì)再次調(diào)用 resetInternal() 來重置 ViewHolder,這樣 ViewHolder 就可以當(dāng)作一個(gè)全新的 ViewHolder 來使用了,這也就是為什么從這里拿的 ViewHolder 都需要重新 onBindViewHolder() 了。那如果在 ViewPool 里還是沒有找到呢,繼續(xù)往下看:
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
//...省略看過的代碼
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
//省略無關(guān)代碼...
final int type = mAdapter.getItemViewType(offsetPosition);
//省略看過的的代碼...
//都沒找到的話,就調(diào)用Adapter.onCreateAdapter()來新建一個(gè)ViewHolder了
if (holder == null) {
//省略無關(guān)代碼...
holder = mAdapter.createViewHolder(RecyclerView.this, type);//新建一個(gè)ViewHolder
//省略無關(guān)代碼...
}
}
//省略之后步驟,后續(xù)分析
}
如果 ViewPool 中都沒有找到 ViewHolder 來使用的話,那就調(diào)用 Adapter 的 onCreateViewHolder 來創(chuàng)建一個(gè)新的 ViewHolder 使用。上面一共有很多步驟來找 ViewHolder,不管在哪個(gè)步驟,只要找到 ViewHolder 的話,那下面那些步驟就不用管了,然后都要繼續(xù)往下判斷是否需要重新綁定數(shù)據(jù),還有檢查布局參數(shù)是否合法。如下:
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
//...省略上述分析的找ViewHolder的代碼...
//代碼執(zhí)行到這里,ViewHolder肯定不為Null了,因?yàn)榫退阍诟鱾€(gè)緩存里沒找到,最后一步也會(huì)重新創(chuàng)建一個(gè)
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
// do not update unless we absolutely have to.
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
if (DEBUG && holder.isRemoved()) {
throw new IllegalStateException("Removed holder should be bound and it should" + " come here only in pre-layout. Holder: " + holder);
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
//調(diào)用Adapter.onBindViewHolder()來重新綁定數(shù)據(jù)
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
//下面是驗(yàn)證itemView的布局參數(shù)是否可用,并設(shè)置可用的布局參數(shù)
final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
final LayoutParams rvLayoutParams;
if (lp == null) {
rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
holder.itemView.setLayoutParams(rvLayoutParams);
} else if (!checkLayoutParams(lp)) {
rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
holder.itemView.setLayoutParams(rvLayoutParams);
} else {
rvLayoutParams = (LayoutParams) lp;
}
rvLayoutParams.mViewHolder = holder;
rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
return holder;
//結(jié)束
}
到這里,tryGetViewHolderForPositionByDeadline() 這個(gè)方法就結(jié)束了。這大概就是 RecyclerView 的復(fù)用機(jī)制,中間我們跳過很多地方,因?yàn)?RecyclerView 有各種場景可以刷新他的 view,比如重新 setLayoutManager(),重新 setAdapter(),或者 notifyDataSetChanged(),或者滑動(dòng)等等之類的場景,只要重新layout,就會(huì)去回收和復(fù)用 ViewHolder,所以這個(gè)復(fù)用機(jī)制需要考慮到各種各樣的場景。把代碼一行行的啃透有點(diǎn)吃力,所以我就只借助 RecyclerView 的滑動(dòng)的這種場景來分析它涉及到的回收和復(fù)用機(jī)制。下面就分析一下回收機(jī)制 。
回收機(jī)制
回收機(jī)制的入口就有很多了,因?yàn)?Recycler 有各種結(jié)構(gòu)體,比如mAttachedScrap,mCachedViews 等等,不同結(jié)構(gòu)體回收的時(shí)機(jī)都不一樣,入口也就多了。所以,還是基于 RecyclerView 的滑動(dòng)場景下,移出屏幕的卡位回收時(shí)的入口是:
//回收入口之一
public void recycleView(View view) {
// This public recycle method tries to make view recycle-able since layout manager
// intended to recycle this view (e.g. even if it is in scrap or change cache)
ViewHolder holder = getChildViewHolderInt(view);
if (holder.isTmpDetached()) {
removeDetachedView(view, false);
}
if (holder.isScrap()) {
holder.unScrap();
} else if (holder.wasReturnedFromScrap()){
holder.clearReturnedFromScrapFlag();
}
//回收的內(nèi)部實(shí)現(xiàn),跟進(jìn)看看
recycleViewHolderInternal(holder);
}
本篇分析的滑動(dòng)場景,在 RecyclerView 滑動(dòng)時(shí),會(huì)交由 LinearLayoutManager 的 scrollVerticallyBy() 去處理,然后 LayoutManager 會(huì)接著調(diào)用 fill() 方法去處理需要復(fù)用和回收的卡位,最終會(huì)調(diào)用上述 recyclerView() 這個(gè)方法開始進(jìn)行回收工作。
void recycleViewHolderInternal(ViewHolder holder) {
//省略代碼...
if (forceRecycle || holder.isRecyclable()) {
//mViewCacheMax大小默認(rèn)為2
if (mViewCacheMax > 0 /*省略其他條件*/) {
// Retire oldest cached view
int cachedViewSize = mCachedViews.size();
//回收時(shí),先將ViewHolder緩存在mCachedViews里,如果滿了,調(diào)用recycleCachedViewAt(0)移除一個(gè),好空出位置來
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize--;
}
//省略無關(guān)代碼...
//將最近剛剛回收的ViewHolder放在mCachedViews里
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
//如果設(shè)置不用mCachedViewd緩存的話,那回收時(shí)就扔進(jìn)ViewPool里等待復(fù)用
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
//省略無關(guān)代碼...
}
跟進(jìn) recycleCachedViewAt(0) 方法看看:
void recycleCachedViewAt(int cachedViewIndex) {
if (DEBUG) {
Log.d(TAG, "Recycling cached view at index " + cachedViewIndex);
}
ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
if (DEBUG) {
Log.d(TAG, "CachedViewHolder to be recycled: " + viewHolder);
}
//將mCachedViews里緩存的ViewHolder取出來,扔進(jìn)ViewPool里緩存
addViewHolderToRecycledViewPool(viewHolder, true);
mCachedViews.remove(cachedViewIndex);
}
繼續(xù)跟進(jìn) addViewHolderToRecycledViewPool() 里看看,這個(gè)方法在上上代碼塊里也出現(xiàn)
void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
clearNestedRecyclerViewIfNotNested(holder);
ViewCompat.setAccessibilityDelegate(holder.itemView, null);
if (dispatchRecycled) {
//這個(gè)方法會(huì)去回調(diào)Adapter里的onViewRecycle(),所以Adapter接收到該回調(diào)時(shí)是ViewHolder被扔進(jìn)ViewPool里才會(huì)觸發(fā)的
//如果ViewHolder只是被mCachedViews緩存了,那Adapter的onViewRecycle()是不會(huì)回調(diào)的,所以不是所有被移出屏幕的item都會(huì)觸發(fā)onViewRecycle()方法的
dispatchViewRecycled(holder);
}
holder.mOwnerRecyclerView = null
//在扔進(jìn)ViewPool前回調(diào)一些方法,并對(duì)ViewHolder的一些標(biāo)志置位,然后繼續(xù)跟進(jìn)看看
getRecycledViewPool().putRecycledView(holder);
}
在 ViewHolder 扔進(jìn) ViewPool 里之前,會(huì)先去回調(diào) Adapter 里的 onViewRecycle(),所以 Adapter 接收到該回調(diào)時(shí)是 ViewHolder 被扔進(jìn) ViewPool 里才會(huì)觸發(fā)的。如果 ViewHolder 只是被 mCachedViews 緩存了,那 Adapter 的 onViewRecycle() 是不會(huì)回調(diào)的,所以不是所有被移出屏幕的 item 都會(huì)觸發(fā) onViewRecycle() 方法的,這點(diǎn)需要注意一下。繼續(xù)跟進(jìn)看看 :
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap;
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
//如果ViewPool滿了,就不緩存了,默認(rèn)大小為5
return;
}
if (DEBUG && scrapHeap.contains(scrap)) {
throw new IllegalArgumentException("this scrap item already exists");
}
//緩存前先將ViewHolder的信息重置,這樣ViewHolder下次被拿出來復(fù)用時(shí)就可以當(dāng)作全新的ViewHolder來使用了
scrap.resetInternal();
scrapHeap.add(scrap);
}
所以,ViewHolder 在扔進(jìn) ViewPool 前會(huì)先 reset,這里的重置指的是 ViewHolder 保存的一些信息,比如 position,跟它綁定的 RecycleView 啊之類的,并不會(huì)清空 itemView,所以復(fù)用時(shí)才會(huì)經(jīng)常出現(xiàn) itemView 顯示之前卡位的圖片信息之類的情況,這點(diǎn)需要區(qū)分一下。
回收的邏輯比較簡單,由 LayoutManager 來遍歷移出屏幕的卡位,然后對(duì)每個(gè)卡位進(jìn)行回收操作,回收時(shí),都是把 ViewHolder 放在 mCachedViews 里面,如果 mCachedViews 滿了,那就在 mCachedViews 里拿一個(gè) ViewHolder 扔到 ViewPool 緩存里,然后 mCachedViews 就可以空出位置來放新回收的 ViewHolder 了。
總結(jié)
RecyclerView 滑動(dòng)場景下的回收復(fù)用涉及到的結(jié)構(gòu)體兩個(gè):mCachedViews 和 RecyclerViewPool。
mCachedViews 優(yōu)先級(jí)高于 RecyclerViewPool,回收時(shí),最新的 ViewHolder 都是往 mCachedViews 里放,如果它滿了,那就移出一個(gè)扔到 ViewPool 里好空出位置來緩存最新的 ViewHolder。
復(fù)用時(shí),也是先到 mCachedViews 里找 ViewHolder,但需要各種匹配條件,概括一下就是只有原來位置的卡位可以復(fù)用存在 mCachedViews 里的 ViewHolder,如果 mCachedViews 里沒有,那么才去 ViewPool 里找。
在 ViewPool 里的 ViewHolder 都是跟全新的 ViewHolder 一樣,只要 type 一樣,有找到,就可以拿出來復(fù)用,重新綁定下數(shù)據(jù)即可。
整體的流程圖如下:
最后,解釋一下開頭的問題
Q1:如果向下滑動(dòng),新一行的5個(gè)卡位的顯示會(huì)去復(fù)用緩存的 ViewHolder,第一行的5個(gè)卡位會(huì)移出屏幕被回收,那么在這個(gè)過程中,是先進(jìn)行復(fù)用再回收?還是先回收再復(fù)用?還是邊回收邊復(fù)用?也就是說,新一行的5個(gè)卡位復(fù)用的 ViewHolder 有可能是第一行被回收的5個(gè)卡位嗎?
答:先復(fù)用再回收,新一行的5個(gè)卡位先去目前的 mCachedViews 和 ViewPool 的緩存中尋找復(fù)用,沒有就重新創(chuàng)建,然后移出屏幕的那行的5個(gè)卡位再回收緩存到 mCachedViews 和 ViewPool 里面,所以新一行5個(gè)卡位和復(fù)用不可能會(huì)用到剛移出屏幕的5個(gè)卡位。
Q2: 在這個(gè)過程中,為什么當(dāng) RecyclerView 再次向上滑動(dòng)重新顯示第一行的5個(gè)卡位時(shí),只有后面3個(gè)卡位觸發(fā)了 onBindViewHolder() 方法,重新綁定數(shù)據(jù)呢?明明5個(gè)卡位都是復(fù)用的。
答:滑動(dòng)場景下涉及到的回收和復(fù)用的結(jié)構(gòu)體是 mCachedViews 和 ViewPool,前者默認(rèn)大小為2,后者為5。所以,當(dāng)?shù)谌酗@示出來后,第一行的5個(gè)卡位被回收,回收時(shí)先緩存在 mCachedViews,滿了再移出舊的到 ViewPool 里,所有5個(gè)卡位有2個(gè)緩存在 mCachedViews 里,3個(gè)緩存在 ViewPool,至于是哪2個(gè)緩存在 mCachedViews,這是由 LayoutManager 控制。上面講解的例子使用的是 GridLayoutManager,滑動(dòng)時(shí)的回收邏輯則是在父類 LinearLayoutManager 里實(shí)現(xiàn),回收第一行卡位時(shí)是從后往前回收,所以最新的兩個(gè)卡位是0、1,會(huì)放在 mCachedViews 里,而2、3、4的卡位則放在 ViewPool 里。
所以,當(dāng)再次向上滑動(dòng)時(shí),第一行5個(gè)卡位會(huì)去兩個(gè)結(jié)構(gòu)體里找復(fù)用,之前說過,mCachedViews 里存放的 ViewHolder 只有原本位置的卡位才能復(fù)用,所以0、1兩個(gè)卡位都可以直接去 mCachedViews 里拿 ViewHolder 復(fù)用,而且這里的 ViewHolder 是不用重新綁定數(shù)據(jù)的,至于2、3、4卡位則去 ViewPool 里找,剛好 ViewPool 里緩存著3個(gè) ViewHolder,所以第一行的5個(gè)卡位都是用的復(fù)用的,而從 ViewPool 里拿的復(fù)用需要重新綁定數(shù)據(jù),才會(huì)這樣只有三個(gè)卡位需要重新綁定數(shù)據(jù)。
Q3:接下去不管是向上滑動(dòng)還是向下滑動(dòng),滑動(dòng)幾次,都不會(huì)再有 onCreateViewHolder() 的日志了,也就是說 RecyclerView 總共創(chuàng)建了17個(gè) ViewHolder,但有時(shí)一行的5個(gè)卡位只有3個(gè)卡位需要重新綁定數(shù)據(jù),有時(shí)卻又5個(gè)卡位都需要重新綁定數(shù)據(jù),這是為什么呢?
答:有時(shí)一行只有3個(gè)卡位需要重新綁定的原因跟Q2一樣,因?yàn)?mCachedView 里正好緩存著當(dāng)前位置的 ViewHolder,本來就是它的 ViewHolder 當(dāng)然可以直接拿來用。而至于為什么會(huì)創(chuàng)建了17個(gè) ViewHolder,那是因?yàn)樵俚谒男械目ㄎ灰@示出來時(shí),ViewPool 里只有3個(gè)緩存,而第四行的卡位又用不了 mCachedViews 里的2個(gè)緩存,因?yàn)檫@兩個(gè)緩存的是6、7卡位的 ViewHolder,所以就需要再重新創(chuàng)建2個(gè) ViewHodler 來給第四行最后的兩個(gè)卡位使用。