本文已受權微信公衆號【玉剛說】獨家原創發佈緩存
RecyclerView大概是Android開發者接觸最多的一個控件了,官方對其作了很好的封裝抽象,使得它既靈活又好用,可是你真的瞭解它麼?在它簡單的使用方式之下着實是不簡單,首先咱們看一下官方對它的介紹:bash
A flexible view for providing a limited window into a large data set.微信
很簡單,就一句話「爲大量數據集提供一個有限展現窗口的靈活視圖」怎麼展現大量的數據是個技術活,這些數據伴隨着滾動逐漸展現在咱們眼前,可是展現過的滾走的視圖呢?它們是否還存在?我想你們確定知道它們是要被回收的,否者來個幾百上千條數據那還不OOM了。那麼咱們今天就圍繞RecyclerView的視圖回收機制來談一談,到底RecyclerView的回收機制是怎樣的。app
咱們先了解下Recycler的緩存結構是怎樣的,先了解兩個專業詞彙:ide
RecyclerView的緩存類型呢基本也就是上面的兩種,這時可能有同窗要站出來講我不對了,胡說,RecyclerView明明有四級緩存,怎麼就兩種了,騷年稍安勿躁,且聽我來慢慢分解。首先咱們先看一個RV(RecyclerView在後文簡稱RV)的內部類Recycler。佈局
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
…… 省略 ……
}
複製代碼
就是介個類掌握着RV的緩存大權,從上面的代碼片斷咱們能夠看到這個類聲明瞭五個成員變量。咱們一個個的來講一下:性能
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;
…… 省略 ……
}
SparseArray<ScrapData> mScrap = new SparseArray<>();
…… 省略後面代碼 ……
}
複製代碼
上面咱們介紹了RV的各緩存層級,可是它們是怎麼工做的呢?爲何要設計這些層級呢?別急,咱們去源碼中找找答案。一葉落而知天下秋,咱們就從官方自帶的最簡單的佈局管理者LinearLayoutManager入手,來看看到底如何使用這幾級緩存寫出一個合格的佈局管理者。flex
首先咱們看一下RV從無到有是怎麼顯示出數據來的。你們因該知道一個視圖的顯示要通過onMeasure、onLayout、onDraw三個方法,那麼咱們就先從第一個方法onMeasure入手,來看看裏面作了什麼。動畫
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
if (mLayout.mAutoMeasure) {
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
dispatchLayoutStep2();
}
}
複製代碼
上面代碼省略了一些無關代碼,咱們只看咱們關心的,dispatchLayoutStep1和2方法,1方法中若是mState.mRunPredictiveAnimations爲true會調用mLayout.onLayoutChildren(mRecycler, mState)這個方法,可是通常RV的預測動畫都爲false,因此咱們看一下2方法,方法中一樣調用了mLayout.onLayoutChildren(mRecycler, mState)方法,來看一下:ui
//已省略無關代碼
private void dispatchLayoutStep2() {
eatRequestLayout();
onEnterLayoutOrScroll();
// Step 2: Run layout
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
mState.mLayoutStep = State.STEP_ANIMATIONS;
onExitLayoutOrScroll();
resumeRequestLayout(false);
}
複製代碼
這裏onLayoutChildren方法是必走的,而mLayout是RV的成員變量,也就是LayoutManager,接下來咱們去LinearLayoutManager裏看看onLayoutChildren方法作了什麼。
//已省略無關代碼
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler);
if (mAnchorInfo.mLayoutFromEnd) {
// fill towards start
fill(recycler, mLayoutState, state, false);
// fill towards end
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
} else {
// fill towards end
fill(recycler, mLayoutState, state, false);
// fill towards start
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
}
}
複製代碼
這個方法挺長的,咱們只看最關心的,來看下detachAndScrapAttachedViews(recycler)方法中作了什麼。
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);
}
}
複製代碼
若是有子view調用了scrapOrRecycleView(recycler, i, v)方法,繼續追蹤。
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);
}
}
複製代碼
正常開始佈局的時候會進入else分支,首先是調用detachViewAt(index)來分離視圖,而後調用了recycler.scrapView(view)方法。前面咱們說過Recycler是RV的內部類,是管理RV緩存的核心類,而後咱們繼續追蹤這個srapView方法,看看裏面作了什麼。
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("……");
}
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
}
}
複製代碼
這裏咱們看到了熟悉的身影,「mAttachedScrap」,到此爲止咱們知道了,onLayoutChildren方法中調用detachAndScrapAttachedViews方法把存在的子view先分離而後緩存到了AttachedScrap中。咱們回到onLayoutChildren方法中看看接下來作了什麼,咱們發現它先判斷了方向,由於LinearLayoutManager有橫縱兩個方向,不管哪一個方向最後都是調用fill方法,見名知意,這是個填充佈局的方法,fill方法中又調用了layoutChunk這個方法,咱們看一眼這個方法。
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
if (view == null) {
return;
}
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
}
}
複製代碼
該方法中咱們看到經過layoutState.next(recycler)方法來拿到視圖,若是這個視圖爲null那麼方法終止,不然就會調用addView方法將視圖添加或者從新attach回來,這個咱們不關心,咱們看看是怎麼拿到視圖的。
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
複製代碼
首先咱們看到若是mScrapList不爲空會去其中取視圖,mScrapList是什麼呢?實際上它就是mAttachedScrap,可是它是隻讀的,並且只有在開啓預測動畫時纔會被賦值,因此咱們忽略它便可。重點關注下recycler.getViewForPosition(mCurrentPosition)方法,這個方法通過層層調用,最終是調用的Recycler類中的「tryGetViewHolderForPositionByDeadline(int position,boolean dryRun,long deadlineNs)」方法,接下來看一下這個方法作了哪些事。
@Nullable
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) {
// 2) Find from scrap/cache via stable ids, if exists
if (holder == null && mViewCacheExtension != null) {
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
}
if (holder == null) {
holder = getRecycledViewPool().getRecycledView(type);
}
if (holder == null) {
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
}
return holder;
}
複製代碼
這段代碼着實作了很多事情,獲取View和綁定View都是在這個方法中完成的,固然關於綁定和其它的無關代碼這裏就不貼了。咱們一步步的看一下:
到此爲止咱們獲取一個視圖的流程就講完了,獲取到視圖以後就是怎麼擺放視圖並添加到RV之中,而後最終展現到咱們面前。細心的小夥伴可能發現這個流程貌似有點問題啊?第一次進入onLayoutChildren時尚未任何子view,在fill方法前等於沒有緩存子view,全部的子View都是第五步onCreateViewHolder建立而來的。實際上這裏的設計是有道理的,除了一些特殊狀況onLayoutChildren方法會被屢次調用外,一個View從無到有展現在咱們面前要至少通過兩次onMeasure,一次onLayout,一次onDraw方法(爲何是這樣的呢,感興趣的小夥伴能夠去ViewRootImpl中找找答案)。因此這裏須要作個緩存,而不至於每次都從新建立新的視圖。整個過程大體如圖:
這裏提一下,在RV展現成功後,Scrap這層的緩存就爲空了,在從Scrap中取視圖的同時就被移出了緩存。在onLayout這裏最終會調用到dispatchLayoutStep3方法,沒錯,除了1和2還有3,在3中,若是Scrap還有緩存,那麼緩存會被清空,清空的緩存會被添加到mCachedViews或者RecyclerPool中。RV是能夠經過滾動來展現大量數據的控件,那麼由當前屏幕滾動而出的View去哪了?滾動而入的View哪來的?一樣的,咱們去源碼中找找答案。
scrollHorizontallyBy,scrollVerticallyBy
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
final int start = layoutState.mAvailable;
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
// TODO ugly bug fix. should not happen
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
}
複製代碼
這這段代碼中判斷了當前是不是滾動觸發的fill方法,若是是調用recycleByLayoutState(recycler, layoutState)方法。這個方法幾經週轉會調用到removeAndRecycleViewAt方法:
public void removeAndRecycleViewAt(int index, Recycler recycler) {
final View view = getChildAt(index);
removeViewAt(index);
recycler.recycleView(view);
}
複製代碼
這裏注意先把視圖remove掉了,而不是detach掉。而後調用Recycler中的recycleView方法,這個方法最後會調用recycleViewHolderInternal方法,方法以下:
void recycleViewHolderInternal(ViewHolder holder) {
if (forceRecycle || holder.isRecyclable()) {
if (省略) {
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize--;
}
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
}
複製代碼
刪除不相關代碼後邏輯很清晰。前面咱們說過mCachedViews是有容量限制的,默認爲2。那麼若是符合放到mCachedViews中的條件,首先會判斷mCachedViews是否已經滿了,若是滿了會經過recycleCachedViewAt(0)方法把最老得那個緩存放進RecyclerPool,而後在把新的視圖放進mCachedViews中。若是這個視圖不符合條件會直接被放進RecyclerPool中。咱們注意到,在緩存進mCachedViews以前,咱們的視圖只是被remove掉了,綁定的數據等信息都還在,這意味着從mCachedViews取出的視圖若是符合須要的目標視圖是能夠直接展現的,而不須要從新綁定。而放進RecyclerPool最終是要調用putRecycledView方法的。
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;
}
scrap.resetInternal();
scrapHeap.add(scrap);
}
複製代碼
這個方法中一樣對容量作了判斷,跟mCachedViews不同,若是容量滿了,就再也不繼續緩存了。在緩存以前先調用了scrap.resetInternal()方法,這個方法顧名思義是個重置的方法,緩存以前把視圖的信息都清除掉了,這也是爲何這裏緩存滿了以後就再也不繼續緩存了,而不是把老的緩存替換掉,由於它們重置後都同樣了(這裏指具備同種itemType的是同樣的)。這就是滑動緩存的全過程,至此咱們知道了滾動出去的視圖去哪了,那麼滾動進來的視圖哪來的呢?
這塊我就簡單說一下結論,感興趣的同窗能夠自行查看源碼。爲何咱們在有數據刷新的時候推薦你們使用notifyItemChanged等方法而不使用notifyDataSetChanged方法呢?
咱們從緩存的幾個類型以及佈局、滾動、刷新幾個方面全方位的剖析了RV的緩存機制。
這麼多層緩存是怎麼工做的?何時用什麼緩存?各個緩存之間有沒有什麼PY交易?若是讓你本身寫一個LayoutManager你能處理好緩存問題麼?
我相信你已經有了本身的答案。後續會推出一篇關於自定義LayoutManager的文章,敬請期待。