深刻理解Android RecyclerView的緩存機制

咱們知道,RecyclerView在大量數據時依然能夠絲滑般順暢的滑動,那它到底是怎麼實現的呢,而RecyclerView之因此好用得益於它優秀的緩存機制。緩存

咱們知道,RecyclerView自己是一個ViewGroup,所以在滑動時就避免不了添加或移除子View(子View經過RecyclerView#Adapter中的onCreateViewHolder建立),若是每次使用子View都要去從新建立,確定會影響滑動的流暢性,因此RecyclerView經過Recycler來緩存的是ViewHolder(內部包含子View),這樣在滑動時能夠複用子View,某些條件下還能夠複用子View綁定的數據。因此本質上來講,RecyclerView之因此可以實現順暢的滑動效果,是由於緩存機制,由於緩存減小了重複繪製View和綁定數據的時間,從而提升了滑動時的性能。數據結構

1、緩存

1.一、四級緩存

Recycler緩存ViewHolder對象有4個等級,優先級從高到底依次爲:ide

  • mAttachedScrap:緩存屏幕中可見範圍的ViewHolder;
  • mCachedViews:緩存滑動時即將與RecyclerView分離的ViewHolder,默認最大2個;
  • ViewCacheExtension:自定義實現的緩存;
  • RecycledViewPool :ViewHolder緩存池,能夠支持不一樣的ViewType;

1.1.1 mAttachedScrap

mAttachedScrap存儲的是當前屏幕中的ViewHolder,mAttachedScrap的對應數據結構是ArrayList,在調用LayoutManager#onLayoutChildren方法時對views進行佈局,此時會將RecyclerView上的Views所有暫存到該集合中,該緩存中的ViewHolder的特性是,若是和RV上的position或者itemId匹配上了那麼能夠直接拿來使用的,無需調用onBindViewHolder方法。佈局

1.1.2 mChangedScrap

mChangedScrap和mAttachedScrap屬於同一級別的緩存,不過mChangedScrap的調用場景是notifyItemChanged和notifyItemRangeChanged,只有發生變化的ViewHolder纔會放入到mChangedScrap中。mChangedScrap緩存中的ViewHolder是須要調用onBindViewHolder方法從新綁定數據的。性能

1.1.3 mCachedViews

mCachedViews緩存滑動時即將與RecyclerView分離的ViewHolder,按子View的position或id緩存,默認最多存放2個。mCachedViews對應的數據結構是ArrayList,可是該緩存對集合的大小是有限制的。this

該緩存中ViewHolder的特性和mAttachedScrap中的特性是同樣的,只要position或者itemId對應就無需從新綁定數據。開發者能夠調用setItemViewCacheSize(size)方法來改變緩存的大小,該層級緩存觸發的一個常見的場景是滑動RecyclerView。固然調用notify()也會觸發該緩存。spa

1.1.4 ViewCacheExtension

ViewCacheExtension是須要開發者本身實現的緩存,基本上頁面上的全部數據均可以經過它進行實現。code

1.1.5 RecyclerViewPool

ViewHolder緩存池,本質上是一個SparseArray,其中key是ViewType(int類型),value存放的是 ArrayList< ViewHolder>,默認每一個ArrayList中最多存放5個ViewHolder。對象

1.2 四級緩存對比

緩存級別 涉及對象 說明 是否從新建立視圖View 是否從新綁定數據
一級緩存 mAttachedScrap mChangedScrap 緩存屏幕中可見範圍的ViewHolder false false
二級緩存 mCachedViews 緩存滑動時即將與RecyclerView分離的ViewHolder,按子View的position或id緩存 false false
三級緩存 mViewCacheExtension 開發者自行實現的緩存
四級緩存 mRecyclerPool ViewHolder緩存池,本質上是一個SparseArray,其中key是ViewType(int類型),value存放的是 ArrayList< ViewHolder>,默認每一個ArrayList中最多存放5個ViewHolder false true

1.3 調用過程

一般,RecyclerView滑動時會觸發onTouchEvent#onMove,回收及複用ViewHolder在這裏就會開始。咱們知道設置RecyclerView時須要設置LayoutManager,LayoutManager負責RecyclerView的佈局,包含對ItemView的獲取與複用。以LinearLayoutManager爲例,當RecyclerView從新佈局時會依次執行下面幾個方法:圖片

  • onLayoutChildren():對RecyclerView進行佈局的入口方法
  • fill(): 負責對剩餘空間不斷地填充,調用的方法是layoutChunk()
  • layoutChunk():負責填充View,該View最終是經過在緩存類Recycler中找到合適的View的

上述的整個調用鏈:onLayoutChildren()->fill()->layoutChunk()->next()->getViewForPosition(),getViewForPosition()便是是從RecyclerView的回收機制實現類Recycler中獲取合適的View。

2、複用流程

RecyclerView對ViewHolder的複用是從LayoutState的next()方法開始的。LayoutManager在佈局itemView時,須要獲取一個ViewHolder對象,以下所示。

View next(RecyclerView.Recycler recycler) {
            if (mScrapList != null) {
                return nextViewFromScrapList();
            }
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        }

next方法調用RecyclerView的getViewForPosition方法來獲取一個View,而getViewForPosition方法最終會調用到RecyclerView的tryGetViewHolderForPositionByDeadline方法,而RecyclerView真正複用的核心就在這裏。

@Nullable
    ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
        ViewHolder holder = null;
        // 0) 若是它是改變的廢棄的ViewHolder,在scrap的mChangedScrap找
        if (mState.isPreLayout()) {
            holder = getChangedScrapViewForPosition(position);
            fromScrapOrHiddenOrCache = holder != null;
        }
        // 1)根據position分別在scrap的mAttachedScrap、mChildHelper、mCachedViews中查找
        if (holder == null) {
            holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        }

        if (holder == null) {
            final int type = mAdapter.getItemViewType(offsetPosition);
            // 2)根據id在scrap的mAttachedScrap、mCachedViews中查找
            if (mAdapter.hasStableIds()) {
                holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
            }
            if (holder == null && mViewCacheExtension != null) {
                //3)在ViewCacheExtension中查找,通常不用到,因此沒有緩存
                final View view = mViewCacheExtension
                        .getViewForPositionAndType(this, position, type);
                if (view != null) {
                    holder = getChildViewHolder(view);
                }
            }
            //4)在RecycledViewPool中查找
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                holder.resetInternal();
                if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
                }
            }
        }
        //5)到最後若是尚未找到複用的ViewHolder,則新建一個
        holder = mAdapter.createViewHolder(RecyclerView.this, type);
    }

能夠看到,tryGetViewHolderForPositionByDeadline()方法分別去scrap、CacheView、ViewCacheExtension、RecycledViewPool中獲取ViewHolder,若是沒有則建立一個新的ViewHolder。

2.1 getChangedScrapViewForPosition

通常狀況下,當咱們調用adapter的notifyItemChanged()方法,數據發生變化時,item緩存在mChangedScrap中,後續拿到的ViewHolder須要從新綁定數據。此時查找ViewHolder就會經過position和id分別在scrap的mChangedScrap中查找。

ViewHolder getChangedScrapViewForPosition(int position) {
        //經過position
        for (int i = 0; i < changedScrapSize; i++) {
            final ViewHolder holder = mChangedScrap.get(i);
            return holder;
        }
        // 經過id
        if (mAdapter.hasStableIds()) {
            final long id = mAdapter.getItemId(offsetPosition);
            for (int i = 0; i < changedScrapSize; i++) {
                final ViewHolder holder = mChangedScrap.get(i);
                return holder;
            }
        }
        return null;
    }

2.2 getScrapOrHiddenOrCachedHolderForPosition

若是沒有找到視圖,根據position分別在scrap的mAttachedScrap、mChildHelper、mCachedViews中查找,涉及的方法以下。

ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
        final int scrapCount = mAttachedScrap.size();

        // 首先從mAttachedScrap中查找,精準匹配有效的ViewHolder
        for (int i = 0; i < scrapCount; i++) {
            final ViewHolder holder = mAttachedScrap.get(i);
            return holder;
        }
        //接着在mChildHelper中mHiddenViews查找隱藏的ViewHolder
        if (!dryRun) {
            View view = mChildHelper.findHiddenNonRemovedView(position);
            if (view != null) {
                final ViewHolder vh = getChildViewHolderInt(view);
                scrapView(view);
                return vh;
            }
        }
        //最後從咱們的一級緩存中mCachedViews查找。
        final int cacheSize = mCachedViews.size();
        for (int i = 0; i < cacheSize; i++) {
            final ViewHolder holder = mCachedViews.get(i);
            return holder;
        }
    }

能夠看到,getScrapOrHiddenOrCachedHolderForPosition查找ViewHolder的順序以下:

  • 首先,從mAttachedScrap中查找,精準匹配有效的ViewHolder;
  • 接着,在mChildHelper中mHiddenViews查找隱藏的ViewHolder;
  • 最後,從一級緩存中mCachedViews查找。

2.3 getScrapOrCachedViewForId

若是在getScrapOrHiddenOrCachedHolderForPosition沒有找到視圖,澤經過id在scrap的mAttachedScrap、mCachedViews中查找,代碼以下。

ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {
        //在Scrap的mAttachedScrap中查找
        final int count = mAttachedScrap.size();
        for (int i = count - 1; i >= 0; i--) {
            final ViewHolder holder = mAttachedScrap.get(i);
            return holder;
        }

        //在一級緩存mCachedViews中查找
        final int cacheSize = mCachedViews.size();
        for (int i = cacheSize - 1; i >= 0; i--) {
            final ViewHolder holder = mCachedViews.get(i);
            return holder;
        }
    }

getScrapOrCachedViewForId()方法查找的順序以下:

  • 首先, 從mAttachedScrap中查找,精準匹配有效的ViewHolder;
  • 接着, 從一級緩存中mCachedViews查找;

2.4 mViewCacheExtension

mViewCacheExtension是由開發者定義的一層緩存策略,Recycler並無將任何view緩存到這裏。

if (holder == null && mViewCacheExtension != null) {
        final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
        if (view != null) {
            holder = getChildViewHolder(view);
        }
    }

這裏沒有自定義緩存策略,那麼就找不到對應的view。

2.5 RecycledViewPool

在ViewHolder的四級緩存中,咱們有提到過RecycledViewPool,它是經過itemType把ViewHolder的List緩存到SparseArray中的,在getRecycledViewPool().getRecycledView(type)根據itemType從SparseArray獲取ScrapData ,而後再從裏面獲取ArrayList<ViewHolder>,從而獲取到ViewHolder。

@Nullable
    public ViewHolder getRecycledView(int viewType) {
        final ScrapData scrapData = mScrap.get(viewType);//根據viewType獲取對應的ScrapData 
        if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
            final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
            for (int i = scrapHeap.size() - 1; i >= 0; i--) {
                if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
                    return scrapHeap.remove(i);
                }
            }
        }
        return null;
    }

2.6 建立新的ViewHolder

若是尚未獲取到ViewHolder,則經過mAdapter.createViewHolder()建立一個新的ViewHolder返回。

// 若是尚未找到複用的ViewHolder,則新建一個
  holder = mAdapter.createViewHolder(RecyclerView.this, type);

下面是尋找ViewHolder的一個完整的流程圖:
在這裏插入圖片描述

3、回收流程

RecyclerView回收的入口有不少, 可是無論怎麼樣操做,RecyclerView 的回收或者複用必然涉及到add View 和 remove View 操做, 因此咱們從onLayout的流程入手分析回收和複用的機制。

首先,在LinearLayoutManager中,咱們來到itemView佈局入口的方法onLayoutChildren(),以下所示。

@Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
            if (state.getItemCount() == 0) {
                removeAndRecycleAllViews(recycler);//移除全部子View
                return;
            }
        }
        ensureLayoutState();
        mLayoutState.mRecycle = false;//禁止回收
        //顛倒繪製佈局
        resolveShouldLayoutReverse();
        onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);

        //暫時分離已經附加的view,即將全部child detach並經過Scrap回收
        detachAndScrapAttachedViews(recycler);
    }

在onLayoutChildren()佈局的時候,先根據實際狀況是否須要removeAndRecycleAllViews()移除全部的子View,哪些ViewHolder不可用;而後經過detachAndScrapAttachedViews()暫時分離已經附加的ItemView,並緩存到List中。

