理解RecyclerView(八)—RecyclerView的回收複用緩存機制詳解

前言: 生命總是要有信仰,有夢想才能一直前行,哪怕走的再慢,也是在前進。

一、概述

  RecyclerView作爲官方指定的高效、高拓展性的列表控件,做了很好的封裝,靈活好用,深受我們喜歡。官方對它的介紹:爲大量數據提供有限展示窗口的靈活視圖。要想在有限的手機內存中展示大量的數據,並且保證不會OOM,它是怎麼做到的呢?

我們在adapter的onCreateViewHolder()onBindViewHolder()分別打印了log,其中,onCreateViewHolder()會在創建一個新view的時候調用,onBindViewHolder()會在已存在view,綁定數據的時候調用。所以,如果是新創建的view,會調用onCreateViewHolder()來創建view,調用onBindViewHolder()來綁定數據;如果是複用的view,則不會調用onCreateViewHolder()創建方法,只會調用onBindViewHolder()綁定數據。

	private int sum = 0;
	
    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        Log.e("LinearVerticalAdapter", "onCreateViewHolder == " + sum);
        sum += 1;
       	······
        return null;
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        Log.e("LinearVerticalAdapter", "onBindViewHolder");
        ······
    }

第一次進入的時候打印數據如下:
在這裏插入圖片描述
這裏發現打印了13條數據(我手機屏幕滿屏是13條數據),都走了onCreateViewHolder()onBindViewHolder()方法;當我們來回滾動的時候,發現只走了onBindViewHolder()綁定數據的方法,沒有走onCreateViewHolder()創建ViewHolder的方法:
在這裏插入圖片描述
因爲RecyclerView能夠自動回收複用,這必須有強大的緩存機制支撐,RecyclerView的緩存機制是RecyclerView的核心部分。這裏圍繞RecyclerView的緩存機制來談一談,RecyclerView的回收複用機制是怎麼樣的?

爲了方便下面文章的理解,我們先了解幾個方法的含義:

方法 對應Flag 含義 出現場景
isInvalid() FLAG_INVALID ViewHolder的數據是無效的 1.調用adapter的setAdapter()
2.adapter調用了notifyDataSetChanged();
3.調用RecyclerView的invalidateItemDecorations()。
isRemoved() FLAG_REMOVED ViewHolder已經被移除,源數據被移除了部分數據 adapter調用了notifyItemRemoved()
isUpdated() FLAG_UPDATE item的ViewHolder數據信息過時了,需要重新綁定數據 1.上述isInvalid()的三種情況都會;
2.調用adapter的onBindViewHolder();
3.調用了adapter的notifyItemChanged()。
isBound() FLAG_BOUND ViewHolder已經綁定了某個位置的item上,數據是有效的 調用了onBindViewHolder()方法

二、Recycler的幾級緩存

  RecyclerView不需要像ListView那樣if(contentView==null) {}else{}處理複用的邏輯,它回收複用是由Recycler來負責的,Recycler是負責管理scrapped(廢棄)或者detached(分離)的視圖(ViewHolder)以便重複使用。要想了解RecyclerView的回收複用原理,那麼首先了解Recycler的幾個結構體:

 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;

        RecycledViewPool mRecyclerPool;

        private ViewCacheExtension mViewCacheExtension;

        static final int DEFAULT_CACHE_SIZE = 2;
    }

Recycler中設置了四層緩存池,按照使用的優先級順序依次是Scrap、CacheView、ViewCacheExtension、RecycledViewPool;其中Scrap包括mAttachedScrap和mChangedScrap,ViewCacheExtension是默認沒有實現的,它RecyclerView留給開發者拓展的回收池。

● mAttachedScrap: 不參與滑動時的回收複用,只保存重新佈局時從RecyclerView分離的item的無效、未移除、未更新的holder。因爲RecyclerView在onLayout的時候,會先把children全部移除掉,再重新添加進入,mAttachedScrap臨時保存這些holder複用。

● mChangedScrap: mChangedScrap和mAttachedScrap類似,不參與滑動時的回收複用,只是用作臨時保存的變量,它只會負責保存重新佈局時發生變化的item的無效、未移除的holder,那麼會重走adapter綁定數據的方法。

● mCachedViews : 用於保存最新被移除(remove)的ViewHolder,已經和RecyclerView分離的視圖;它的作用是滾動的回收複用時如果需要新的ViewHolder時,精準匹配(根據position/id判斷)是不是原來被移除的那個item;如果是,則直接返回ViewHolder使用,不需要重新綁定數據;如果不是則不返回,再去mRecyclerPool中找holder實例返回,並重新綁定數據。這一級的緩存是有容量限制的,最大數量爲2。

