本系列博客基于
com.android.support:recyclerview-v7:26.1.0
1.【進(jìn)階】RecyclerView源碼解析(一)——繪制流程
2.【進(jìn)階】RecyclerView源碼解析(二)——緩存機(jī)制
3.【進(jìn)階】RecyclerView源碼解析(三)——深度解析緩存機(jī)制
4.【進(jìn)階】RecyclerView源碼解析(四)——RecyclerView進(jìn)階優(yōu)化使用
5.【框架】基于AOP的RecyclerView復(fù)雜樓層樣式的開發(fā)框架,樓層打通,支持組件化,支持MVP(不用每次再寫Adapter了~)
上一篇博客從源碼角度分析了RecyclerView讀取緩存的步驟,讓我們對(duì)于RecyclerView的緩存有了一個(gè)初步的理解,但對(duì)于RecyclerView的緩存的原理還是不能理解。本篇博客將從實(shí)際項(xiàng)目角度來(lái)理解RecyclerView的緩存原理。

其中可以看到,這里是一個(gè)我們經(jīng)常使用RecycleView實(shí)現(xiàn)列表。右側(cè)輸出面板展示了ScrapView的最大數(shù)量,CacheView的數(shù)量和內(nèi)容,Pool中存在的內(nèi)容。左側(cè)面板展示了onBindViewHolder和onCreateViewHolder的過程。(Demo是基于一篇博客的Demo的拓展:手摸手第二彈,可視化 RecyclerView 緩存機(jī)制)
Demo地址:RecyclerViewStudy感興趣的可以順手點(diǎn)個(gè)star~
1.ScrapViews
起初,我對(duì)于這個(gè)緩存的概念一直很模糊,我嘗試過很多方法想要將這個(gè)緩存中的View讀取出來(lái)看看里面的內(nèi)容,但是發(fā)現(xiàn)這個(gè)緩存的大小總是為0,這個(gè)就讓我很疑惑一個(gè)大
小總是為0的緩存還有什么作用?
無(wú)意中讀到了一篇博客,這篇博客對(duì)于RecyclerView提出了Detach和Remove的概念的區(qū)別,對(duì)于RecycleView的ScrapView進(jìn)行了講解。
1.1 Detach和Remove
所以我們需要區(qū)分兩個(gè)概念,Detach和Remove
detach: 在ViewGroup中的實(shí)現(xiàn)很簡(jiǎn)單,只是將ChildView從ParentView的ChildView數(shù)組中移除,ChildView的mParent設(shè)置為null, 可以理解為輕量級(jí)的臨時(shí)remove, 因
為View此時(shí)和View樹還是藕斷絲連, 這個(gè)函數(shù)被經(jīng)常用來(lái)改變ChildView在ChildView數(shù)組中的次序。View被detach一般是臨時(shí)的,在后面會(huì)被重新attach。
remove: 真正的移除,不光被從ChildView數(shù)組中除名,其他和View樹各項(xiàng)聯(lián)系也會(huì)被徹底斬?cái)?不考慮Animation/LayoutTransition這種特殊情況), 比如焦點(diǎn)被清除,從TouchTarget中被移除等。
1.2 緩存作用
首先我們要了解,任何一個(gè)ViewGroup都會(huì)經(jīng)歷兩次onLayout的過程,對(duì)應(yīng)的childView就會(huì)經(jīng)歷detach和attach的過程,而在這個(gè)過程中,ScrapViews就起了緩存的作用,這樣就不需要重復(fù)創(chuàng)建childView和bind。
所以ScrapView主要用于對(duì)于屏幕內(nèi)的ChildView的緩存,緩存中的ViewHolder不需要重新Bind,緩存時(shí)機(jī)是在onLayout的過程中,并且用完即清空
1.3 Demo驗(yàn)證
我們可以看一下demo驗(yàn)證一下我們的想法。
首先我們重寫了RecylclerView的onLayout方法。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
onLayoutListener.beforeLayout();
super.onLayout(changed, l, t, r, b);
onLayoutListener.afterLayout();
}
在beforLayout時(shí)設(shè)置通過反射將RecyclerView內(nèi)部的mAttachedScrap替換成我們自己重寫的數(shù)據(jù)結(jié)構(gòu)。
public void setAllCache() {
try {
Field mRecycler =
Class.forName("android.support.v7.widget.RecyclerView").getDeclaredField("mRecycler");
mRecycler.setAccessible(true);
RecyclerView.Recycler recyclerInstance =
(RecyclerView.Recycler) mRecycler.get(this);
Class<?> recyclerClass = Class.forName(mRecycler.getType().getName());
Field mAttachedScrap = recyclerClass.getDeclaredField("mAttachedScrap");
mAttachedScrap.setAccessible(true);
mAttachedScrap.set(recyclerInstance, mAttachedRecord);
Field mCacheViews = recyclerClass.getDeclaredField("mCachedViews");
mCacheViews.setAccessible(true);
mCacheViews.set(recyclerInstance, mCachedRecord);
} catch (Exception e) {
e.printStackTrace();
}
}
為什么要這樣做哪?這里利用了Hook的思想。這樣的話,RecyclerView內(nèi)部在對(duì)mAttachedScrap進(jìn)行操作的時(shí)候,比如RecyclerView內(nèi)部對(duì)于mAttachedScrap的添加是使用add(T t)這個(gè)方法,這樣我們?cè)O(shè)置的子類只要重寫這個(gè)add(T t)的方法,在添加的時(shí)候就會(huì)調(diào)用我們子類重寫的add方法。
@Override
public boolean add(T t) {
RecyclerView.ViewHolder vh = (RecyclerView.ViewHolder) t;
RcyLog.log(key + "添加---【position=" + vh.getAdapterPosition() + "】");
if (canReset) {
if (size() + 1 > lastSize) {
maxSize = size() + 1;
}
}
return super.add(t);
}
@Override
public T remove(int index) {
RecyclerView.ViewHolder vh = (RecyclerView.ViewHolder) get(index);
RcyLog.log(key + "移除---【position=" + vh.getAdapterPosition() + "】");
return super.remove(index);
}
可以看到這里,當(dāng)RecyclerView內(nèi)部對(duì)mAttachedScrap進(jìn)行add和remove的時(shí)候,我們都會(huì)進(jìn)行打印log。并且記錄一下maxSize。按照我們的猜想,RecyclerView會(huì)在onLayout的過程中對(duì)mAttachedScrap進(jìn)行添加和移除操作,執(zhí)行完后,mAttachedScrap的大小為0。


