天天看點

RecyclerView 源碼分析(一):Recycler前言RecyclerView.Recycler場景分析後記參考

文章目錄

  • 前言
  • RecyclerView.Recycler
    • 主要成員變量
    • RecycledViewPool
      • 成員變量
      • 主要方法
        • getScrapDataForType
        • setMaxRecycledViews
        • getRecycledView
        • putRecycledView
    • ViewCacheExtension
    • 主要方法
      • getViewForPosition
      • recycleView
    • 補充
      • mChangedScrap 和 mAttachedScrap 中的 View 從何而來
        • Recycler#scrapView
  • 場景分析
    • 第一次 layout
      • LinearLayoutManager#onLayoutChildren
      • LinearLayoutManager#fill
      • LinearLayoutManager#layoutChunk
    • 更新清單
      • Adapter#notifyDataSetChanged
        • LayoutManager#detachAndScrapAttachedViews
        • Recycler#recycleViewHolderInternal
      • Adapter#notifyItemChanged
        • RecyclerView#dispatchLayoutStep1
        • LayoutManager#scrapOrRecycleView
  • 後記
  • 參考

前言

RecyclerView 是一個好用又複雜的控件,其功能的高度解耦化,規範化的 ViewHolder 寫法,以及對動畫的友好支援,都是它與傳統 ListView 的差別。

它有幾大子產品:

  • LayoutManager:控制 item 的布局
  • RecyclerView.Adapter:為 RecyclerView 提供資料
  • ItemDecoration:為 RecyclerView 添加分割線
  • ItemAnimator:控制 item 的動畫
  • Recycler:負責回收和提供 View,和 RecyclerView 的複用機制相關

下面就從源碼(API 28)角度分析 RecyclerView,RecyclerView 的源碼很複雜,很難在一篇文章内講完,是以打算分幾篇來講,本文是第一篇,将圍繞 RecyclerView 的内部類 Recycler 展開分析:

RecyclerView.Recycler

首先看一下它的作用,源碼上是這樣寫的:

A Recycler is responsible for managing scrapped or detached item views for reuse.

意思就是 Recycler 負責管理廢棄或被 detached 的 item 視圖,以便重複利用。

它有以下幾個成員變量:

主要成員變量

final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
    
    ArrayList<ViewHolder> mChangedScrap = null;

    final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

    RecycledViewPool mRecyclerPool;

    private ViewCacheExtension mViewCacheExtension;
           

這幾個成員變量都和 RecyclerView 的緩存相關,如果按照四級緩存的話,它們可以這樣劃分:

第一級緩存:mAttachedScrap、mChangedScrap

第二級緩存:mCachedViews

第三級緩存:ViewCacheExtension

第四級緩存:RecycledViewPool

後面再介紹 mAttachedScrap、mChangedScrap、mCachedViews 具體存的是哪些 ViewHolder。

現在先了解下 RecycledViewPool 和 ViewCacheExtension這兩個類:

RecycledViewPool

繼續先看官方注釋:

RecycledViewPool lets you share Views between multiple RecyclerViews.

RecycledViewPool 用于在多個 RecyclerView 間共享 View。

在使用時,隻需建立 RecycledViewPool 執行個體,然後調用 RecyclerView 的 setRecycledViewPool(RecycledViewPool) 方法即可。

RecycledViewPool 存儲在 Recycler 中,通過 Recycler 存取。

成員變量

RecycledViewPool 有一個重要的成員變量:

// SparseArray 類似于 key 為 int 類型 的 HashMap
    SparseArray<ScrapData> mScrap = new SparseArray<>();
           

其中 ScrapData 的定義如下:

static class ScrapData {
        final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
        int mMaxScrap = DEFAULT_MAX_SCRAP;  // 5
        long mCreateRunningAverageNs = 0;
        long mBindRunningAverageNs = 0;
    }
           

mScrap 是一個 <int, ScrapData> 的映射,其中 int 代表了 viewType,ScrapData 則存儲了一個 ViewHolder 集合。

主要方法

getScrapDataForType

private ScrapData getScrapDataForType(int viewType) {
        ScrapData scrapData = mScrap.get(viewType);
        if (scrapData == null) {
            scrapData = new ScrapData();
            mScrap.put(viewType, scrapData);
        }
        return scrapData;
    }
           

