學習筆記-淺析RecyclerView複用機制

你瞭解recyclerView如何實現的複用,要不要一塊兒來探索一下,剝開recyclerView神祕面紗?java

1. 一次滑動

爲了找到recyclerView如何實現的複用,先從最多見的場景,手指滑動屏幕開始入手。面試

1.1. 從事件起點入手

手指接觸屏幕滑動時,首先觸發到onTouchEvet(),在ACTION_MOVE事件中調用到scrollByInternal(),以後繼續傳遞進入了scrollStep()數據庫

而在scrollStep()中,將滾動直接委託分派給了LayoutManger處理。緩存

class RecyclerView {
    LayoutManager mLayout;
    ...
    public boolean onTouchEvent(MotionEvent e) {
        ...
        switch (action) {
            ...
            case MotionEvent.ACTION_MOVE: {
                ...
                int dx = mLastTouchX - x;
                int dy = mLastTouchY - y;
                if (scrollByInternal(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        e)) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }

            } break;
        }
    }

    boolean scrollByInternal(int x, int y, MotionEvent ev) {
        ...
        if (mAdapter != null) {
            ...
            scrollStep(x, y, mReusableIntPair);
        }
    }

    void scrollStep(int dx, int dy, @Nullable int[] consumed) {
        ...
        if (dx != 0) {
            consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
        }
        if (dy != 0) {
            consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
        }
    }
}
複製代碼

1.2. 處理滾動

以縱向的LinearLayoutManager,向上滑動爲例。接着往下面走,看看到底是怎麼處理的。markdown

首先LinearLayoutManager.scrollVerticallyBy()中,判斷是否是縱向的,而後向後傳遞到scrollBy()函數

public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (mOrientation == HORIZONTAL) {
        return 0;
    }
    return scrollBy(dy, recycler, state);
}
複製代碼

傳遞到scrollBy(),終於開始處理滾動:oop

  1. 首先把dy分解成方向和偏移值,其中方向對應兩個值,分別是LAYOUT_END=1,LAYOUT_START=-1。前面說過咱們目前看的向上滑動,dy > 0,對應LAYOUT_END佈局

  2. 調用到updateLayoutState(),這個方法是給LayoutState中的參數賦值,而LayoutState就是如何進行填充的配置參數。post

  3. 計算consumed,能夠理解爲計算出RecyclerView支持滾動的距離,其中調用到的fill(),就是控制表項的建立、回收、複用等的地方。動畫

  4. 最後就是對Children作偏移,mOrientationHelper.offsetChildren()最終會回傳到RecyclerView中對全部表項作位移。這裏有個判斷條件absDelta > consumed,就是滑動的距離和RecyclerView支持滾動的距離比較,實際的滾動距離,就是二者中較小的一個。

int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...
    final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
    final int absDelta = Math.abs(delta);
    
    updateLayoutState(layoutDirection, absDelta, true, state);
    
    final int consumed = mLayoutState.mScrollingOffset
            + fill(recycler, mLayoutState, state, false);
    
    final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
    mOrientationHelper.offsetChildren(-scrolled);
}
複製代碼

繼續日後走,進入到updateLayoutState(),計算的幾個關鍵值,給LayoutState中的參數賦值。

  1. layoutDirection:滾動的方向,前面計算的,直接賦值。
  2. scrollingOffset:不添加新子項的狀況下可滾動的距離。如今看的是向上滑動,這個值就是最底部的子項在屏幕外的高度。也就是最底部的子項的底部位置與RecyclerView底部位置的差值。
  3. available:須要填充的高度。滑動距離與scrollingOffset的差值(多是負值)。
private void updateLayoutState(int layoutDirection, int requiredSpace, boolean canUseExistingSpace, RecyclerView.State state) {
    ...
    mLayoutState.mLayoutDirection = layoutDirection;
    boolean layoutToEnd = layoutDirection == LayoutState.LAYOUT_END;
    int scrollingOffset;
    if (layoutToEnd) {
        ...
        final View child = getChildClosestToEnd();
        mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
        scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
                - mOrientationHelper.getEndAfterPadding();

    }
    mLayoutState.mAvailable = requiredSpace;
    if (canUseExistingSpace) {
        mLayoutState.mAvailable -= scrollingOffset;
    }
    mLayoutState.mScrollingOffset = scrollingOffset;
}
複製代碼

再回頭進入到了fill(),控制表項的建立、回收、複用等的地方。

在這裏能夠看到兩個關鍵函數,recycleByLayoutState()layoutChunk(),從名字上就能夠看出對應的就是回收以及複用方法。

