系列文章:
前言
此前已經(jīng)介紹完RecyclerView
的繪制流程护赊,在繪制流程中我們還殘留RecyclerView
的緩存機(jī)制的問題沒有解釋院刁。
在分析ListView
過程中佑稠,我們先分析了ListView
中緩存的核心實(shí)現(xiàn)類RecycleBin
题画,類似的在RecyclerView中也存在一個(gè)Recycler類,其中代碼邏輯比RecycleBin
復(fù)雜六水。其定位是RecyclerView的View管理者桨啃,其功能包括生成新View,復(fù)用舊View不瓶,回收View,重新綁定View灾杰。外部只需調(diào)用其相關(guān)接口即可蚊丐,而無需關(guān)心其內(nèi)部的具體實(shí)現(xiàn)細(xì)節(jié)。
下面先列出Recycler中涉及到緩存機(jī)制的相關(guān)變量:
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
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;
private RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
static final int DEFAULT_CACHE_SIZE = 2;
}
RecyclerView緩存機(jī)制初步分析
注意:以下涉及到
LayoutManager
的子類均以LinearLayoutManager
為例
LayoutState.next方法
在上篇博客中我們提到RecyclerView中的緩存機(jī)制開始于LayoutState.next
方法艳吠,下面我們就進(jìn)入next
方法一步一步來解析RecyclerView的緩存機(jī)制:
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
可以看到這里除了我們之前提到的Recycler
的緩存外麦备,還存在緩存mScrapList
,mScrapList
被LinearLayoutManager
持有昭娩,其聲明如下:
List<RecyclerView.ViewHolder> mScrapList = null;
上述聲明說明mScrapList
是一個(gè)ViewHolder
類型的List
凛篙。當(dāng)LinearLayoutManager
需要布局特定視圖時(shí),它會(huì)設(shè)置mScrapList
栏渺,在這種情況下呛梆,LayoutState
將僅從該列表返回視圖,如果找不到則返回null磕诊。nextViewFromScrapList
的源碼如下:
private View nextViewFromScrapList() {
final int size = mScrapList.size();
for (int i = 0; i < size; i++) {
final View view = mScrapList.get(i).itemView;
final RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams();
if (lp.isItemRemoved()) {
continue;
}
if (mCurrentPosition == lp.getViewLayoutPosition()) {
assignPositionFromScrapList(view);
return view;
}
}
return null;
}
上述具體代碼暫不分析填物,Recycler
是被RecyclerView
持有纹腌,緩存的重頭戲還是RecyclerView
的內(nèi)部類Recycler
,當(dāng)mScrapList
為null時(shí)滞磺,則調(diào)用了Recycler的getViewForPosition
方法升薯。
Recycler.getViewForPosition方法
此方法就是從Recycler
處獲取View:
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
getViewForPosition
方法最后調(diào)用的是tryGetViewHolderForPositionByDeadline
方法,此方法的注釋寫的很清楚击困,就是試圖從Recycler
的scrap涎劈,cache,RecycldViewPool獲取ViewHolder阅茶,都獲取不到則直接創(chuàng)建ViewHolder责语,很明顯分析此方法代碼就要從這四種情況來討論。
Recycler.tryGetViewHolderForPositionByDeadline方法
// Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
// cache, the RecycledViewPool, or creating it directly.
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
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);
....
}
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
+ "position " + position + "(offset:" + offsetPosition + ")."
+ "state:" + mState.getItemCount() + exceptionLabel());
}
final int type = mAdapter.getItemViewType(offsetPosition);
// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
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 (holder == null) { // fallback to pool
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
+ position + ") fetching from shared pool");
}
holder = getRecycledViewPool().getRecycledView(type);
...
}
if (holder == null) {
...
holder = mAdapter.createViewHolder(RecyclerView.this, type);
...
}
}
...
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
+ exceptionLabel());
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
...
return holder;
}
下面就一步一步來分析tryGetViewHolderForPositionByDeadline
方法吧:
非常規(guī)緩存:從mChangedScrap中獲取ViewHodler
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
public boolean isPreLayout() {
return mInPreLayout;
}
首先判斷mInPreLayout
變量目派,它默認(rèn)為false坤候,當(dāng)有動(dòng)畫時(shí)此變量才為true,再來看看getChangedScrapViewForPosition()
方法企蹭,源碼如下:
ViewHolder getChangedScrapViewForPosition(int position) {
...
// find by position
for (int i = 0; i < changedScrapSize; i++) {
final ViewHolder holder = mChangedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
// find by id
if (mAdapter.hasStableIds()) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
if (offsetPosition > 0 && offsetPosition < mAdapter.getItemCount()) {
final long id = mAdapter.getItemId(offsetPosition);
for (int i = 0; i < changedScrapSize; i++) {
final ViewHolder holder = mChangedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
}
}
return null;
}
從上面可以看出白筹,getChangedScrapViewForPosition()
方法通過兩種方式從Recycler
中的mChangedScrap
獲取ViewHolder
,同時(shí)只有有動(dòng)畫時(shí)谅摄,mChangedScrap
才起作用徒河,這也是為什么沒有將mChangedScrap
放在常規(guī)緩存中。
第一級(jí)緩存:從mAttachedScrap和mCacheViews中獲取View(通過位置嘗試)
先大概看下相關(guān)源碼:
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
...
}
}
此段代碼中先通過getScrapOrHiddenOrCachedHolderForPosition
方法來獲取ViewHolder
送漠,源碼如下:
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
...
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
...
}
if (!dryRun) {
View view = mChildHelper.findHiddenNonRemovedView(position);
...
}
// Search in our first-level recycled view cache.
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
...
}
return null;
}
此方法大概可以分為以下三步:
- 先從
mAttachedScrap
中獲取ViewHolder
顽照; - 從
HiddenViews
中獲取View
,再根據(jù)View
獲取ViewHolder
闽寡; - 從
mCachedViews
中獲取ViewHolder
代兵。
其流程上可以總結(jié)如下:
RecyclerViewScrapTest1.png
第一級(jí)緩存:從mAttachedScrap和mCacheViews中獲取ViewHolder(通過id嘗試)
final int type = mAdapter.getItemViewType(offsetPosition);
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
第一級(jí)緩存的第二種嘗試先執(zhí)行了mAdapter.getItemViewType(offsetPosition)
,getItemViewType
方法是RecyclerView
用于多類型子View的相關(guān)方法爷狈≈灿埃可以看出對(duì)于前面通過位置獲取ViewHolder的第一級(jí)緩存沒有考慮子View的type情況。
接下來又對(duì)mAdapter.hasStableIds()
進(jìn)行了判斷涎永,此方法返回的是mHasStableIds
變量思币,關(guān)于hasStableIds
方法這里給出一些Android 源碼注釋,但是苦于無法很好的進(jìn)行翻譯羡微,故而貼出英文版:
Returns true if this adapter publishes a unique value that can act as a key for the item at a given position in the data set. If that item is relocated in the data set, the ID returned for that item should be the same.
若hasStableIds()
方法返回true谷饿,則接著調(diào)用getScrapOrCachedViewForId
方法,源碼如下:
ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {
// Look in our attached views first
final int count = mAttachedScrap.size();
for (int i = count - 1; i >= 0; i--) {
final ViewHolder holder = mAttachedScrap.get(i);
...
}
// Search the first-level cache
final int cacheSize = mCachedViews.size();
for (int i = cacheSize - 1; i >= 0; i--) {
final ViewHolder holder = mCachedViews.get(i);
...
}
return null;
}
此方法的代碼邏輯和上述的差不多妈倔,但在判斷方面多了有關(guān)id和type的判斷(具體源碼可以見前文tryGetViewHolderForPositionByDeadline
方法源碼處)博投,因此當(dāng)我們將mHasStableIds
變量設(shè)為true后,我們需要重寫holder.getItemId() 方法启涯,來為每一個(gè)item設(shè)置一個(gè)單獨(dú)的id贬堵,這也和hasStableIds()方法的官方注釋相符合恃轩。
其流程上可以總結(jié)如下:
第二級(jí)緩存:從mViewCacheExtension中獲取ViewHolder
直接上源碼:
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);
...
}
}
mViewCacheExtension
用于自定義緩存,mViewCacheExtension
是一個(gè)ViewCacheExtension
對(duì)象黎做,來看下ViewCacheExtension
定義:
public abstract static class ViewCacheExtension {
/**
* Returns a View that can be binded to the given Adapter position.
*/
public abstract View getViewForPositionAndType(Recycler recycler, int position, int type);
}
這個(gè)類是個(gè)抽象類叉跛,只定義了一個(gè)抽象方法,開發(fā)者若想實(shí)現(xiàn)自定義緩存蒸殿,則需實(shí)現(xiàn)此方法筷厘。可以看出自定義緩存沒什么限制,完全由開發(fā)者自己實(shí)現(xiàn)算法。此外普通的緩存操作都是有存放和獲取的接口乙各,而此類中只提供了獲取接口,沒有存放接口充石。
第三級(jí)緩存:從RecycledViewPool中獲取ViewHolder
接著看余下的代碼:
if (holder == null) { // fallback to pool
...
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
這個(gè)緩存是針對(duì)RecycledViewPool
的,先調(diào)用getRecycledViewPool()
方法獲取RecycledViewPool
對(duì)象:
RecycledViewPool getRecycledViewPool() {
if (mRecyclerPool == null) {
mRecyclerPool = new RecycledViewPool();
}
return mRecyclerPool;
}
如果mRecyclerPool
為null霞玄,則新建一個(gè)并返回骤铃,獲取到mRecyclerPool
后,調(diào)用了RecycledViewPool.getRecycledView()
方法坷剧。
RecyclerViewPool類
先來看下此類的大概結(jié)構(gòu):
public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 5;
static class ScrapData {
@UnsupportedAppUsage
ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
SparseArray<ScrapData> mScrap = new SparseArray<>();
private int mAttachCount = 0;
...
public ViewHolder getRecycledView(int viewType) {
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;
}
...
可以看出RecycledViewPool
使用了SparseArray
這個(gè)數(shù)據(jù)結(jié)構(gòu)惰爬,SparseArray
內(nèi)部的key
就是我們的ViewType
,value
存放的是RecycledViewPool.ScrapData
類型數(shù)據(jù)惫企,ScrapData
中有ArrayList<ViewHolder>
類型數(shù)據(jù)撕瞧,默認(rèn)最大容量為5。
此外狞尔,getRecycledView()
方法先通過mScrap
由ViewType
獲取scrapData
后丛版,去除并返回mScrapHeap
中的最后一個(gè)數(shù)據(jù)。
第四級(jí)緩存:創(chuàng)建ViewHolder(onCreateViewHolder方法)
if (holder == null) {
...
holder = mAdapter.createViewHolder(RecyclerView.this, type);
...
}
創(chuàng)建ViewHolder
主要調(diào)用了Adapter
的createViewHolder
方法:
public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) {
...
final VH holder = onCreateViewHolder(parent, viewType);
...
}
從上可以看到最終調(diào)用了onCreateViewHolder
方法沪么,而此方法正是我們繼承RecyclerView.Adapter
的自定義Adapter所要實(shí)現(xiàn)的抽象方法硼婿。此方法中我們會(huì)創(chuàng)建一個(gè)ViewHolder
并返回。因此如果能正確創(chuàng)建ViewHolder
禽车,我們最終肯定能獲取到一個(gè)ViewHolder
。
onBindViewHolder方法
通過以上多級(jí)緩存方式獲取到ViewHolder
后刊殉,可能還需要調(diào)用onBindViewHolder
方法殉摔,而且我們知道在使用RecyclerView
時(shí)自定義的Adapter中重寫了onCreateViewHolder
方法和onBindViewHolder
方法,這兩個(gè)方法是Adapter中最重要的记焊。那么再何種情況下會(huì)回調(diào)onBindViewHolder
方法呢逸月?
...
else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
...
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
當(dāng)ViewHolder
符合以上代碼中幾個(gè)條件之一時(shí),便會(huì)調(diào)用tryBindViewHolderByDeadline
方法遍膜,此方法源碼如下:
private boolean tryBindViewHolderByDeadline(ViewHolder holder, int offsetPosition,
int position, long deadlineNs) {
...
mAdapter.bindViewHolder(holder, offsetPosition);
...
}
tryBindViewHolderByDeadline
中調(diào)用了bindViewHolder
方法碗硬,其源碼如下:
public final void bindViewHolder(VH holder, int position) {
...
onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());
...
}
可見在此方法中最終回調(diào)了onBindViewHolder
方法瓤湘,而開發(fā)者一般會(huì)在此方法中做一些View的內(nèi)容設(shè)置工作。
總結(jié)
至此我們基本解釋了RecyclerView的四級(jí)緩存恩尾,來做個(gè)總結(jié):
-
RecyclerView
有四級(jí)緩存:mAttachedScrap弛说,mCacheViews,ViewCacheExtension翰意,RecycledViewPool木人,創(chuàng)建ViewHolder; -
mAttachedScrap,mCacheViews
在第一次嘗試的時(shí)候只是對(duì)View的復(fù)用冀偶,不區(qū)分type醒第,只通過位置來尋找,但在第二次嘗試的時(shí)候是區(qū)分了type进鸠,是對(duì)于ViewHolder
的復(fù)用稠曼,ViewCacheExtension,RecycledViewPool
是對(duì)于ViewHolder
的復(fù)用,而且區(qū)分type客年; - 如果緩存ViewHolder時(shí)發(fā)現(xiàn)超過了mCachedView的限制蒲列,會(huì)將最老的ViewHolder(也就是mCachedView緩存隊(duì)列的第一個(gè)ViewHolder)移到RecycledViewPool中。
放上騰訊Bugly的相關(guān)博客對(duì)RecyclerView
的緩存的流程圖吧(自己手動(dòng)畫了下搀罢,加強(qiáng)理解):
RecyclerView緩存機(jī)制深度分析
mAttachedScrap作用
介紹mAttachedScrap
的作用之前蝗岖,我們先需要了解Detach和Remove
兩個(gè)概念的區(qū)別:
-
Detach
用于將ViewGroup
中的子View從父View的子View數(shù)組中移除,子View的ParentView屬性置為null榔至;這個(gè)操作是一個(gè)輕量級(jí)臨時(shí)的Remove抵赢,被Detach的子View還是和View樹有千絲萬縷的聯(lián)系,在后面會(huì)被重新Attach到父View中唧取。 - Remove用于真正的移除子View铅鲤,不僅從父View的子View數(shù)組中移除,其他和View樹各項(xiàng)聯(lián)系也會(huì)被徹底斬?cái)喾愕埽热缃裹c(diǎn)被清除邢享。
介紹完Detach和Remove
的區(qū)別后,我們?cè)賮碚fmAttachedScrap
的真正作用淡诗。我們知道任何ViewGroup
都會(huì)經(jīng)歷兩次onLayout過程骇塘,因此對(duì)應(yīng)的子View就會(huì)經(jīng)歷detach和attach過程,在這個(gè)過程中韩容,mAttachedScrap
就起到了緩存作用款违,保證ViewHolder
不需要重新調(diào)用onBindViewHolder
方法。下面具體分析下此過程:
- 在第一次
onLayout
過程中群凶,會(huì)調(diào)用LayoutManager.onLayoutChildren
方法對(duì)子View布局插爹,其中會(huì)調(diào)用detachAndScrapAttachedViews()
方法對(duì)子View進(jìn)行回收,但在第一次布局過程中還沒有子View,所以此方法在第一次onLayout過程中無作用:
public void detachAndScrapAttachedViews(Recycler recycler) {
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
接著LayoutManager.onLayoutChildren
方法又會(huì)調(diào)用fill方法來填充屏幕赠尾,fill方法中調(diào)用了layoutState.next去獲取View力穗,接著利用了前文所說的多級(jí)緩存去獲取ViewHolder,但是在第一次布局中前三級(jí)緩存都不能獲取到ViewHolder或View气嫁,因此只能調(diào)用onCreateViewHolder方法來創(chuàng)建ViewHolder当窗。
- 在第二次onLayout過程中,又會(huì)調(diào)用
LayoutManager.onLayoutChildren
方法對(duì)子View布局杉编,再來看看此時(shí)的detachAndScrapAttachedViews()
方法執(zhí)行情況超全,由于此時(shí)已經(jīng)又子View了,所以便會(huì)執(zhí)行scrapOrRecycleView
方法:
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
...
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
首先調(diào)用了getChildViewHolderInt
方法邓馒,該方法源碼較為簡(jiǎn)單嘶朱,返回子View對(duì)應(yīng)的ViewHolder,接下來又對(duì)ViewHolder
進(jìn)行驗(yàn)證光酣,如果ViewHolder無效且未被移除且不擁有穩(wěn)定的id疏遏,則將該View移除(Remove)并調(diào)用recycleViewHolderInternal
方法進(jìn)行回收ViewHolder救军,此方法是將ViewHolder加入mCacheViews
和RecyclerViewPool
中财异;否則detach該View疫鹊,并調(diào)用了recycler.scrapView方法捞奕,將ViewHolder
加入mAttachedScrap
或mChangedScrap
中墩邀,此處我們先來看scrapView
方法,后續(xù)再看recycleViewHolderInternal
方法灌闺。
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
throw new IllegalArgumentException("Called scrap view with an invalid view."
+ " Invalid views cannot be reused from scrap, they should rebound from"
+ " recycler pool.");
}
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}
在此方法中首先調(diào)用了getChildViewHolderInt
方法獲取子View對(duì)應(yīng)的ViewHolder艰争,接下來對(duì)該ViewHolder
進(jìn)行了相關(guān)條件判斷,符合第一個(gè)條件則將該ViewHolder加入mAttachedScrap中桂对,否則將該ViewHolder加入mChangedScrap中甩卓。
綜上可見,mAttachedScrap
的主要作用就是在RecyclerView第二次layout過程中蕉斜,其大小一般即為首次加載時(shí)屏幕上能放下的最大子View數(shù)逾柿。(例如:首次加載時(shí),屏幕上有10個(gè)子View蛛勉,則mAttachedScrap
的大小即為10)
mCacheViews作用
mCacheViews
真正起作用是RecyclerView發(fā)生滑動(dòng)時(shí)鹿寻,類似于ListView
中RecycleBin
的scrapView
。再來看一下關(guān)于mCacheViews
的兩處緩存代碼:
- 第一處:
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
if (!dryRun) {
mCachedViews.remove(i);
}
if (DEBUG) {
Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
+ ") found match in cache: " + holder);
}
return holder;
}
}
第一處獲取的時(shí)候是遍歷mCacheViews
诽凌,當(dāng)緩存的ViewHolder和所需要的position相同的并且有效才可以復(fù)用毡熏,滿足復(fù)用條件時(shí),當(dāng)dryRun為false時(shí)侣诵,從mCacheViews中移除此ViewHolder痢法。
- 第二處:
final int cacheSize = mCachedViews.size();
for (int i = cacheSize - 1; i >= 0; i--) {
final ViewHolder holder = mCachedViews.get(i);
if (holder.getItemId() == id) {
if (type == holder.getItemViewType()) {
if (!dryRun) {
mCachedViews.remove(i);
}
return holder;
} else if (!dryRun) {
recycleCachedViewAt(i);
return null;
}
}
}
第一處獲取的時(shí)候也是遍歷mCacheViews
,當(dāng)緩存的ViewHolder和所需要的id和和ViewType都相同的才可以復(fù)用杜顺,滿足復(fù)用條件時(shí)财搁,當(dāng)dryRun為false時(shí),從mCacheViews中移除此ViewHolder躬络。
看完了如何從mCacheViews
中獲取ViewHolder
尖奔,我們還需要說明下是如何向mCacheViews
中添加ViewHolder
的,前面介紹mAttachedScrap
作用時(shí),提到了recycleViewHolderInternal
方法提茁,此方法是將ViewHolder
加入mCacheViews
和RecyclerViewPool
中淹禾。下面便來具體看看代碼:
void recycleViewHolderInternal(ViewHolder holder) {
...
if (forceRecycle || holder.isRecyclable()) {
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
// Retire oldest cached view
int cachedViewSize = mCachedViews.size();
// 如果size大于等于最大容量,則刪除第一個(gè)
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize--;
}
...
// 加入mCacheViews
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
// 否則加入RecyclerViewPool中
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
...
}
此方法的邏輯比較清晰茴扁,大概分為以下幾點(diǎn):
- 如果
mCachedViews
現(xiàn)有大小>=
默認(rèn)大小铃岔,則會(huì)移除mCachedViews
中的第一個(gè)ViewHolder
,并調(diào)用recycleCachedViewAt
方法使被移除的ViewHolder
加入到RecyclerViewPool
中峭火,最后將需要加入緩存的ViweHolder
加入到CacheViews
中毁习; - 如果加入到
mCacheViews
中失敗了,則加入到RecyclerViewPool
中卖丸。
至于為什么mCacheViews
在RecyclerView
滑動(dòng)時(shí)起作用纺且,后文闡述RecyclerView
滑動(dòng)過程中的緩存機(jī)制中再描述。
RecyclerViewPool作用
前面已經(jīng)介紹過RecyclerViewPool
的結(jié)構(gòu)了坯苹,是通過RecyclerViewPool
的getRecycledView
方法來獲取ViewHolder
的隆檀,前文已說明過此方法,在此方法中獲取到ViewHolder
后移除了RecyclerViewPool
中的緩存粹湃。
已清楚如何通過RecyclerViewPool
獲取ViewHolder
恐仑,再來看下是在哪里向RecyclerViewPool
中添加ViewHolder
的,正好前文敘述mCacheViews
作用時(shí)提到了recycleViewHolderInternal
方法为鳄,此方法分為兩點(diǎn)裳仆,第一點(diǎn)中當(dāng)mCachedViews現(xiàn)有大小>=默認(rèn)大小時(shí)會(huì)將移除的ViewHolder通過recycleCachedViewAt方法使被移除的ViewHolder
加入到RecyclerViewPool
中,recycleCachedViewAt調(diào)用了addViewHolderToRecycledViewPool方法孤钦;第二點(diǎn)是加入到mCacheViews中失敗了歧斟,則加入到RecyclerViewPool中,調(diào)用了addViewHolderToRecycledViewPool方法將ViewHolder加入RecyclerPool中偏形,因此來看addViewHolderToRecycledViewPool
源碼:
void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
...
getRecycledViewPool().putRecycledView(holder);
}
此方法最終調(diào)用了putRecycledView
方法:
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap;
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return;
}
if (DEBUG && scrapHeap.contains(scrap)) {
throw new IllegalArgumentException("this scrap item already exists");
}
scrap.resetInternal();
scrapHeap.add(scrap);
}
其中resetInternal
方法值得我們注意静袖,其中所有被put進(jìn)入RecyclerPool中的ViewHolder都會(huì)被重置,這也就意味著RecyclerPool中的ViewHolder再被復(fù)用的時(shí)候是需要重新調(diào)用onBindViewHolder方法俊扭,這一點(diǎn)可以區(qū)分和mCacheViews
中緩存的區(qū)別队橙。
RecyclerView滑動(dòng)時(shí)的緩存機(jī)制
分析所有View的滑動(dòng)時(shí),都需要分析onTouchEvent
方法萨惑,下面來看下RecyclerView
的nTouchEvent
方法:
public boolean onTouchEvent(MotionEvent e) {
...
case MotionEvent.ACTION_MOVE: {
...
if (scrollByInternal(canScrollHorizontally ? dx : 0,canScrollVertically ? dy : 0, vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
...
return true;
}
我們重點(diǎn)關(guān)注onTouchEvent
的ACTION_MOVE
事件捐康,可以看到,其中對(duì)canScrollHorizontally
和canScrollVertically
進(jìn)行了判斷庸蔼,并最終將偏移量傳給了scrollByInternal
方法解总,而在scrollByInternal
方法中,調(diào)用了LayoutManager
的scrollHorizontallyBy
或scrollVerticallyBy
方法姐仅,這兩個(gè)方法的具體實(shí)現(xiàn)都在子類LayoutManager中花枫,以scrollVerticallyBy
為例刻盐,其最后調(diào)用了scrollBy
方法:
int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
...
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
...
}
可以看到這里調(diào)用了fill方法,又回到了分析RecyclerView繪制流程中乌昔,fill中真正填充子View的方法是layoutChunk()隙疚,具體過程可參考RecyclerView繪制流程壤追。
至此磕道,已基本理清楚關(guān)于RecyclerView
的緩存機(jī)制問題了,再來個(gè)關(guān)于這幾種不同緩存方式的對(duì)比總結(jié)吧: