真正帶你搞懂RecyclerView的緩存機制

whatsapp-interface-1660652_1280

本文已受權微信公衆號【玉剛說】獨家原創發佈緩存

前言

RecyclerView大概是Android開發者接觸最多的一個控件了,官方對其作了很好的封裝抽象,使得它既靈活又好用,可是你真的瞭解它麼?在它簡單的使用方式之下着實是不簡單,首先咱們看一下官方對它的介紹:bash

A flexible view for providing a limited window into a large data set.微信

很簡單,就一句話「爲大量數據集提供一個有限展現窗口的靈活視圖」怎麼展現大量的數據是個技術活,這些數據伴隨着滾動逐漸展現在咱們眼前,可是展現過的滾走的視圖呢?它們是否還存在?我想你們確定知道它們是要被回收的,否者來個幾百上千條數據那還不OOM了。那麼咱們今天就圍繞RecyclerView的視圖回收機制來談一談,到底RecyclerView的回收機制是怎樣的。app

緩存層級

咱們先了解下Recycler的緩存結構是怎樣的,先了解兩個專業詞彙:ide

  • Scrap (view):在佈局期間進入臨時分離狀態的子視圖。廢棄視圖能夠重複使用,而不會與父級RecyclerView徹底分離,若是不須要從新綁定,則不進行修改,若是視圖被視爲髒,則由適配器修改。(這裏的髒怎麼理解呢?就是指那些在展現以前必須從新綁定的視圖,好比一個視圖原來展現的是「張三」,以後須要展現「李四」了,那麼這個視圖就是髒視圖,須要從新綁定數據後再展現的。)
  • Recycle (view):先前用於顯示適配器特定位置的數據的視圖能夠放置在高速緩存中以供稍後重用再次顯示相同類型的數據。這能夠經過跳過初始佈局或構造來顯着提升性能。

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的緩存大權,從上面的代碼片斷咱們能夠看到這個類聲明瞭五個成員變量。咱們一個個的來講一下:性能

  1. mAttachedScrap:咱們能夠看到這個變量是個存放ViewHolder對象的ArrayList,這一級緩存是沒有容量限制的,只要符合條件的我來者不拒,全收了。前面講兩個專業術語的時候提到了Scrap,這個就屬於Scrap中的一種,這裏的數據是不作修改的,不會從新走Adapter的綁定方法。
  2. mChangedScrap:這個變量和上邊的mAttachedScrap是同樣的,惟一不一樣的從名字也能夠看出來,它存放的是發生了變化的ViewHolder,若是使用到了這裏的緩存的ViewHolder是要從新走Adapter的綁定方法的。
  3. mCachedViews:這個變量一樣是一個存放ViewHolder對象的ArrayList,可是這個不一樣於上面的兩個裏面存放的是dettach掉的視圖,它裏面存放的是已經remove掉的視圖,已經和RV分離的關係的視圖,可是它裏面的ViewHolder依然保存着以前的信息,好比position、和綁定的數據等等。這一級緩存是有容量限制的,默認是2(不一樣版本API可能會有差別,本文基於API26.1.0)。
  4. mRecyclerPool:這個變量呢自己是一個類,跟上面三個都不同。這裏面保存的ViewHolder不只僅是removed掉的視圖,並且是恢復了出廠設置的視圖,任何綁定過的痕跡都沒有了,想用這裏緩存的ViewHolder那是鐵定要從新走Adapter的綁定方法了。並且咱們知道RV支持多佈局,因此這裏的緩存是按照itemType來分開存儲的,咱們來大體的看一下它的結構:
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<>();
        …… 省略後面代碼 ……
    }
複製代碼
  • 首先咱們看到一個常量‘DEFAULT_MAX_SCRAP’,這個就是緩存池定義的一個默認的緩存數,固然這個緩存數咱們是能夠本身設置的。並且這個緩存數量不是指整個緩存池只能緩存這麼多,而是每一個不一樣itemType的ViewHolder的緩存數量。
  • 接着往下看,咱們看到一個靜態內部類ScrapData,這裏咱們只看跟緩存相關的兩個變量,先說mMaxScrap,前面的常量賦值給了它,這也就印證了咱們前面說的這個緩存數量是對應每一種類型的ViewHolder的。再來看這個mScrapHeap變量,熟悉的一幕又來了,一樣是一個緩存ViewHolder對象的ArrayList,它的容量默認是5.
  • 最後咱們看到mScrap這個變量,它是一個存儲咱們上面提到的ScrapData類的對象的SparseArray,這樣咱們這個RecyclerPool就把不一樣itemType的ViewHolder按類型分類緩存了起來。
  1. mViewCacheExtension:這一級緩存是留給開發者自由發揮的,官方並無默認實現,它自己是null。
    waste-separation-502952_1280
    垃圾桶講完了,哦不,是緩存層級講完了。這裏提一句,其實還有一層沒有提到,由於它不在Recycler這個類中,它在ChildHelper類中,其中有個mHiddenViews,是個緩存被隱藏的ViewHolder的ArrayList。到這裏我想你們對這幾層緩存內心已經有個數了,可是還遠遠不夠,這麼多層緩存是怎麼工做的?何時用什麼緩存?各個緩存之間有沒有什麼PY交易?若是讓你本身寫一個LayoutManager你能處理好緩存問題麼?就比如垃圾分類後,咱們知道每種垃圾桶的定義和功能,可是面對大媽的靈魂拷問我依然分不清本身是什麼垃圾,我太難了~相比之下,RV的幾個垃圾桶簡單多了,下面咱們一塊兒來看看,這些個緩存都咋用。

各緩存的使用

上面咱們介紹了RV的各緩存層級,可是它們是怎麼工做的呢?爲何要設計這些層級呢?別急,咱們去源碼中找找答案。一葉落而知天下秋,咱們就從官方自帶的最簡單的佈局管理者LinearLayoutManager入手,來看看到底如何使用這幾級緩存寫出一個合格的佈局管理者。flex

RV從無到有的加載過程

首先咱們看一下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都是在這個方法中完成的,固然關於綁定和其它的無關代碼這裏就不貼了。咱們一步步的看一下:

  1. 第一步先從getChangedScrapViewForPosition(position)方法中找須要的視圖,可是有個條件mState.isPreLayout()要爲true,這個通常在咱們調用adapter的notifyItemChanged等方法時爲true,其實也很好理解,數據發生了變化,viewholder被detach掉後緩存在mChangedScrap之中,在這裏拿到的viewHolder後續須要從新綁定。
  2. 第二步,若是沒有找到視圖則從getScrapOrHiddenOrCachedHolderForPosition這個方法中繼續找。這個方法的代碼就不貼了,簡單說下這裏的查找順序。
  • 首先從mAttachedScrap中查找
  • 再次從前面略過的ChildHelper類中的mHiddenViews中查找
  • 最後是從mCachedViews中查找的
  1. 第三步, mViewCacheExtension中查找,咱們說過這個對象默認是null的,是由咱們開發者自定義緩存策略的一層,因此若是你沒有定義過,這裏是找不到View的。
  2. 第四步,從RecyclerPool中查找,前面咱們介紹過RecyclerPool,先經過itemType從SparseArray類型的mscrap中拿到ScrapData,不爲空繼續拿到scrapHeap這個ArrayList,而後取到視圖,這裏拿到的視圖須要從新綁定。
  3. 第五步,若是前面幾步都沒有拿到視圖,那麼調用了mAdapter.createViewHolder(RecyclerView.this, type)方法,這個方法內部調用了一個抽象方法onCreateViewHolder,是否是很熟悉,沒錯,就是咱們本身寫一個Adapter要實現的方法之一。

到此爲止咱們獲取一個視圖的流程就講完了,獲取到視圖以後就是怎麼擺放視圖並添加到RV之中,而後最終展現到咱們面前。細心的小夥伴可能發現這個流程貌似有點問題啊?第一次進入onLayoutChildren時尚未任何子view,在fill方法前等於沒有緩存子view,全部的子View都是第五步onCreateViewHolder建立而來的。實際上這裏的設計是有道理的,除了一些特殊狀況onLayoutChildren方法會被屢次調用外,一個View從無到有展現在咱們面前要至少通過兩次onMeasure,一次onLayout,一次onDraw方法(爲何是這樣的呢,感興趣的小夥伴能夠去ViewRootImpl中找找答案)。因此這裏須要作個緩存,而不至於每次都從新建立新的視圖。整個過程大體如圖:

1566893166006
這裏提一下,在RV展現成功後,Scrap這層的緩存就爲空了,在從Scrap中取視圖的同時就被移出了緩存。在onLayout這裏最終會調用到dispatchLayoutStep3方法,沒錯,除了1和2還有3,在3中,若是Scrap還有緩存,那麼緩存會被清空,清空的緩存會被添加到mCachedViews或者RecyclerPool中。

RV滑動時的緩存過程

RV是能夠經過滾動來展現大量數據的控件,那麼由當前屏幕滾動而出的View去哪了?滾動而入的View哪來的?一樣的,咱們去源碼中找找答案。

scrollHorizontallyBy,scrollVerticallyBy

  • 一個LayoutManager若是能夠滑動,那麼上面的兩個方法要返回非0值,分別表明能夠橫向滾動和縱向滾動。最終兩個方法都會調用scrollBy方法,而後scrollby方法調用了fill方法,這個fill咱們已經見過了,如今再看一下。
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的是同樣的)。這就是滑動緩存的全過程,至此咱們知道了滾動出去的視圖去哪了,那麼滾動進來的視圖哪來的呢?

  • 和從無到有的過程同樣,最後滾動也調用了fill方法,那最後必然是要走到前面分析的獲取視圖的5個流程。前面說過在佈局完成以後,Scrap層的緩存就是空的了,那就只能從mCachedViews或者RecyclerPool中取了,都取不到最後就會走onCreateViewHolder建立視圖。到這裏滑動時的緩存以及取緩存就講完了。

數據更新時的緩存過程

這塊我就簡單說一下結論,感興趣的同窗能夠自行查看源碼。爲何咱們在有數據刷新的時候推薦你們使用notifyItemChanged等方法而不使用notifyDataSetChanged方法呢?

  • 在調用notifyDataSetChanged方法後,全部的子view會被標記,這個標記致使它們最後都被緩存到RecyclerPool中,而後從新綁定數據。而且因爲RecyclerPool有容量限制,若是不夠最後就要從新建立新的視圖了。
  • 可是使用notifyItemChanged等方法會將視圖緩存到mChangedScrap和mAttachedScrap中,這兩個緩存是沒有容量限制的,因此基本不會從新建立新的視圖,只是mChangedScrap中的視圖須要從新綁定一下。

總結

咱們從緩存的幾個類型以及佈局、滾動、刷新幾個方面全方位的剖析了RV的緩存機制。

這麼多層緩存是怎麼工做的?何時用什麼緩存?各個緩存之間有沒有什麼PY交易?若是讓你本身寫一個LayoutManager你能處理好緩存問題麼?

我相信你已經有了本身的答案。後續會推出一篇關於自定義LayoutManager的文章,敬請期待。

相關文章
相關標籤/搜索