可以看到我們打開應(yīng)用Demo的這個(gè)操作,沒有做其他任何操作,僅僅是打開,mAttachedScrap經(jīng)歷了添加屏幕內(nèi)9個(gè)ChildView的過程,并將9個(gè)ChildView移除的過程。而mAttachedScrap的大小剛好為屏幕內(nèi)可以顯示的Item的數(shù)量。
為什么說不需要重寫B(tài)ind哪?通過上篇博客,我們從源碼角度對(duì)RecyclerView的緩存有了一個(gè)初步的了解:
//先從scrap中尋找
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;
}
}
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()) {
//如果FLAG是ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID,則需要調(diào)bind
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);
}
可以看到,我們?cè)赟crap中尋找的時(shí)候,是有一個(gè)判斷!holder.isInvalid(),而對(duì)于需要bind的時(shí)候判斷是否需要bind有一個(gè)判斷holder.isInvalid()。所以兩個(gè)條件是互斥的。
2.CacheViews
CacheViews其實(shí)就是和我們平常使用過程中息息相關(guān)的一個(gè)緩存。CacheViews緩存的特點(diǎn)是CacheViews內(nèi)的緩存在復(fù)用的時(shí)候不需要調(diào)用bind,也就是在滑動(dòng)的過程中,免去了bind的過程,提高滑動(dòng)的效率。
2.1 緩存源碼
首先來(lái)看一下對(duì)于CacheViews內(nèi)緩存的獲取的源碼:
/ /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);
// invalid view holders may be in cache if adapter has stable ids as they can be
// retrieved via getScrapOrCachedViewForId
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;
}
}
首先我們通過源碼可以知道CacheViews是一個(gè)ArrayList,可以看到獲取的時(shí)候是遍歷CacheViews,當(dāng)緩存的ViewHolder和所需要的position相同的并且有效才可以復(fù)用。
和上面分析的一樣,可以知道這個(gè)緩存的ViewHolder是有效的才可以復(fù)用,所以在判斷是否需要bind的時(shí)候,就不需要重新bind了。
接著來(lái)看一下緩存的源碼:
既然是緩存,那肯定是滑動(dòng)過程中的比較直觀:
@Override
public boolean onTouchEvent(MotionEvent e) {
case MotionEvent.ACTION_MOVE: {
.........
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
........
return true;
}
boolean scrollByInternal(int x, int y, MotionEvent ev) {
......
if (x != 0) {
consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
unconsumedX = x - consumedX;
}
if (y != 0) {
consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
unconsumedY = y - consumedY;
}
.......
return consumedX != 0 || consumedY != 0;
}
可以看到這里省略了部分代碼,在onTouchEvent的ACTION_MOVE事件中,可以看到,這里對(duì)canScrollVertically方法進(jìn)行了判斷,并最終將偏移量傳給了scrollByInternal方法,而在scrollByInternal方法中,調(diào)用了LayoutManager的scrollVerticallyBy方法。而scrollVerticallyBy最后調(diào)用了scrollBy方法。
int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
......
//調(diào)用了fill方法
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
......
return scrolled;
}
可以看到fill方法又調(diào)回了前一篇博客分析的fill()方法,這樣就很明顯了。而緩存的源碼其實(shí)上面博客上面提到過一個(gè)方法onLayoutChild()方法里面有個(gè)detachAndScrapAttachedViews方法。
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);
}
}
/**
* 1.Recycle操作對(duì)應(yīng)的是removeView, View被remove后調(diào)用Recycler的recycleViewHolderInternal回收其ViewHolder
2.Scrap操作對(duì)應(yīng)的是detachView,View被detach后調(diào)用Reccyler的scrapView暫存其ViewHolder
* @param recycler
* @param index
* @param view
*/
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.shouldIgnore()) {
if (DEBUG) {
Log.d(TAG, "ignoring view " + viewHolder);
}
return;
}
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
//注意這里是remove
removeViewAt(index);
//往cacheview和pool中
recycler.recycleViewHolderInternal(viewHolder);
} else {
//注意這里是detach
detachViewAt(index);
//存到scrap中
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
這里就可以看到前面所說的Remove和Detach的區(qū)別,如果是remove,會(huì)執(zhí)行recycleViewHolderInternal(viewHolder);方法,而這個(gè)方法最終會(huì)將ViewHolder加入CacheView和Pool中,而當(dāng)是Detach,會(huì)將View加入到ScrapViews中,注意View和ViewHolder的區(qū)別,前面提到過,ScrapViews是對(duì)View的復(fù)用,而CacheView和Pool是對(duì)ViewHolder的復(fù)用。
既然是看CacheViews,那么就看一下recycleViewHolderInternal方法。
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();
//如果超過默認(rèn)大小,則刪除第一個(gè)
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
//從CacheViews中刪除第一個(gè),并加入到Pool中
recycleCachedViewAt(0);
cachedViewSize--;
}
......
//加入緩存
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
//不然直接加入Pool中
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
.......
}
可以看到幾個(gè)關(guān)鍵邏輯:
1.如果超過默認(rèn)大小,則會(huì)移除CacheViews中的第一個(gè),并加入到Pool中,然后在將需要加入緩存的ViweHolder加入到CacheView中。
2.如果不能加入到CacheViews中,則加入到Pool中。
2.2 Demo驗(yàn)證
(1)進(jìn)入應(yīng)用
我們首先進(jìn)入應(yīng)用會(huì)發(fā)現(xiàn)當(dāng)前CacheViews的大小是0,也就是說進(jìn)入應(yīng)用時(shí)沒有滑動(dòng),是沒有任何ViewHolder回收的,這不需要解釋吧。。。,而且Bind也只走了頁(yè)面渲染的0-8。

(2)向下滑動(dòng)一個(gè),第一個(gè)移除
這時(shí)我們向下滑動(dòng),加載出第9個(gè)

可以看到這時(shí)候除了加載了頁(yè)面的
position=9,還提前加載出了position=10,執(zhí)行了onBind,而這時(shí),由于第一個(gè)移出界面,所以position=0也就被加入到了CacheViews中。(3)向上滑動(dòng),再顯示第一個(gè)

這時(shí)候我們會(huì)發(fā)現(xiàn)幾個(gè)特別的點(diǎn):
1.onBind的面板沒有新的Log,說明新出來(lái)的
position=0沒有走onBind方法。
2.CacheViews中由剛才保存的position=0和position=10,變成了position=10和position=9
由此可見:
CacheViews中緩存的ViewHolder當(dāng)被復(fù)用的時(shí)候是不會(huì)走Bind流程的
3.RecycledViewPool
其實(shí)根據(jù)前一節(jié)的講解,我們已經(jīng)對(duì)RecycleView的緩存有了一個(gè)很具體的了解了,RecyclerPool其實(shí)是RecyclerView區(qū)分ListView的一個(gè)亮點(diǎn)。利用這級(jí)緩存我們可以實(shí)現(xiàn)多個(gè)RecyclerView之間的ViewHolder的復(fù)用。(關(guān)于這一點(diǎn)的利用我準(zhǔn)備在下一篇博客對(duì)RecycleView使用的技巧進(jìn)行舉例講解)
3.1 緩存源碼
首先我們看一下ReyclerPool的結(jié)構(gòu)。
public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 5;
static class ScrapData {
ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
SparseArray<ScrapData> mScrap = new SparseArray<>();
}
可以看到RecyclerPool內(nèi)部其實(shí)是一個(gè)SparseArray,可想而知,key就是我們的ViewType,而Value是ArrayList<ViewHolder>。
我們來(lái)看一下RecyclerPool的put方法。
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList<ViewHolder> 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");
}
//重置ViewHolder
scrap.resetInternal();
scrapHeap.add(scrap);
}
其中resetInternal方法值得我們注意。
void resetInternal() {
mFlags = 0;
mPosition = NO_POSITION;
mOldPosition = NO_POSITION;
mItemId = NO_ID;
mPreLayoutPosition = NO_POSITION;
mIsRecyclableCount = 0;
mShadowedHolder = null;
mShadowingHolder = null;
clearPayload();
mWasImportantForAccessibilityBeforeHidden = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
clearNestedRecyclerViewIfNotNested(this);
}
可以看到所有被put進(jìn)入RecyclerPool中的ViewHolder都會(huì)被重置,這也就意味著RecyclerPool中的ViewHolder再被復(fù)用的時(shí)候是需要重新Bind的。這一點(diǎn)就可以區(qū)分和CacheViews中緩存的區(qū)別。
總結(jié)
還是那篇Bugly博客中的圖片吧(都怪我太懶了。。。)