該方法根據 viewType 擷取相應的 ScrapData,如果該 viewType 還沒有綁定 ScrapData,就新建立一個 ScrapData 并綁定到該 viewType。

setMaxRecycledViews

public void setMaxRecycledViews(int viewType, int max) {
        ScrapData scrapData = getScrapDataForType(viewType);
        scrapData.mMaxScrap = max;
        final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
		// 從後面開始删除,直到滿足新的容量
        while (scrapHeap.size() > max) {
            scrapHeap.remove(scrapHeap.size() - 1);
        }
    }
           

該方法可以設定相應 viewType 的 View 容量,超出容量時,從後面開始删除,直到滿足新的容量。

getRecycledView

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

該方法根據 viewType 擷取一個 ViewHolder,擷取到的 ViewHolder 将會被移除出 Scrap 堆。擷取不到則傳回 null。

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;
        }
        // 重置 ViewHolder,例如清空 flags
        scrap.resetInternal();
        // 将該 ViewHolder 添加到對應 viewType 的 集合中緩存起來
        scrapHeap.add(scrap);
    }
           

該方法也很好了解,根據 ViewHolder 的 viewType 放入 RecycledViewPool 的相應集合中,如果集合已滿,不再添加。

接下來看另一個類:

ViewCacheExtension

ViewCacheExtension 是一個由開發者控制的 View 緩存幫助類,其定義如下:

public abstract static class ViewCacheExtension {

        /**
         * Returns a View that can be binded to the given Adapter position.
         */
        @Nullable
        public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position,
                int type);
    }
           

開發者可以實作這個抽象類,通過調用 RecyclerView 的 setViewCacheExtension(ViewCacheExtension) 方法設定,最終将 ViewCacheExtension 存儲在 Recycler 中。

當調用 Recycler 的 getViewForPosition 方法時,如果 attached scrap 和 已經緩存都沒有找到合适的 View,就會調用 ViewCacheExtension 的 getViewForPositionAndType 方法來擷取 View。

需要注意的是,Recycler 不會對這個類做任何緩存處理,是否需要緩存 View 由開發者自己控制。

主要方法

看完這兩個類,現在回到 Recycler 中,看一下 Rcycler 的主要方法:

getViewForPosition

getViewForPosition 方法比較重要,用于擷取某個位置需要展示的 View,如下:

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

繼續看 tryGetViewHolderForPositionByDeadline 方法,該方法會依次從幾個緩存中擷取,分别來看一下:

// 如果是處于預布局階段(先簡單了解為執行 dispatchLayoutStep1 方法)
    // (其實下面方法要傳回 ture 還需要開啟“預處理動畫”,這跟動畫有關,先不多說)
	if (mState.isPreLayout()) {
		holder = getChangedScrapViewForPosition(position);
		fromScrapOrHiddenOrCache = holder != null;
	}
           

第一步,從 mChangedScrap 中擷取,擷取不到就傳回 null。

如果 holder 還是為 null,執行下面代碼:

// 1) Find by position from scrap/hidden list/cache
	if (holder == null) {
		holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
		if (holder != null) {
			if (!validateViewHolderForOffsetPosition(holder)) {
				// recycle holder (and unscrap if relevant) since it can't be used
				// 回收無效的 ViewHolder
			    // ...
			} else {
				fromScrapOrHiddenOrCache = true;
			}
		}
	}
           

第二步,根據 position 依次從 mAttachedScrap、mHiddenViews(存儲在 ChildHelper 類)、mCachedViews 中擷取緩存的 ViewHolder。

可以從 mHiddenViews 擷取到緩存的話,就将其從 mHiddenViews 移除并添加到 Scrap 緩存(根據情況添加到 mAttachedScrap 或 mChangedScrap)。可以從 mCacheViews 中擷取到緩存的話,就将其從 mCacheViews 移除。

擷取到後,發現無效的話,将對擷取到的 ViewHolder 進行清理并回收(放入 mCachedViews 或 RecycledViewPool)。

擷取不到,就繼續往下執行:

// 預設傳回 false,可通過 Adapter.setHasStableIds 方法設定該值
	if (mAdapter.hasStableIds()) {
		// 根據 id 依次在 mAttachedScrap、mCachedViews 中擷取緩存
		holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
				type, dryRun);
		if (holder != null) {
			holder.mPosition = offsetPosition;
			fromScrapOrHiddenOrCache = true;
		}
	}
           