● mViewCacheExtension: RecyclerView給開發者預留的緩存池,開發者可以自己拓展回收池,一般不會用到,用RecyclerView系統自帶的已經足夠了。

● mRecyclerPool: 是一個終極回收站,真正存放着被標識廢棄(其他池都不願意回收)的ViewHolder的緩存池,如果上述mAttachedScrap、mChangedScrap、mCachedViews、mViewCacheExtension都找不到ViewHolder的情況下,就會從mRecyclerPool返回一個廢棄的ViewHolder實例,但是這裏的ViewHolder是已經被抹除數據的,沒有任何綁定的痕跡,需要重新綁定數據。它是根據itemType來存儲的,是以SparseArray嵌套一個ArraryList的形式保存ViewHolder的。

接着我們來詳細分析一下各個緩存池:

2.1 緩存池一 (Scrap)

Scrap是RecyclerView最輕量的緩存,包括mAttachedScrap和mChangedScrap,它不參與列表滾動時的回收複用,作爲重新佈局時的臨時緩存,它的作用是,緩存當界面重新佈局前和界面重新佈局後都出現的ViewHolder,這些ViewHolder是無效、未移除、未標記的。在這些無效、未移除、未標記的ViewHolder之中,mAttachedScrap負責保存其中沒有改變的ViewHolder;剩下的由mChangedScrap負責保存。mAttachedScrap和mChangedScrap也只是分工合作保存不同ViewHolder而已。

注意:Scrap只是作爲佈局的臨時緩存,它和滑動時的緩存沒有任何關係,它的detach和atach只是臨時存在於佈局過程中。佈局結束時,Scrap列表應該是空的,緩存的數據要麼重新佈局出來,要麼被清空;總之在佈局結束後Scrap列表不應該存在任何東西。

我們上圖分析:
在這裏插入圖片描述
在一個手機屏幕中,將itemB刪除,並且調用notifyItemRemoved()方法,如果item是無效並且被移除的就會回收到其他的緩存,否則都是緩存到Scrap中,那麼mAttachedScrap和mChangedScrap會分別存儲itemView,itemA沒有任何的變化,存儲到mAttachedScrap中,itemB雖然被移出了,但是還有效,也被存儲到mAttachedScrap中(但是會被標記REMOVED,之後會移除);itemC和itemD發生了變化,位置往上移動了,會被存儲到mChangedScrap中。刪除時,ABCD都會進入Scrap中;刪除後,ACD都會回來,A沒有任何變化,CD只是位置發生了變化,內容沒有發生變化。

RecyclerView的局部刷新就是依賴Scrap的臨時緩存,當我們通過notifyItemRemoved()notifyItemChanged()通知item發生變化的時候,通過mAttachedScrap緩存沒有發生變化的ViewHolder,其他的則由mChangedScrap緩存,添加itemView的時候快速從裏面取出,完成局部刷新。注意,如果我們使用notifyDataSetChanged()來通知RecyclerView刷新,屏幕上的itemView被標記爲FLAG_INVALID並且未被移除,所以不會使用Scrap緩存,而是直接扔到CacheView或者RecycledViewPool池中,回來的時候重新走一次綁定數據。

注意:itemE並沒有出現在屏幕中,它不屬於Scrap管轄的範圍,Scrap只會換在屏幕中已經加載出來的itemView的holder。

2.2 緩存池二 (CacheView)

CacheView用於RecyclerView列表位置產生變動時,對剛剛移出屏幕的view進行回收複用。根據position/id來精準匹配是不是原來的item,如果是則直接返回使用,不需要重新綁定數據;如果不是則去RecycledViewPool中找holder實例返回,並且重新綁定數據。

CacheView的最大容量爲2,緩存一個新的ViewHolder時,如果超出了最大限制,那麼會將CacheView緩存的第一個數據添加到RecycledViewPool後再移除掉,最後纔會將新的ViewHolder添加進來。我們在滑動RecyclerView的時候,Recycler會不斷地緩存剛剛移出屏幕不可見的View到CacheView中,CacheView到達上限時又會不斷替換CacheView中舊的ViewHolder,將它們扔到RecycledViewPool中。如果一直朝一個方向滾動,CacheView並沒有在效率上產生幫助,它只是把後面滑過的ViewHolder緩存起來,如果經常來回滑動,那麼從CacheView根據對應位置的item直接複用,不需要重新綁定數據,將會得到很好的利用。

用圖來看看CacheView的複用場景:
在這裏插入圖片描述
從圖中可以看出,CacheView緩存剛剛變爲不可見的view,如果當前View再次進入屏幕中的時候,進行精準匹配,這個itemView還是 之前的itemView,那麼會從CacheView中獲取ViewHolder進行復用。如果一直向某一個方向滑動,那麼CacheView將會不斷替換緩存裏面的ViewHolder(CacheView最多隻能存儲2個),將替換掉的ViewHolder先放到RecycledViewPool中。在CacheView中拿不到複用的ViewHolder,那麼最後只能去RecycledViewPool中獲取。

2.3 緩存池三 (ViewCacheExtension)

ViewCacheExtension是緩存拓展的幫助類,額外提供了一層緩存池給開發者。開發者視情況而定是否使用ViewCacheExtension增加一層緩存池,Recycler首先去scrap和CacheView中尋找複用view,如果沒有就去ViewCacheExtension中尋找View,如果還是沒有找到,那麼最後去RecycledViewPool尋找複用的View。下面的講解將會不涉及ViewCacheExtension的知識,大家知道即可。

注意:Recycler並沒有將任何的view緩存到ViewCacheExtension中。所以在ViewCacheExtension中並沒有緩存任何數據。

2.4 緩存池四 (RecycledViewPool)

在Scrap、CacheView、ViewCacheExtension都不願意回收的時候,都會丟到RecycledViewPool中回收,所以RecycledViewPool是Recycler的終極回收站。

RecycledViewPool實際上是以SparseArray嵌套一個ArraryList的形式保存ViewHolder的,因爲RecycledViewPool保存的ViewHolder是以itemType來區分的。這樣方便不同的itemType保存不同的ViewHolder。它在回收的時候只是回收該viewType的ViewHolder對象,並沒有保存原來的數據信息,在複用的時候需要重新走onBindViewHolder()方法重新綁定數據。

我們來看看RecycledViewPool的結構:

    public static class RecycledViewPool {
        private static final int DEFAULT_MAX_SCRAP = 5;
        static class ScrapData {
            final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
            int mMaxScrap = DEFAULT_MAX_SCRAP;
        }
        SparseArray<ScrapData> mScrap = new SparseArray<>();
    }

可以看出,RecycledViewPool中定義了SparseArray<ScrapData> mScrap,它是一個根據不同itemType來保存靜態類ScrapData對象的SparseArray,ScrapData中包含了ArrayList<ViewHolder> mScrapHeap ,mScrapHeap是保存該itemType類型下ViewHolder的ArrayList。

緩存池定義了默認的緩存大小DEFAULT_MAX_SCRAP = 5,這個數量不是說整個緩存池只能緩存這多個ViewHolder,而是不同itemType的ViewHolder的list的緩存數量,即mScrap的數量,說明最多隻有5組不同類型的mScrapHeap。mMaxScrap = DEFAULT_MAX_SCRAP說明每種不同類型的ViewHolder默認保存5個,當然mMaxScrap的值是可以設置的。這樣RecycledViewPool就把不同ViewType的ViewHolder按類型分類緩存起來。

其實,Scrap緩存池不參與滾動的回收複用,CacheView緩存池被稱爲一級緩存,又因爲ViewCacheExtension緩存池是給開發者定義的緩存池,一般不用到,所以RecycledViewPool緩存池被稱爲二級緩存,那麼這樣來說實際只有兩層緩存。

三、源碼解析(回收和複用)

  單單看上面的解釋可能比較抽象、生硬,不明白這段話所表達的意思。這裏我們結合源碼來分析一下RecyclerView的回收複用流程,跟着源碼走你會明白RecyclerView的緩存整體結構。以LinearLayoutManager爲例,在RecyclerView<6>對RecyclerView的佈局流程進行了分析,但是沒有涉及到RecyclerView的回收複用機制,我們知道RecyclerView的佈局和回收複用都是在RecyclerView.LayoutManager處理的。

3.1 回收流程

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中。

試想我們插入了item或者刪除了item亦或者打亂了列表的順序,怎麼重新佈局這些item呢?如何將屏幕上現有的item佈局到新的位置呢?最簡單的方法就是把每個item從屏幕中分離下來,保存着,然後按照位置要求重新排列上去。

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,Recycler先廢棄它們,然後再在緩存列表中拿出來複用。

   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);
        }
    }

進入else分支,可以看到先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中
        }
    }