這裏能夠看到,在最開始先調用了一次回收方法,接着走進了一個while循環,循環的條件就是有須要填充的高度,以及能夠有足夠的控件能夠用於填充。而在循環中的流程能夠分爲三步:

  1. 添加下一個控件,layoutChunk()
  2. 從新計算available
  3. 回收,recycleByLayoutState()

對比循環前以及循環中的回收,都對scrollingOffset作了必定計算,回到前面對scrollingOffset的定義,不添加新子項的狀況下可滾動的距離。在這裏須要更新一下理解,scrollingOffset是循環中每一次添加表項的時候從新計算出來的實際滾動距離,也就是每一次添加表項時候,滑動的距離和支持滾動的距離二者的最小值。

最後,函數的返回值換算一下,就等於循環中添加的全部控件的高度和,也就是填充的高度。傳遞給外面計算支持滾動的距離。

int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
    ...
    final int start = layoutState.mAvailable;
    if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
        if (layoutState.mAvailable < 0) {
            layoutState.mScrollingOffset += layoutState.mAvailable;
        }
        recycleByLayoutState(recycler, layoutState);
    }
    int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) 
        ...
        //step0
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
        //step1
        layoutState.mAvailable -= layoutChunkResult.mConsumed;
        remainingSpace -= layoutChunkResult.mConsumed;
        //step2
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            recycleByLayoutState(recycler, layoutState);
        }
    }
    return start - layoutState.mAvailable;
}
複製代碼

1.3. 回收

前面分析到在fill()方法中,調用recycleByLayoutState()方法實現表項的回收,進入其中探索一下。

  1. recycleByLayoutState()中根據滾動的方向,分發到不一樣的方法,前面說到分析的是向上滑動,對應的方法就是recycleViewsFromStart()。這裏也將前面所分析到的每一次的實際滾動距離scrollingOffset的值傳遞了下去。
  2. recycleViewsFromStart()從頭遍歷子控件,經過控件底部的位置與實際滾動長度比較,找出第一個底部位置大於滾動長度的控件,表示這些控件將會滾出屏幕,而後跳出到下一個方法recycleChildren()
  3. recycleChildren()中的方法,遍歷上一步選擇出來的控件,這裏使用到的實際上是上一步找到的控件位置減一的位置,也就是說回收的是底部位置小於滾動長度的控件。
  4. 最後就是調用到了removeAndRecycleViewAt(),裏面分別調用了remove方法移除控件,以及調用Recycler的方法,對控件就行回收。看到這裏,RecyclerView的回收操做是委託給內部類RecyclerView.Recycler的,下一節會深刻到這個類。
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
    ...
    int scrollingOffset = layoutState.mScrollingOffset;
    int noRecycleSpace = layoutState.mNoRecycleSpace;
    if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
        recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);
    } else {
        recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
    }
}

private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset, int noRecycleSpace) {
    ...
    final int limit = scrollingOffset - noRecycleSpace;
    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        View child = getChildAt(i);
        if (mOrientationHelper.getDecoratedEnd(child) > limit
                || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
            recycleChildren(recycler, 0, i);
            return;
        }
    }
}

private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
    if (startIndex == endIndex) {
        return;
    }
    ...
    if (endIndex > startIndex) {
        for (int i = endIndex - 1; i >= startIndex; i--) {
            removeAndRecycleViewAt(i, recycler);
        }
    }
}

public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) {
    final View view = getChildAt(index);
    removeViewAt(index);
    recycler.recycleView(view);
}
複製代碼

1.4. 添加

回過頭最後看一下layoutChunk()是如何添加控件的。首先是獲取控件,最終獲取表項的地方仍是進入到了RecyclerView.Recycler中。獲取到表項以後,就添加進來以及走了measurelayout流程。

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
    ...
    View view = layoutState.next(recycler);
    //add
    addView(view);
    //measure
    measureChildWithMargins(view, 0, 0);
    result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
    //layout
    layoutDecoratedWithMargins(view, left, top, right, bottom);
}

//LayoutState方法
View next(RecyclerView.Recycler recycler) {
    ...
    final View view = recycler.getViewForPosition(mCurrentPosition);
    return view;
}
複製代碼

1.5. 思考

源碼閱讀到這裏,心中不由畫了一個巨大的問號,這不是在滾動嗎?爲何還沒滾動就已經開始回收?那以後的滾動操做不就會有一部分空白展現出來??