第三步,根據 id 依次從 mAttachedScrap、mCachedViews 中擷取緩存,還沒有擷取到就繼續往下:

// 如果使用者設定了 ViewCacheExtension
	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);
			// ...
		}
	}
           

第四步,從使用者設定的 ViewCacheExtension 中擷取緩存,沒有擷取到就繼續往下:

if (holder == null) { // fallback to pool
		holder = getRecycledViewPool().getRecycledView(type);
        // ...
	}
           

第五步,根據 viewType 從 RecycledViewPool 中得到緩存。

RecycledViewPool 已經是最後一級緩存了,如果這裡也沒有擷取到,隻能通過 Adapter 的 createViewHolder 方法建立一個 ViewHolder:

if (holder == null) {

		holder = mAdapter.createViewHolder(RecyclerView.this, type);

        // ...
	}
           

最後小結一下擷取某個位置的 View 的過程:

  1. 先後根據 position 或 id 從 mChangedScrap 中擷取緩存
  2. 根據 position 依次從 mAttachedScrap、mHiddenViews(存儲在 ChildHelper 類)、mCachedViews 中擷取緩存
  3. 根據 id 依次從 mAttachedScrap、mCachedViews 中擷取緩存
  4. 從使用者設定的 ViewCacheExtension 中擷取緩存
  5. 從 RecycledViewPool 中得到緩存的廢棄 ViewHolder
  6. 通過 Adapter 的 createViewHolder 方法建立一個 ViewHolder

recycleView

既然叫 Recycler,那肯定要做回收工作了,recycleView 方法就完成了這些工作,下面看一下該方法的實作:

public void recycleView(@NonNull View view) {
        ViewHolder holder = getChildViewHolderInt(view);
        // ...
        recycleViewHolderInternal(holder);
    }
           

繼續看 recycleViewHolderInternal:

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();
				// 若 CacheViews 達到最大容量(2),将最老的緩存從 CacheViews 移除,并添加到 RecycledViewPool 中
                if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                    recycleCachedViewAt(0);
                    cachedViewSize--;
                }

				// ...
				
				// 将 View 緩存到 mCachedViews 中
                mCachedViews.add(targetCacheIndex, holder);
                cached = true;
            }
            if (!cached) {
				// 沒有添加到 mCachedViews 的話,就添加到 RecycledViewPool 中
                addViewHolderToRecycledViewPool(holder, true);
                recycled = true;
            }
        }

		// ...
    }
           

可以看到,回收過程主要涉及到兩層緩存,第一層緩存是 CacheViews,在添加時,如果發現原來的 CacheViews 已經達到最大容量,就将最老的緩存從 CacheViews 移除,并添加到 RecycledViewPool。第二層緩存是 RecycledViewPool,如果不能添加到 mCacheViews,就會添加到 RecycledViewPool 中。

補充

mChangedScrap 和 mAttachedScrap 中的 View 從何而來

從前面可以得知,在執行 Recycler 的 recycleView 方法時,會将回收的 View 緩存到 mCahceViews 或 recycledViewPool 中,那麼另外兩個 Scrap 緩存(mChangedScrap 和 mAttachedScrap)中的 View 是何時添加進來的呢?

無論是 mAttachedScrap 還是 mChangedScrap ,它們獲得 View 的途徑都隻有一個,那就是通過 Recycler 的 scrapView 方法。先看下該方法:

Recycler#scrapView

void scrapView(View view) {
        final ViewHolder holder = getChildViewHolderInt(view);
		
		// 滿足這幾個條件中的一個就可以進入 if 循環,有機會将 View 緩存到 mAttachedScrap
		// 1. ViewHolder 設定了 FLAG_REMOVED 或 FLAG_INVALID
		// 2. ViewHolder 沒有設定 FLAG_UPDATE
		// 3. 沒有設定動畫或者動畫可以重用該 ViewHolder
        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." + exceptionLabel());
            }
			// 給 ViewHolder 綁定 Recycler
            holder.setScrapContainer(this, false);
            mAttachedScrap.add(holder);
        } 
		// 不滿足上述任意一個條件時,将 View 緩存到 mChangedScrap 中
		else {
            if (mChangedScrap == null) {
                mChangedScrap = new ArrayList<ViewHolder>();
            }
            holder.setScrapContainer(this, true);
            mChangedScrap.add(holder);
        }
    }
           