進入if()分支的ViewHolder保存到mAttachedScrap中,else分支的保存到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()層層追查下去,會來到recycler.recycleView(view)Recycler的公共回收方法中,:

  public void recycleView(@NonNull View view) {
        ViewHolder holder = getChildViewHolderInt(view);
        if (holder.isTmpDetached()) {
            removeDetachedView(view, false);
        }
        recycleViewHolderInternal(holder);
    }

回收分離的視圖到緩存池中,方便以後重新綁定和複用,這裏又來到了recycleViewHolderInternal(holder),和上面的一樣,按照優先級緩存 mCachedViews > RecycledViewPool。

那麼回收流程就到這裏結束了。

3.2 複用流程

itemView的回收流程分析完了,那麼這些回收的ViewHolder到底在什麼時候,什麼地方拿出來使用呢?回到LinearLayoutManager的佈局入口的方法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;
            }
        }
    
        //暫時分離已經附加的view,即將所有child detach並通過Scrap回收
        detachAndScrapAttachedViews(recycler);
        
        if (mAnchorInfo.mLayoutFromEnd) {
            //描點位置從start位置開始填充ItemView佈局
            updateLayoutStateToFillStart(mAnchorInfo);
            fill(recycler, mLayoutState, state, false);//填充所有itemView
           
 			//描點位置從end位置開始填充ItemView佈局
            updateLayoutStateToFillEnd(mAnchorInfo);
            fill(recycler, mLayoutState, state, false);//填充所有itemView
            endOffset = mLayoutState.mOffset;
        }else {
            //描點位置從end位置開始填充ItemView佈局
            updateLayoutStateToFillEnd(mAnchorInfo);
            fill(recycler, mLayoutState, state, false);
 
            //描點位置從start位置開始填充ItemView佈局
            updateLayoutStateToFillStart(mAnchorInfo);
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
        }
    }

回收view後,緊接着就是填充view了,上面提到,在重新佈局的時候會臨時將view緩存起來,再一個個把ViewHolder按照正確的位置填充上去。fill()就是填充由layoutState定義的給定佈局:

 int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
        recycleByLayoutState(recycler, layoutState);//回收滑出屏幕的view
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {//一直循環,知道沒有數據
            layoutChunkResult.resetInternal();
            layoutChunk(recycler, state, layoutState, layoutChunkResult);//添加一個child
            ······
            if (layoutChunkResult.mFinished) {//佈局結束,退出循環
                break;
            }
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;//根據添加的child高度偏移計算   
        }
     	······
        return start - layoutState.mAvailable;//返回這次填充的區域大小
    }

判斷當前可見區域還有沒有剩餘空間,如果有則填充view上去,核心是通過while()循環執行layoutChunk()填充一個itemView到屏幕, layoutChunk()完成佈局工作:

 void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);//獲取複用的view
        ······
        }

該方法通過layoutState.next(recycler)拿到視圖,我們看看它是怎麼拿到視圖的:

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

    @NonNull
    public View getViewForPosition(int position) {
        return getViewForPosition(position, false);
    }

    View getViewForPosition(int position, boolean dryRun) {
        return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
    }

tryGetViewHolderForPositionByDeadline()纔是獲取view的方法,它會根據給出的position/id去scrap、cache、RecycledViewPool、或者創建獲取一個ViewHolder:

    @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);
    }

這個方法確實做了不少事情,分別去scrap、CacheView、ViewCacheExtension、RecycledViewPool中獲取ViewHolder,如果沒有則創建一個新的ViewHolder返回,我們一步步來分析:

(1)第一步:如果是廢棄的發生改變的ViewHolder,則在scrap的mChangedScrap查找視圖,通過position和id分別查找;
這個一般在我們調用adapter的notifyItemChanged()方法時,數據發生變化,item緩存在mChangedScrap中,後續拿到的ViewHolder需要重新綁定數據。

   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)第二步:如果沒有找到視圖,根據position分別在scrap的mAttachedScrap、mChildHelper、mCachedViews中查找。在getScrapOrHiddenOrCachedHolderForPosition(position, dryRun)這個方法按照以下順序查找:

  • 首先從mAttachedScrap中查找,精準匹配有效的ViewHolder;
  • 接着在mChildHelper中mHiddenViews查找隱藏的ViewHolder;
  • 最後從我們的一級緩存中mCachedViews查找。
    //根據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;
        }
    }

(3)第三步:如果沒有找到視圖,通過id在scrap的mAttachedScrap、mCachedViews中查找。在getScrapOrCachedViewForId()這個方法按照以下順序:

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

注意:這一步是跟id來查找的,與上一步根據position查找類似。

  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;
        }
    }        