回答這個問題,首先要先回答另外一個問題,咱們是如何實現滾動或者動畫這種連續的操做的?這種連續,在計算機的世界中都是連續值採樣獲得的離散值。換個通俗的話,滾動並非不是連續,從顯示上是一幀一幀的快速變換的畫面構成,從操做上是一次一次的快速掃描位置構成。

再回到最初的問題,如今處理的問題真的是滾動嗎?咱們拿到的dy,是在一個採樣時間段內拿到的手指的位移,咱們要作的事情並非要從第一個位置連續的播放滾動動畫到下一個位置,而是根據滾動距離dy,直接從第一個位置切換到第二個位置。

因此對滾動的操做其實是簡單的一段一段偏移操做,將連續的動做簡單化爲兩個瞬態樣式的轉換 。

這也就很好的解釋了,爲何在滾動以前,就根據滾動距離,對頂部的控件進行了回收。這只是在對下一個瞬態的樣式作準備罷了。

分析到這裏,一次滾動的總體流程就結束了。不過也留下了一個更大的問題,截止的地方都是進入到了RecyclerView.Recycler中,接下來進入其中一探究竟,它是如何實現的回收以及複用。

2. RecyclerView.Recycler

前面看到,表項的複用和回收都是委託給了RecyclerView.Recycler處理。

爲了更順暢的閱讀,先介紹一些後面分析獲得的結論。

首先REcycler中的緩存分了不少級,分別是scrapcachepool以及extension。這四級的緩存分別存儲在這五個對象中:

  • attachedScrap:佈局過程當中屏幕可見表項的回收和複用。佈局時,先將全部表項進行回收,而後再添加進來,數量沒有上限,通常數量就是屏幕中全部的可見表項,佈局走到最後會所有清除。會在調用添加(notifyItemInserted())、刪除(notifyItemRemoved())、移動(notifyItemMoved())和修改(notifyItemChanged())表項的時候起做用。
  • changedScrap:佈局過程當中屏幕可見而且發生改變表項的回收和複用。只會存儲被修改(notifyItemChanged())的表項。一樣也會在佈局結束時所有清除。
  • cachedViews:數量默認上限2個,先進先出的隊列結構,當有新的表項須要存入但數量達到上限時,會將最先的存入recyclerPool。至關於recyclerPool的預備隊列。
  • recyclerPool:根據viewType分類存儲,每一個類型上限默認5個。
  • viewCacheExtension:緩存擴展,用戶可自定義的緩存策略。

在添加、刪除和修改表項的時候,RecyclerView會進行兩次佈局,第一次佈局會對全部表項進行佈局,第二次會對更改後的狀態進行佈局。根據兩次佈局之間的區別判斷如何執行動畫。這裏的第一次佈局,就是預佈局preLayout

瞭解這些以後,咱們首先先進入到複用的方法看看。

2.1. 複用

根據上一節複用的方法recycler.getViewForPosition(),查看調用鏈會走到tryGetViewHolderForPositionByDeadline()中,再回頭查看,全部的建立方法最後都會調用到這個方法。咱們直接從這個方法開始分析。

在這個方法中爲了獲得使用的表項,進行了六次次嘗試。

  1. getChangedScrapViewForPosition(),嘗試在changeScrap中搜索。
  2. getScrapOrHiddenOrCachedHolderForPosition(),嘗試經過positionattachedScrap、隱藏的表項以及cachedView中搜索。
  3. getScrapOrCachedViewForId(),嘗試經過idattachedScrapcachedView中搜索。
  4. 嘗試經過viewCacheExtension回調從自定義緩存中搜索。
  5. 嘗試經過recyclerPool獲取表項。
  6. 直接建立表項。

這裏先忽略掉自定義緩存和直接建立,對其餘的一探究竟,繼續往裏面走。

ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
    ...
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
    // 0) 嘗試從changedScrap中搜索
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    // 1)經過position在attachedScrap、隱藏的表項以及cachedView中搜索
    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        if (holder != null) {
            if (!validateViewHolderForOffsetPosition(holder)) {
                ...
                holder = null;
            } else {
                fromScrapOrHiddenOrCache = true;
            }
        }
    }
    if (holder == null) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        final int type = mAdapter.getItemViewType(offsetPosition);
        // 2) 嘗試經過id在attachedScrap、cachedView中搜索
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
            if (holder != null) {
                // update position
                holder.mPosition = offsetPosition;
                fromScrapOrHiddenOrCache = true;
            }
        }
        // 3) 嘗試在viewCacheExtension中搜索
        if (holder == null && mViewCacheExtension != null) {
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
            }
        }
        // 4) 嘗試在recyclerPool中搜索
        if (holder == null) {
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                holder.resetInternal();
            }
        }
        // 5) 直接建立
        if (holder == null) {
            holder = mAdapter.createViewHolder(this, type);
        }
    }
    ...
    return holder;
}
複製代碼