該方法通過判斷 ViewHolder 的 flag 以及是否設定 ItemAnimator 等,決定将 View 緩存到 mAttachedScrap 還是 mChangedScrap。

那麼該方法在何時調用呢?有兩種情況:

  1. 以 LinearLayoutManager 為例,在它的 onLayoutChildren 方法中,會調用

該方法定義在 RecyclerView 的 LayoutManager 中,它繼續調用 scrapOrRecycleView 方法,如果在該方法符合條件就調用 Recycler 的 scrapView 方法。

  1. 通過 mHiddenViews 擷取到緩存時,也會調用 scrapView 方法。

場景分析

下面就根據一些場景來分析下 Recycler 是如何進行回收和複用的。

第一次 layout

由于這裡不是專門分析 layout 過程的,就不從 onLayout 開始說了,中間的過程省略掉,它最終會調用到 LayoutManager 的 onLayoutChildren,這裡以 LinearLayoutManager 為例:

LinearLayoutManager#onLayoutChildren

@Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        // ...

        // 找到錨點(具體過程等到分析 layout 時再說)
		
		// (1)
		detachAndScrapAttachedViews(recycler);
		
        if (mAnchorInfo.mLayoutFromEnd) {
			// ...
        } else {
            
            // (2)
            fill(recycler, mLayoutState, state, false);

			// ...
        }
		
		// ...
    }
           

首先看(1)處,detachAndScrapAttachedViews 方法會根據情況将子 View 回收到相應緩存,具體過程之後再看,由于現在是第一次 layout,RecyclerView 中沒有子 View,是以現在該方法沒啥用。

接下來看(2)處,這裡的 fill 方法比較重要,它的作用是填充布局。看一下該方法:

LinearLayoutManager#fill

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {

		// 進行 layout 時 layoutState.mScrollingOffset 的值被設定為
		// LayoutState.SCROLLING_OFFSET_NaN,不會進入此 if 塊
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
			// ...
            recycleByLayoutState(recycler, layoutState);
        }
		
		// 需要填充的空間
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
		// 還有需要填充的空間并且 item 數未滿
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
			// ...
			
			// (1)
            layoutChunk(recycler, state, layoutState, layoutChunkResult);

			// 計算剩餘空間

			// 同上,在 layout 時不會進入 if 塊中
            if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
				// ...
                recycleByLayoutState(recycler, layoutState);
            }
			
			// ...
        }
    }
           

主要看(1)處的 layoutChunk 方法,隻要還有需要填充的空間,就會不斷調用該方法:

LinearLayoutManager#layoutChunk

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
		// (1)
        View view = layoutState.next(recycler);

		// ...
		
		// 預設情況下,layoutState.mScrapList 等于 null
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
				// (2)
                addView(view);
            } else {
                addView(view, 0);
            }
        } else {
			// ...
        }
    }
           

(2)處的 addView 方法就不多說了,該方法将得到的子 View 添加到 RecyclerView 中。主要看(1)處,看看子 View 從何而來:

View next(RecyclerView.Recycler recycler) {
        // ...
        
        final View view = recycler.getViewForPosition(mCurrentPosition);

        return view;
    }
           

這個方法是不是很熟悉呢?沒錯,它就是之前分析的 Recycler 的 getViewForPosition 方法。

不過由于現在沒有任何緩存,是以第一次 layout 的時候是通過 Adapter 的 createViewHolder 來建立子 View的,并且沒有添加任何緩存。

更新清單

更新清單可以使用 Adapter 的一系列 notify 方法,這裡分析其中兩個方法:notifyDataSetChanged 和 notifyItemChanged(int)。

Adapter#notifyDataSetChanged

該方法最終調用了 RecyclerViewDataObserver 的 onChanged 方法:

@Override
    public void onChanged() {
		// ...

		// 該方法主要做了這兩件事
		// 1. 給所有 ViewHolder 添加了 FLAG_UPDATE 和 FLAG_INVALID
		// 2. 預設情況下(mHasStableIds 為 false)清空 CacheViews
        processDataSetCompletelyChanged(true);
		
        if (!mAdapterHelper.hasPendingUpdates()) {
			// 進行視圖重繪
            requestLayout();
        }
    }
           