detachAndScrapAttachedViews()的做用就是把當前屏幕全部的item與屏幕分離,將他們從RecyclerView的佈局中拿下來,保存到list中,在從新佈局時,再將ViewHolder從新一個個放到新的位置上去。

將屏幕上的ViewHolder從RecyclerView的佈局中拿下來後,存放在Scrap中,Scrap包括mAttachedScrap和mChangedScrap,它們是一個list,用來保存從RecyclerView佈局中拿下來ViewHolder列表,detachAndScrapAttachedViews()只會在onLayoutChildren()中調用,只有在佈局的時候,纔會把ViewHolder detach掉,而後再add進來從新佈局,可是你們須要注意,Scrap只是保存從RecyclerView佈局中當前屏幕顯示的item的ViewHolder,不參與回收複用,單純是爲了現從RecyclerView中拿下來再從新佈局上去。對於沒有保存到的item,會放到mCachedViews或者RecycledViewPool緩存中參與回收複用。

public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
        final int childCount = getChildCount();
        for (int i = childCount - 1; i >= 0; i--) {
            final View v = getChildAt(i);
            scrapOrRecycleView(recycler, i, v);
        }
    }

上面代碼的做用是,遍歷全部view,分離全部已經添加到RecyclerView的itemView。

private void scrapOrRecycleView(Recycler recycler, int index, View view) {
        final ViewHolder viewHolder = getChildViewHolderInt(view);
        if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                && !mRecyclerView.mAdapter.hasStableIds()) {
            removeViewAt(index);//移除VIew
            recycler.recycleViewHolderInternal(viewHolder);//緩存到CacheView或者RecycledViewPool中
        } else {
            detachViewAt(index);//分離View
            recycler.scrapView(view);//scrap緩存
            mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
        }
    }

而後,咱們看detachViewAt()方法分離視圖,再經過scrapView()緩存到scrap中。

void scrapView(View view) {
        final ViewHolder holder = getChildViewHolderInt(view);
        if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
            holder.setScrapContainer(this, false);
            mAttachedScrap.add(holder);//保存到mAttachedScrap中
        } else {
            if (mChangedScrap == null) {
                mChangedScrap = new ArrayList<ViewHolder>();
            }
            holder.setScrapContainer(this, true);
            mChangedScrap.add(holder);//保存到mChangedScrap中
        }
    }

而後,咱們回到scrapOrRecycleView()方法中,進入if()分支。若是viewHolder是無效、未被移除、未被標記的則放到recycleViewHolderInternal()緩存起來,同時removeViewAt()移除了viewHolder。

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

                int cachedViewSize = mCachedViews.size();
                if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {//若是超出容量限制,把第一個移除
                    recycleCachedViewAt(0);
                    cachedViewSize--;
                }
                     ·····
                mCachedViews.add(targetCacheIndex, holder);//mCachedViews回收
                cached = true;
            }
            if (!cached) {
                addViewHolderToRecycledViewPool(holder, true);//放到RecycledViewPool回收
                recycled = true;
            }
        }
    }

若是符合條件,會優先緩存到mCachedViews中時,若是超出了mCachedViews的最大限制,經過recycleCachedViewAt()將CacheView緩存的第一個數據添加到終極回收池RecycledViewPool後再移除掉,最後纔會add()新的ViewHolder添加到mCachedViews中。

剩下不符合條件的則經過addViewHolderToRecycledViewPool()緩存到RecycledViewPool中。

void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
        clearNestedRecyclerViewIfNotNested(holder);
        View itemView = holder.itemView;
        ······
        holder.mOwnerRecyclerView = null;
        getRecycledViewPool().putRecycledView(holder);//將holder添加到RecycledViewPool中
    }

最後,就是在填充佈局調用fill()方法的時候,它會回收移出屏幕的view到mCachedViews或者RecycledViewPool中。

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
              recycleByLayoutState(recycler, layoutState);//回收移出屏幕的view
        }
    }

而recycleByLayoutState()方法就是用來回收移出屏幕的view,完整的流程以下圖。
在這裏插入圖片描述

相關文章
相關標籤/搜索