2.1.1. getChangedScrapViewForPosition()

嘗試在changeScrap中搜索。

上一步能夠看到,進入這個方法必須是preLayout,再結合changeScrap中只會存儲被修改的表項,因此這個表項也就只能在preLayout過程當中複用被修改的表項。

其中的邏輯就是前後經過positionidchangeScrap中搜索。

ViewHolder getChangedScrapViewForPosition(int position) {
    // If pre-layout, check the changed scrap for an exact match.
    final int changedScrapSize;
    if (mChangedScrap == null || (changedScrapSize = mChangedScrap.size()) == 0) {
        return null;
    }
    // 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()) {
        ...
        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;
}
複製代碼

2.1.2. getScrapOrHiddenOrCachedHolderForPosition()

首先經過positionattchedScrap中搜索。而後查看當前position有沒有隱藏的表項。最後經過positioncachedView中搜索。

ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {

    // Try first for an exact, non-invalid match from scrap.
    final int scrapCount = mAttachedScrap.size();
    for (int i = 0; i < scrapCount; i++) {
        final ViewHolder holder = mAttachedScrap.get(i);
        if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
                && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
            holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
            return holder;
        }
    }

    if (!dryRun) {
        View view = mChildHelper.findHiddenNonRemovedView(position);
        if (view != null) {
            ...
            vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
                    | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
            return vh;
        }
    }

    // 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);
        if (!holder.isInvalid() && holder.getLayoutPosition() == position
                && !holder.isAttachedToTransitionOverlay()) {
            ...
            return holder;
        }
    }
    return null;
}
複製代碼

2.1.3. getScrapOrCachedViewForId()

這個方法與上一個十分類似,把position替換成了id,以及少了從隱藏的表項中查找。

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);
        if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
            if (type == holder.getItemViewType()) {
                ...
                holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                return holder;
            }
        }
    }

    // Search the first-level cache
    final int cacheSize = mCachedViews.size();
    for (int i = cacheSize - 1; i >= 0; i--) {
        final ViewHolder holder = mCachedViews.get(i);
        if (holder.getItemId() == id && !holder.isAttachedToTransitionOverlay()) {
            if (type == holder.getItemViewType()) {
                ...
                return holder;
            }
        }
    }
    return null;
}
複製代碼

2.1.4. recyclerPool

複用池中,不一樣的type存儲在不一樣的scrapData對象中,每一個scrapData可緩存的數量上限默認是5個。

從複用池中獲取複用的組件,只須要type匹配,而且這個組件目前沒有綁定到當前RecyclerView便可。

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;
        long mCreateRunningAverageNs = 0;
        long mBindRunningAverageNs = 0;
    }

    SparseArray<ScrapData> mScrap = new SparseArray<>();

    public ViewHolder getRecycledView(int viewType) {
        final RecycledViewPool.ScrapData scrapData = mScrap.get(viewType);
        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.2. 複用

分析一下上一節所說的幾個緩存的入口都在那裏。

2.2.1. scrap

尋找attachScrapchangedScrap的調用鏈,會看到兩個方法都只有一個地方調用add()方法,而且都在scrapView()方法中。在這個方法中,能夠看到判斷條件中,有一個holder.isUpdated(),正如以前所說,changedScrap只在存儲修改的表項。

void scrapView(View view) {
    final ViewHolder holder = getChildViewHolderInt(view);
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
            || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
        ...
        mAttachedScrap.add(holder);
    } else {
        if (mChangedScrap == null) {
            mChangedScrap = new ArrayList<ViewHolder>();
        }
        mChangedScrap.add(holder);
    }
}
複製代碼

繼續往上查看調用scrapView()的地方,會走到LayoutManger.onLayoutChildren()中,在其中先將全部表項回收,而後再調用fill()添加進來,fill()就是分析滾動時候的那個fill()方法。

再接着往上找調用,onLayoutChildren()這個方法也就是RecyclerViewlayout過程當中會調用到的方法,也就是scrap緩存數據的添加是在佈局過程當中。