看過上面的分析,這張圖片就很好理解了。
最后
給大家分享幾篇我認(rèn)為不錯(cuò)的RecyclerView源碼分析的博客吧,我的分析其中有些地方就是從這些博客中學(xué)習(xí)來(lái)的。
1.Bugly分析ListView和RecyclerView的區(qū)別的,建議深入了解后再看
2.CSDN的一個(gè)大神的分析,分了有6篇博客,值得一讀
3.一篇很好的RecyclerView的源碼分析博客,適合深入閱讀
4.可視化RecyclerView緩存機(jī)制,也就是本篇博客Demo的參考
5.一篇將RecyclerView的緩存講的通俗易懂的博客,源碼不是比較深入,但是很好理解
。。。還有一些就不上了,以上5篇是我認(rèn)為很值得反復(fù)閱讀學(xué)習(xí)的。
下篇博客可能是RecyclerView分析系列的結(jié)尾篇了,可能從實(shí)際使用角度分析一些我所了解的RecyclerView的一些進(jìn)階知識(shí)
相關(guān)
基于AOP的RecyclerView復(fù)雜樓層樣式的開發(fā)框架,樓層打通,支持組件化,支持MVP(不用每次再寫Adapter了~)-EMvp
Star??支持一下~
歡迎提issues討論~