(4)第四步:在mViewCacheExtension中查找,前面提到這個緩存池是由開發者定義的一層緩存策略,Recycler並沒有將任何view緩存到這裏。這裏沒有定義過,所有找不到對應的view。

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

(5)第五步:從RecycledViewPool中查找,上面講過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;
    }

(6)第六步:如果還沒有獲取到ViewHolder,則通過mAdapter.createViewHolder()創建一個新的ViewHolder返回。

  //5)到最後如果還沒有找到複用的ViewHolder,則新建一個
  holder = mAdapter.createViewHolder(RecyclerView.this, type);

那麼複用流程到這裏也完畢了。

四、RecyclerVIew的回收複用原理

4.1 RecyclerVIew的回收原理

RecyclerView重新佈局onLayoutChildren()或者填充佈局fill()的時候,會先把必要的item與屏幕分離或者移除,並做好標記,保存到list中,在重新佈局時,再將ViewHolde拿出來重新一個個放到新的位置上去。

(1)如果是RecyclerView不滾動情況下緩存(比如刪除item),重新佈局時,把屏幕上的ViewHolder與屏幕分離下來,存放到Scrap中,即發生改變的ViewHolder緩存到mChangedScrap中,不發生改變的ViewHolder存放到mAttachedScrap中;剩下ViewHolder的會按照mCachedViews>RecycledViewPool的優先級緩存到mCachedViews或者RecycledViewPool中。

(2)如果是RecyclerVIew滾動情況下緩存(比如滑動列表),在滑動時填充佈局,先移除滑出屏幕的item,第一級緩存mCachedViews優先緩存這些ViewHolder,但是mCachedViews最大容量爲2,當mCachedViews滿了以後,會利用先進先出原則,把舊的ViewHolder存放到RecycledViewPool中後移除掉,騰出空間,再將新的ViewHolder添加到mCachedViews中,最後剩下的ViewHolder都會緩存到終極回收池RecycledViewPool中,它是根據itemType來緩存不同類型的ArrayList<ViewHolder>,最大容量爲5。

4.2 RecyclerVIew的複用原理

至此,已經有五個緩存RecyclerView的池子,mChangedScrap、mAttachedScrap、mCachedViews、mViewCacheExtension、mRecyclerPool,除了mViewCacheExtension是系統提供給開發者拓展的沒有用到之外,還有四個池子是參與到複用流程中的。

當RecyclerView要拿一個複用的ViewHolder時,如果是預加載,則會先去mChangedScrap中精準查找(分別根據position和id)對應的ViewHolder,如果有就返回,如果沒有就再去mAttachedScrap和mCachedViews中精確查找(先position後id)是不是原來的ViewHolder,如果是說明ViewHolder是剛剛被移除的,如果不是,則最終去mRecyclerPool找,如果itemType類型匹配對應的ViewHolder,那麼返回實例,讓它重新綁定數據,如果mRecyclerPool也沒有返回ViewHolder纔會調用createViewHolder()重新去創建一個。

這裏需要注意:在mChangedScrap、mAttachedScrap、mCachedViews中拿到的ViewHolder都是精準匹配,但是mChangedScrap的是發生了變化的,需要調用onBindViewHolder()重新綁定數據,mAttachedScrap和mCachedViews沒有發生變化,是直接使用的,不需要重新綁定數據,而mRecyclerPool中的ViewHolder的內容信息已經被抹除,需要重新綁定數據。所以在RecyclerView來回滾動時,mCachedViews緩存池的使用效率最高。

總的來說:RecyclerView着重在兩個場景緩存和回收的優化,一是:在數據更新時,使用Scrap進行局部更新,儘可能複用原來viewHolder,減少綁定數據的工作;二是:在滑動的時候,重複利用原來的ViewHolder,儘可能減少重複創建ViewHolder和綁定數據的工作。最終思想就是,能不創建就不創建,能不重新綁定就不重新綁定,儘可能減少重複不必要的工作。

整個過程大致如下:
在這裏插入圖片描述
至此!本文結束。


請尊重原創者版權,轉載請標明出處:https://blog.csdn.net/m0_37796683/article/details/105141373 謝謝!

相關文章:

理解RecyclerView(五)

 ● RecyclerView的繪製流程

理解RecyclerView(六)

 ● RecyclerView的滑動原理

理解RecyclerView(七)

 ● RecyclerView的嵌套滑動機制

理解RecyclerView(八)

 ● RecyclerView的回收複用緩存機制詳解

理解RecyclerView(九)

 ● RecyclerView的自定義LayoutManager

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章