private void scrapOrRecycleView(Recycler recycler, int index, View view) {
    final ViewHolder viewHolder = getChildViewHolderInt(view);
    if (viewHolder.isInvalid() && !viewHolder.isRemoved()
            && !mRecyclerView.mAdapter.hasStableIds()) {
        ...
    } else {
        detachViewAt(index);
        recycler.scrapView(view);
    }
}


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

public void LinearLayoutManager.onLayoutChildren(Recycler recycler, State state) {
    ...
    detachAndScrapAttachedViews(recycler);
    ...
    fill();
}
複製代碼

再來看一下attachScrapchangedScrapclear()方法的調用鏈,除了一些初始化時的會調用,就只有在佈局最後的調用了,結合前面的內容,肯定了scrap緩存的生命週期只存在於佈局過程。

void clearScrap() {
    mAttachedScrap.clear();
    if (mChangedScrap != null) {
        mChangedScrap.clear();
    }
}

void removeAndRecycleScrapInt(Recycler recycler) {
    ...
    recycler.clearScrap();
}


private void dispatchLayoutStep3() {
    ...
    mLayout.removeAndRecycleScrapInt(mRecycler);
}
複製代碼

2.2.2. cach

接下來看cach緩存是在哪裏添加的,它也只有一個add()調用的地方,在recyclerViewHolderInternal()方法中,在這個方法中能夠看到添加以前對數量進行了判斷,若是已經超過了上限,就會先將其中的第一個表項回收進recyclerPool中。

void recycleViewHolderInternal(ViewHolder holder) {
    ...
    boolean cached = false;
    boolean recycled = false;
    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();
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                recycleCachedViewAt(0);
                cachedViewSize--;
            }
            ...
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
        }
        if (!cached) {
            addViewHolderToRecycledViewPool(holder, true);
            recycled = true;
        }
    }
}

void recycleCachedViewAt(int cachedViewIndex) {
    ...
    ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
    addViewHolderToRecycledViewPool(viewHolder, true);
    mCachedViews.remove(cachedViewIndex);
}
複製代碼

繼續往上尋找調用鏈,就會找到分析滾動時說到的回收方法recyclerView()

public void recycleView(@NonNull View view) {
    ...
    ViewHolder holder = getChildViewHolderInt(view);
    recycleViewHolderInternal(holder);
}
複製代碼

2.2.3. recyclerPool

上面在分析cach緩存的時候,看到回收到recyclerPool的方法addViewHolderToRecycledViewPool(),調用走到了RecyclerPool.putRecycledView()方法,將表項存入對應type的集合之中,若是數量已經到了上限就直接放棄。

void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
    ...
    getRecycledViewPool().putRecycledView(holder);
}


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);
}
複製代碼

2.3. 思考

  1. changeScrap是很特殊的狀況,必須在preLayout步驟,以及必須是被更改的控件,我理解只是對attachedScrap的一個補充,先忽略掉。
  2. attachedScrap的生命週期是在佈局過程當中,先將全部可見的組件進行回收,而後再從新添加。添加的過程當中若是組件的位置以及數據徹底沒有變換,纔會複用,這種狀況下不須要從新加載組件的數據。首次佈局的時候沒有顯示的組件,因此只有在添加、刪除、移動、修改部分組件的時候,請求從新佈局,纔不會將全部顯示的組件標記廢棄。
  3. scrapcatch搜索會有兩種方法,分別是經過positionidid在我理解爲是另外一個惟一性的標識,好比列表數據與數據庫id相綁定,通常狀況下並不會用到,只會用到position搜索。
  4. cachedView做爲recyclerPool的預備隊列,它們的使用卻仍是差了不少,cachedView的使用與attachedScrap比較相似,都是匹配position而且沒有數據的更新纔會複用,這也就是cachedView做爲recyclerPool二者最主要的差距,cachedView複用不須要從新綁定數據,recyclerPool複用須要從新綁定數據。
  5. 對上一條再深刻思考,cachedViewrecyclerPool通常是在滾動的時候回收進來,存放的都是滾出屏幕的表項,而cachedView又只能經過position匹配,也就是說cachedView中表項的複用只在一種狀況下,就是滾出屏幕的表項又從新滾回屏幕。

3. 參考文章

  1. RecyclerView 緩存機制 | 如何複用表項?
  2. RecyclerView 面試題 | 滾動時表項是如何被填充或回收的?
  3. RecyclerView 動畫原理 | pre-layout,post-layout 與 scrap 緩存的關係
  4. RecyclerView緩存機制 | scrap view 的生命週期
  5. RecyclerView的複用緩存機制
  6. RecyclerView的緩存分析
相關文章
相關標籤/搜索