Android源碼分析之RecyclerView源碼分析(二)——緩存機(jī)制

系列文章

  1. Android源碼分析之ListView源碼
  2. Android源碼分析之RecyclerView源碼分析(一)——繪制流程
  3. Android源碼分析之RecyclerView源碼分析(二)——緩存機(jī)制

前言

此前已經(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的緩存外麦备,還存在緩存mScrapListmScrapListLinearLayoutManager持有昭娩,其聲明如下:

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方法升薯。

參考RecyclerView源碼解析(二)——緩存機(jī)制

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方法,此方法的注釋寫的很清楚击困,就是試圖從Recyclerscrap涎劈,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;
}

此方法大概可以分為以下三步:

  1. 先從mAttachedScrap中獲取ViewHolder顽照;
  2. HiddenViews中獲取View,再根據(jù)View獲取ViewHolder闽寡;
  3. mCachedViews中獲取ViewHolder代兵。
    其流程上可以總結(jié)如下:
    RecyclerViewScrapTest1.png

摘自RecyclerView源碼解析(二)——緩存機(jī)制

第一級(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é)如下:


image

摘自RecyclerView源碼解析(二)——緩存機(jī)制

第二級(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就是我們的ViewTypevalue存放的是RecycledViewPool.ScrapData類型數(shù)據(jù)惫企,ScrapData中有ArrayList<ViewHolder>類型數(shù)據(jù)撕瞧,默認(rèn)最大容量為5

此外狞尔,getRecycledView()方法先通過mScrapViewType獲取scrapData后丛版,去除并返回mScrapHeap中的最后一個(gè)數(shù)據(jù)。

第四級(jí)緩存:創(chuàng)建ViewHolder(onCreateViewHolder方法)

if (holder == null) {
    ...
    
    holder = mAdapter.createViewHolder(RecyclerView.this, type);
    
    ...
}

創(chuàng)建ViewHolder主要調(diào)用了AdaptercreateViewHolder方法:

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é):

  1. RecyclerView有四級(jí)緩存:mAttachedScrap弛说,mCacheViews,ViewCacheExtension翰意,RecycledViewPool木人,創(chuàng)建ViewHolder;
  2. mAttachedScrap,mCacheViews第一次嘗試的時(shí)候只是對(duì)View的復(fù)用冀偶,不區(qū)分type醒第,只通過位置來尋找,但在第二次嘗試的時(shí)候是區(qū)分了type进鸠,是對(duì)于ViewHolder的復(fù)用稠曼,ViewCacheExtension,RecycledViewPool是對(duì)于ViewHolder的復(fù)用,而且區(qū)分type客年;
  3. 如果緩存ViewHolder時(shí)發(fā)現(xiàn)超過了mCachedView的限制蒲列,會(huì)將最老的ViewHolder(也就是mCachedView緩存隊(duì)列的第一個(gè)ViewHolder)移到RecycledViewPool中

放上騰訊Bugly的相關(guān)博客對(duì)RecyclerView的緩存的流程圖吧(自己手動(dòng)畫了下搀罢,加強(qiáng)理解):

RecyclerViewCacheProcess.

RecyclerView緩存機(jī)制深度分析

mAttachedScrap作用

介紹mAttachedScrap的作用之前蝗岖,我們先需要了解Detach和Remove兩個(gè)概念的區(qū)別:

  1. Detach用于將ViewGroup中的子View從父View的子View數(shù)組中移除子View的ParentView屬性置為null榔至;這個(gè)操作是一個(gè)輕量級(jí)臨時(shí)的Remove抵赢,被Detach的子View還是和View樹有千絲萬縷的聯(lián)系,在后面會(huì)被重新Attach到父View中唧取。
  2. Remove用于真正的移除子View铅鲤,不僅從父View的子View數(shù)組中移除,其他和View樹各項(xiàng)聯(lián)系也會(huì)被徹底斬?cái)喾愕埽热缃裹c(diǎn)被清除邢享。

參考RecyclerView機(jī)制分析: Recycler

介紹完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方法。下面具體分析下此過程:

  1. 在第一次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当窗。

  1. 在第二次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加入mCacheViewsRecyclerViewPool财异;否則detach該View疫鹊,并調(diào)用了recycler.scrapView方法捞奕,將ViewHolder加入mAttachedScrapmChangedScrap中墩邀,此處我們先來看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í)鹿寻,類似于ListViewRecycleBinscrapView。再來看一下關(guān)于mCacheViews的兩處緩存代碼:

  1. 第一處:
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痢法。

  1. 第二處:
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加入mCacheViewsRecyclerViewPool中淹禾。下面便來具體看看代碼:

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):

  1. 如果mCachedViews現(xiàn)有大小>=默認(rèn)大小铃岔,則會(huì)移除mCachedViews中的第一個(gè)ViewHolder,并調(diào)用recycleCachedViewAt方法使被移除的ViewHolder加入到RecyclerViewPool中峭火,最后將需要加入緩存的ViweHolder加入到CacheViews中毁习;
  2. 如果加入到mCacheViews中失敗了,則加入到RecyclerViewPool中卖丸。

至于為什么mCacheViewsRecyclerView滑動(dòng)時(shí)起作用纺且,后文闡述RecyclerView滑動(dòng)過程中的緩存機(jī)制中再描述。

RecyclerViewPool作用

前面已經(jīng)介紹過RecyclerViewPool的結(jié)構(gòu)了坯苹,是通過RecyclerViewPoolgetRecycledView方法來獲取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方法萨惑,下面來看下RecyclerViewnTouchEvent方法:

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)注onTouchEventACTION_MOVE事件捐康,可以看到,其中對(duì)canScrollHorizontallycanScrollVertically進(jìn)行了判斷庸蔼,并最終將偏移量傳給了scrollByInternal方法解总,而在scrollByInternal方法中,調(diào)用了LayoutManagerscrollHorizontallyByscrollVerticallyBy方法姐仅,這兩個(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é)吧:

RecyclerViewCache1.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末行冰,一起剝皮案震驚了整個(gè)濱河市溺蕉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌悼做,老刑警劉巖疯特,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異肛走,居然都是意外死亡漓雅,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門朽色,熙熙樓的掌柜王于貴愁眉苦臉地迎上來邻吞,“玉大人,你說我怎么就攤上這事葫男”Ю洌” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵梢褐,是天一觀的道長(zhǎng)旺遮。 經(jīng)常有香客問我,道長(zhǎng)盈咳,這世上最難降的妖魔是什么耿眉? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮鱼响,結(jié)果婚禮上鸣剪,老公的妹妹穿的比我還像新娘。我一直安慰自己热押,他們只是感情好西傀,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著桶癣,像睡著了一般拥褂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上牙寞,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天饺鹃,我揣著相機(jī)與錄音莫秆,去河邊找鬼。 笑死悔详,一個(gè)胖子當(dāng)著我的面吹牛镊屎,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播茄螃,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼缝驳,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了归苍?” 一聲冷哼從身側(cè)響起用狱,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎拼弃,沒想到半個(gè)月后夏伊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡吻氧,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年溺忧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片盯孙。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鲁森,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出镀梭,到底是詐尸還是另有隱情刀森,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布报账,位于F島的核電站研底,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏透罢。R本人自食惡果不足惜榜晦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望羽圃。 院中可真熱鬧乾胶,春花似錦、人聲如沸朽寞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽脑融。三九已至喻频,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間肘迎,已是汗流浹背甥温。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工锻煌, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人姻蚓。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓宋梧,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親狰挡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子捂龄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容