該方法會進行視圖重繪,又來到了 layout 過程,繼續以 LinearLayoutManager 為例,從它的 onLayoutChildren 方法看起,由于分析第一次 layout 時已經看過一遍了,這次主要看下不同之處:

@Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
		// ...
		
		detachAndScrapAttachedViews(recycler);
		
		// ...
    }
           

主要差別在于 detachAndScrapAttachedViews 方法,這次它開始起作用了,該方法在 RecyclerView 的 LayoutManager 中定義,看下它的實作:

LayoutManager#detachAndScrapAttachedViews

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

由于不是第一次 layout,RecyclerView 這時已經有子 View 了,該方法周遊子 View,調用 scrapOrRecycleView 方法:

private void scrapOrRecycleView(Recycler recycler, int index, View view) {
        final ViewHolder viewHolder = getChildViewHolderInt(view);
		// 不能回收添加了 FLAG_IGNORE 标記的 ViewHolder
		// 可通過 LayoutManager 的 ignoreView 為相應的 View 添加該标記
        if (viewHolder.shouldIgnore()) {
            return;
        }
		// 這些條件都滿足,進入 if 塊
        if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                && !mRecyclerView.mAdapter.hasStableIds()) {
            removeViewAt(index);
            recycler.recycleViewHolderInternal(viewHolder);
        } else {
            // ...
        }
    }
           

這裡将子 View 移除并通過 Recycler 的 recycleViewHolderInternal 方法進行回收:

Recycler#recycleViewHolderInternal

void recycleViewHolderInternal(ViewHolder holder) {
			// ...
            boolean cached = false;
            boolean recycled = false;

            if (forceRecycle || holder.isRecyclable()) {
				// 由于此時的 ViewHolder 有 FLAG_INVALID 标記,不會進入此 if 塊
                if (mViewCacheMax > 0
                        && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                        | ViewHolder.FLAG_REMOVED
                        | ViewHolder.FLAG_UPDATE
                        | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
					//...
                }
				// cached 仍為 false,進入此 if 塊
                if (!cached) {
					// 通過 RecycledViewPool 的 putRecycledView 方法緩存該 ViewHolder
                    addViewHolderToRecycledViewPool(holder, true);
                    recycled = true;
                }
            } 
			
			// ...
        }
           

最終被移除的子 View 緩存到了 RecycledViewPool 中。

後面在調用 fill 方法進行布局填充時,就可以從 RecycledViewPool 中拿取緩存的 View。

Adapter#notifyItemChanged

該方法傳入一個 int 參數,表示要資料有更新的 item 的 position。

public final void notifyItemChanged(int position) {
        mObservable.notifyItemRangeChanged(position, 1);
    }
           

最終調用 RecyclerViewDataObserver 的 onItemRangeChanged 方法:

@Override
    public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
        // 會在 mAdapterHelper 中建立一個 UpdateOp,将資訊儲存起來
        if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
            // 如果可以進行更新操作,執行該方法
            triggerUpdateProcessor();
        }
    }
           

繼續看 triggerUpdateProcessor 方法:

void triggerUpdateProcessor() {
        // 判斷條件預設為 false,執行 else 塊
        if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
            // ...
        } else {
            mAdapterUpdateDuringMeasure = true;
            requestLayout();
        }
    }
           

在儲存了一些資訊後,還是進行視圖重繪。來到了 layout 過程後,還是以 LinearLayoutManager 為例,這次先看下布局過程的 step1,也就是 dispatchLayoutStep1 方法:

RecyclerView#dispatchLayoutStep1

private void dispatchLayoutStep1() {
        // ...
        
        processAdapterUpdatesAndSetAnimationFlags();
        
        // ...
    }
           

主要看 processAdapterUpdatesAndSetAnimationFlags 方法,從名字也可以看出,它負責更新 adapter 的資訊:

private void processAdapterUpdatesAndSetAnimationFlags() {
		// ...

        if (predictiveItemAnimationsEnabled()) {
            mAdapterHelper.preProcess();
        } else {
            mAdapterHelper.consumeUpdatesInOnePass();
        }

		// ...
    }
           

這裡借助了 mAdapterHelper,它最終又通過接口回調(回調了 markViewHoldersUpdated 方法)調用了 RecyclerView 的 viewRangeUpdate 方法:

void viewRangeUpdate(int positionStart, int itemCount, Object payload) {
        // ...

        for (int i = 0; i < childCount; i++) {
            // ...
            
            if (holder.mPosition >= positionStart && holder.mPosition < positionEnd) {
                // (1)
                holder.addFlags(ViewHolder.FLAG_UPDATE);
                // ...
            }
        }
    }
           

該方法就是周遊所有子 View,找到所有發生了改變的子 View,進行相關操作。這裡重點看注釋(1),為改變的 ViewHolder 添加了 FLAG_UPDATE 标記。先記住這點,在後面會用到。

接下來看 onLayoutChildren 方法,和 notifyDataSetChanged 一樣,主要的不同之處也是在于 detachAndScrapAttachedViews 方法,該方法周遊子 View,調用 scrapOrRecycleView 方法,下面看一下該方法:

LayoutManager#scrapOrRecycleView

private void scrapOrRecycleView(Recycler recycler, int index, View view) {
        final ViewHolder viewHolder = getChildViewHolderInt(view);
		// ...
		
		// 這次 ViewHolder 沒有添加 FLAG_INVALID 标記,進入 else 塊
        if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                && !mRecyclerView.mAdapter.hasStableIds()) {
			// ...
        } else {
            detachViewAt(index);
            recycler.scrapView(view);
            mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
        }
    }
           

這裡就和 notifyDataSetChanged 時不一樣了,由于在視圖重繪前沒有給 ViewHolder 添加 FLAG_INVALID 标記,這次進入的是 else 塊。

首先将 View 從 RecyclerView 中 detach 掉(而不是 remove 掉)。然後在回收時,調用的是 Recycler 的 scrapView 方法。該方法在前面也分析過了,這裡再看一次:

void scrapView(View view) {
        final ViewHolder holder = getChildViewHolderInt(view);
		
		// 滿足這幾個條件中的一個就可以進入 if 循環
		// 1. ViewHolder 設定了 FLAG_REMOVED 或 FLAG_INVALID 
		// 2. ViewHolder 沒有設定 FLAG_UPDATE 
		// 3. 沒有設定動畫或者動畫可以重用該 ViewHolder 
        if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
            // ...
            
            mAttachedScrap.add(holder);
        } 
		// 不滿足上述任意一個條件時,将 View 緩存到 mChangedScrap 中
		else {
            if (mChangedScrap == null) {
                mChangedScrap = new ArrayList<ViewHolder>();
            }
            holder.setScrapContainer(this, true);
            mChangedScrap.add(holder);
        }
    }
           

重點看判斷裡面的條件 2,從前面的分析可以得知,對于發生改變的 ViewHolder,給它設定了 FLAG_UPDATE,是以它現在三個條件都不滿足,進入 else 塊,而對于其他的 ViewHolder,由于沒有設定 FLAG_UPDATE,是以滿足條件 2,進入 if 循環。

是以通過 notifyItemChanged 方法更新清單時,發生了改變的子 View 将被緩存到 ChangedScrap 中,而沒有發生改變的子 View 則緩存到 AttachedScrap 中,之後通過填充布局的時候對于不同 item 就可以從相應的 Scrap 緩存中得到子 View。

另外,Scrap 緩存隻作用于布局階段,在 layout 的 step3 中将會清空 mAttachedScrap 和 mChangedScrap。

其實還有一個常見的場景是滑動操作,滑動出螢幕的子 View 将會緩存到 mCachedView,不過這裡就不詳細說了,在之後會在其他文章專門分析滑動這塊。

後記

本文圍繞 Recycler 展開叙述,重點是要通過它的幾個成員變量了解它的緩存機制,四級緩存分别是什麼,是在何時調用的,各自起到的作用,不同場景下使用哪種緩存等。

Recycler 和 LayoutManager 的布局以及動畫都有聯系,例如 LayoutManager 負責布局,它決定擷取子 View 和回收子 View 的時機,具體的工作就交由 Recycler 負責。這些會在之後對 RecyclerView 的其他方面作分析時進行更詳細的說明。

參考

  • RecyclerView 源碼分析
  • RecyclerView源碼分析
  • RecyclerView的複用機制
  • RecyclerView的緩存分析
  • RecyclerView 源碼分析(三) - RecyclerView的緩存機制