The recycling cache of RecyclerView is completed by the internal class Recycler

1. RecyclerView's third-level cache

Usually there is a four-level cache in RecyclerView, from low to high:

  • directly reusable temporary cache ( mAttachedScrap/ mChangedScrap)

    • mAttachedScrapWhat is cached is the ViewHolder of the visible range in the screen
    • mChangedScrapIt can only be reused in the pre-layout state, because it contains everything that is about to be placed mRecyclerPoolin it Holder, but mAttachedScrapit can be reused in the non-pre-layout state
  • Reusable cache ( mCachedViews): The ViewHolder that will be separated from the RecyclerView when the cache slides, the default is a maximum of 2;

  • Cache() for custom implementations ViewCacheExtension: usually ignored;

  • Cache that needs to rebind data ( RecycledViewPool): ViewHolder cache pool, which can support different ViewTypes, and the returned ViewHolder needs to rebind data;

Since there is no need to customize the cache in most cases, we usually say that RecyclerView has a three-level cache

1.1 PreLayout (PreLayout)

  • Meaning : a layout before the real layout
  • Trigger timing : the pre-layout will only take effect if notifyDataSetChangeda series of notify methods other than this are called on the premise that the data set of the Adapter is updated
  • Function : When the Item is deleted or added, the pre-layout and the real layout will generate different Item snapshots, thereby performing animation according to the two snapshots
  • Example: Delete item2 : First, the pre-layout is to lay out once to form a snapshot ( pre-layout) such as item1234then layout again ( post-layout) to form another snapshot item134so that we actually know the entire animation track and can generate animation

  • How to enable pre-layout : The overridden LayoutManager** supportsPredictiveItemAnimations** method and return truethe three LayoutManagers that come with it have been enabled to achieve this effect

2. Caching mechanism

For convenience, we will not discuss the pre-layout process. First, we need to understand the three-level cache object: mAttachedScrap, mCachedViews, mRecyclerPool, where mAttachedScrapand mCachedViewsare both ArrayList objects, mRecyclerPoolbut RecyclerViewPool objects:

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

    private int mAttachCount = 0;

    /**
     * Acquire a ViewHolder of the specified type from the pool, or {@code null} if none are
     * present.
     *
     * @param viewType ViewHolder type.
     * @return ViewHolder of the specified type acquired from the pool, or {@code null} if none
     * are present.
     */
    @Nullable
    public ViewHolder getRecycledView(int viewType) {
        final 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;
    }
    
    ...

It can be found

  • RecyclerViewPoolThere is an SparseArray<ScrapData>object in the object, and ScrapDatathe object mScrapHeapis ViewHoldercachedArrayList

  • When obtained RecyclerViewPoolfrom it ViewHolder, it is through list.remove(index)the method

2.1 The starting point of the caching mechanism: tryGetViewHolderForPositionByDeadline

When RecyclerView draws, it will go to the method LayoutManagerinside next(). In next(), the caching mechanism is officially started. Here LinearLayoutManageris an example:

onLayoutChildren -> fill -> layoutChunk -> LayoutState.next

//com.android.internal.widget.LinearLayoutManager.LayoutState#next
View next(RecyclerView.Recycler recycler) {
    if (mScrapList != null) {
        return nextViewFromScrapList();
    }
    final View view = recycler.getViewForPosition(mCurrentPosition);
    mCurrentPosition += mItemDirection;
    return view;
}

And getViewForPositionwill call tryGetViewHolderForPositionByDeadlinethe method:

//com.android.internal.widget.RecyclerView.Recycler#tryGetViewHolderForPositionByDeadline
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    if (position < 0 || position >= mState.getItemCount()) {
        throw new IndexOutOfBoundsException("Invalid item position " + position
                + "(" + position + "). Item count:" + mState.getItemCount());
    }
    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) {
            if (!validateViewHolderForOffsetPosition(holder)) {
                // recycle holder (and unscrap if relevant) since it can't be used
                if (!dryRun) {
                    // we would like to recycle this but need to make sure it is not used by
                    // animation logic etc.
                    holder.addFlags(ViewHolder.FLAG_INVALID);
                    if (holder.isScrap()) {
                        removeDetachedView(holder.itemView, false);
                        holder.unScrap();
                    } else if (holder.wasReturnedFromScrap()) {
                        holder.clearReturnedFromScrapFlag();
                    }
                    recycleViewHolderInternal(holder);
                }
                holder = null;
            } else {
                fromScrapOrHiddenOrCache = true;
            }
        }
    }
    if (holder == null) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
            throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
                    + "position " + position + "(offset:" + offsetPosition + ")."
                    + "state:" + mState.getItemCount());
        }

        final int type = mAdapter.getItemViewType(offsetPosition);
        // 2) Find from scrap/cache via stable ids, if exists
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
            if (holder != null) {
                // update position
                holder.mPosition = offsetPosition;
                fromScrapOrHiddenOrCache = true;
            }
        }
        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);
                if (holder == null) {
                    throw new IllegalArgumentException("getViewForPositionAndType returned"
                            + " a view which does not have a ViewHolder");
                } else if (holder.shouldIgnore()) {
                    throw new IllegalArgumentException("getViewForPositionAndType returned"
                            + " a view that is ignored. You must call stopIgnoring before"
                            + " returning this view.");
                }
            }
        }
        if (holder == null) { // fallback to pool
            if (DEBUG) {
                Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
                        + position + ") fetching from shared pool");
            }
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                holder.resetInternal();
                if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
                }
            }
        }
        if (holder == null) {
            long start = getNanoTime();
            if (deadlineNs != FOREVER_NS
                    && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
                // abort - we have a deadline we can't meet
                return null;
            }
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
            if (ALLOW_THREAD_GAP_WORK) {
                // only bother finding nested RV if prefetching
                RecyclerView innerView = findNestedRecyclerView(holder.itemView);
                if (innerView != null) {
                    holder.mNestedRecyclerView = new WeakReference<>(innerView);
                }
            }

            long end = getNanoTime();
            mRecyclerPool.factorInCreateTime(type, end - start);
            if (DEBUG) {
                Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
            }
        }
    }

    // This is very ugly but the only place we can grab this information
    // before the View is rebound and returned to the LayoutManager for post layout ops.
    // We don't need this in pre-layout since the VH is not updated by the LM.
    if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder
            .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
        holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
        if (mState.mRunSimpleAnimations) {
            int changeFlags = ItemAnimator
                    .buildAdapterChangeFlagsForAnimations(holder);
            changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
            final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
                    holder, changeFlags, holder.getUnmodifiedPayloads());
            recordAnimationInfoIfBouncedHiddenView(holder, info);
        }
    }

    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        // do not update unless we absolutely have to.
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        if (DEBUG && holder.isRemoved()) {
            throw new IllegalStateException("Removed holder should be bound and it should"
                    + " come here only in pre-layout. Holder: " + holder);
        }
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }

    final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
    final LayoutParams rvLayoutParams;
    if (lp == null) {
        rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
        holder.itemView.setLayoutParams(rvLayoutParams);
    } else if (!checkLayoutParams(lp)) {
        rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
        holder.itemView.setLayoutParams(rvLayoutParams);
    } else {
        rvLayoutParams = (LayoutParams) lp;
    }
    rvLayoutParams.mViewHolder = holder;
    rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
    return holder;
}

2.2 Get View Holder from mAttachedScrap and mCachedViews according to the location

  • 2.2.1 getScrapOrHiddenOrCachedHolderForPosition

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

    // Try first for an exact, non-invalid match from scrap.
    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) {
           ...
           // 可忽略
            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);
        // invalid view holders may be in cache if adapter has stable ids as they can be
        // retrieved via getScrapOrCachedViewForId
        if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
            if (!dryRun) {
                mCachedViews.remove(i);
            }
            if (DEBUG) {
                Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
                        + ") found match in cache: " + holder);
            }
            return holder;
        }
    }
    return null;
}
  • The first step is mAttachedScrapto get it first viewHolder: if you can get the specified one viewHolder, then judge:

    • if viewHoldernot returned Scrapfrom (flags not included FLAG_RETURNED_FROM_SCRAP);
    • viewHolder.mPosition == position
    • viewHolderto be effective
    • is pre-layout or viewHolderthe state is notremoved
  • If the first step does not get a valid one viewHolder, the second step mChildHelperis to get the View in the Hidden state, but usually there is no View in the Hidden state in the RecyclerView, because when addingView, the hide value is set to false

  • If no valid viewHolder is obtained in the previous two steps, the viewHolder will be obtained mCachedViewfrom it . Since the value of the dryRun parameter defaults to false, the viewHolder taken out will be removed.

2.1.2 Check if viewHolder is valid

#validateViewHolderForOffsetPosition:

boolean validateViewHolderForOffsetPosition(ViewHolder holder) {
    // if it is a removed holder, nothing to verify since we cannot ask adapter anymore
    // if it is not removed, verify the type and id.
    if (holder.isRemoved()) {
        if (DEBUG && !mState.isPreLayout()) {
            throw new IllegalStateException("should not receive a removed view unless it"
                    + " is pre layout");
        }
        return mState.isPreLayout();
    }
    if (holder.mPosition < 0 || holder.mPosition >= mAdapter.getItemCount()) {
        throw new IndexOutOfBoundsException("Inconsistency detected. Invalid view holder "
                + "adapter position" + holder);
    }
    if (!mState.isPreLayout()) {
        // don't check type if it is pre-layout.
        final int type = mAdapter.getItemViewType(holder.mPosition);
        if (type != holder.getItemViewType()) {
            return false;
        }
    }
    if (mAdapter.hasStableIds()) {
        return holder.getItemId() == mAdapter.getItemId(holder.mPosition);
    }
    return true;
}

If step 1 returns null, then step 2.3 will be performed:

2.3 Get viewHolder from mAttachedScrap and mCachedViews according to Id

// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) {
    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
            type, dryRun);
    if (holder != null) {
        // update position
        holder.mPosition = offsetPosition;
        fromScrapOrHiddenOrCache = true;
    }
}
  • It can be seen that when the Adapter has a fixed ID, we can get the viewHolder according to the ID, but by default, we need to perform the following two steps to take effect:
    • set upApapter.setHasStableIds(true)

    • override Adapter.getItemId(position)method

//com.android.internal.widget.RecyclerView.Recycler#getScrapOrCachedViewForId
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);
                if (holder.isRemoved()) {
                    // this might be valid in two cases:
                    // > item is removed but we are in pre-layout pass
                    // >> do nothing. return as is. make sure we don't rebind
                    // > item is removed then added to another position and we are in
                    // post layout.
                    // >> remove removed and invalid flags, add update flag to rebind
                    // because item was invisible to us and we don't know what happened in
                    // between.
                    if (!mState.isPreLayout()) {
                        holder.setFlags(ViewHolder.FLAG_UPDATE, ViewHolder.FLAG_UPDATE
                                | ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED);
                    }
                }
                return holder;
            } else if (!dryRun) {
                // if we are running animations, it is actually better to keep it in scrap
                // but this would force layout manager to lay it out which would be bad.
                // Recycle this scrap. Type mismatch.
                mAttachedScrap.remove(i);
                removeDetachedView(holder.itemView, false);
                quickRecycleScrapView(holder.itemView);
            }
        }
    }

    // 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) {
            if (type == holder.getItemViewType()) {
                if (!dryRun) {
                    mCachedViews.remove(i);
                }
                return holder;
            } else if (!dryRun) {
                recycleCachedViewAt(i);
                return null;
            }
        }
    }
    return null;
}

According to the obtained Idfrom mAttachedScrapand , if obtained and consistent, return directlymCachedViewsviewHolderItemViewType

2.4 Get viewHolder from mRecyclerPool

//3.1 获取viewHolder
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
    // 3.2 重置viewHolder
    holder.resetInternal();
    if (FORCE_INVALIDATE_DISPLAY_LIST) {
        invalidateDisplayListInt(holder);
    }
}

2.4.1 Get viewHolder: getRecycledView

//com.android.internal.widget.RecyclerView.RecycledViewPool#getRecycledView
public ViewHolder getRecycledView(int viewType) {
// 根据viewType获取对应的viewHolder缓存
    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;
}

2.4.2 Reset viewHolder state

void resetInternal() {
    mFlags = 0;
    mPosition = NO_POSITION;
    mOldPosition = NO_POSITION;
    mItemId = NO_ID;
    mPreLayoutPosition = NO_POSITION;
    mIsRecyclableCount = 0;
    mShadowedHolder = null;
    mShadowingHolder = null;
    clearPayload();
    mWasImportantForAccessibilityBeforeHidden = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
    mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
    clearNestedRecyclerViewIfNotNested(this);
}

2.5 Create a new viewHolder

If the holder obtained in the first three steps is empty, you need to create a new viewHolder:

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

2.6 Determine whether to call bindViewHolder according to the state of viewHolder


if (mState.isPreLayout() && holder.isBound()) {
    // do not update unless we absolutely have to.
    holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
    if (DEBUG && holder.isRemoved()) {
        throw new IllegalStateException("Removed holder should be bound and it should"
                + " come here only in pre-layout. Holder: " + holder);
    }
    final int offsetPosition = mAdapterHelper.findPositionOffset(position);
    // 重写bindView
    bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}

private boolean tryBindViewHolderByDeadline(ViewHolder holder, int offsetPosition,
        int position, long deadlineNs) {
    holder.mOwnerRecyclerView = RecyclerView.this;
    final int viewType = holder.getItemViewType();
    long startBindNs = getNanoTime();
    if (deadlineNs != FOREVER_NS
            && !mRecyclerPool.willBindInTime(viewType, startBindNs, deadlineNs)) {
        // abort - we have a deadline we can't meet
        return false;
    }
    // 熟悉的bindViewHolder
    mAdapter.bindViewHolder(holder, offsetPosition);
    long endBindNs = getNanoTime();
    mRecyclerPool.factorInBindTime(holder.getItemViewType(), endBindNs - startBindNs);
    attachAccessibilityDelegate(holder.itemView);
    if (mState.isPreLayout()) {
        holder.mPreLayoutPosition = position;
    }
    return true;
}
  1. recycling mechanism

3.1 Recovery methods

LayoutManager provides various recycling methods:

detachAndScrapView(View child, Recycler recycler)
detachAndScrapViewAt(int index, Recycler recycler)
detachAndScrapAttachedViews(Recycler recycler)


removeAndRecycleView(View child, Recycler recycler)
removeAndRecycleViewAt(int index, Recycler recycler)
removeAndRecycleAllViews(Recycler recycler)
  • The first three methods are responsible for reclaiming the View to the first-level cache ( Recycler.mAttachedMap), and the first-level cache is just a temporary cache. When it is used for initialization or when the data set changes, all Views are temporarily placed in the cache, that is, only in the layout ( onLayoutChildren ) will be called when **** is called , and there is no place to call).detachAndScrapAttachedViews detachAndScrapView/detachAndScrapViewAt

  • The last three methods are responsible for reclaiming the View to the second-level cache ( mCachedViews) or the fourth-level cache ( RecyclerViewPool), mCachedViewswith a default size of 2 (but currently there are mPrefetchMaxCountObservedparameters with a value of 1, so mCachedViewsthe size may be 3)

3.2 Recycling Timing

3.2.1 When to Recycle to Level 1 Cache

Only when the Adapter data set changes, various notify methods are called, so the View on the screen will be recycled to the first-level cache ( mAttachedMap) after re-layout. At this time, the onLayoutChildreninitiator will call the detachAndScrapAttachedViews method, and then call scrapOrRecycleViewthe method:

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

When the viewHolder satisfies invalid/not removed/Adapter does not exist stableIds, it will be recycled to the second-level cache or the fourth-level cache, otherwise it will be recycled to the first-level cache

3.2.2 When to recycle to the second-level cache or the fourth-level cache

  1. When the data set changes , call LayoutManage.onLayoutChildren-> LayoutManage.detachAndScrapAttachedViews-> recycler.recycleViewHolderInternalto recycle;
  2. When the item slides out of the screen , call scrollBy-> fill-> recycleByLayoutStateto recycle

Since the final calls in the above two cases overlap, we will directly use the second case to illustrate:

The overall timing diagram is as follows :

When the user slides the list, LinearLayoutManage.scrollBythe function will be used:

int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
   ...
    updateLayoutState(layoutDirection, absDelta, true, state);
    // 关键点:调用fill函数
    final int consumed = mLayoutState.mScrollingOffset
            + fill(recycler, mLayoutState, state, false);
    ...
    mLayoutState.mLastScrollDelta = scrolled;
    return scrolled;
}

Continue to dig into fillthe function:

//androidx.recyclerview.widget.LinearLayoutManager#fill

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
   
    if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
        // TODO ugly bug fix. should not happen
        if (layoutState.mAvailable < 0) {
            layoutState.mScrollingOffset += layoutState.mAvailable;
        }
        // 1. 回收到二级缓存或四级缓存的起点
        recycleByLayoutState(recycler, layoutState);
    }
    int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        layoutChunkResult.resetInternal();
        ...
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
        ...
        layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
        
        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;
}

#recycleByLayoutState

private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
    if (!layoutState.mRecycle || layoutState.mInfinite) {
        return;
    }
    int scrollingOffset = layoutState.mScrollingOffset;
    int noRecycleSpace = layoutState.mNoRecycleSpace;
    if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
        recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);
    } else {
        recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
    }
}

According to the direction of the slide, choose to call recycleViewsFromEndor not recycleViewsFromStart, and the final calling method is the same. Let's take as recycleViewsFromEndan example to illustrate

// androidx.recyclerview.widget.LinearLayoutManager#recycleViewsFromEnd
private void recycleViewsFromEnd(RecyclerView.Recycler recycler, int scrollingOffset,
        int noRecycleSpace) {
    final int childCount = getChildCount();
   if (mShouldReverseLayout) {
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (mOrientationHelper.getDecoratedStart(child) < limit
                    || mOrientationHelper.getTransformedStartWithDecoration(child) < limit) {
                // stop here
                recycleChildren(recycler, 0, i);
                return;
            }
        }
    } else {
        for (int i = childCount - 1; i >= 0; i--) {
            View child = getChildAt(i);
            if (mOrientationHelper.getDecoratedStart(child) < limit
                    || mOrientationHelper.getTransformedStartWithDecoration(child) < limit) {
                // stop here
                recycleChildren(recycler, childCount - 1, i);
                return;
            }
        }
    }
}

No matter mShouldReverseLayoutwhat the value is (usually false), it will be called recycleChildren:

private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
    if (startIndex == endIndex) {
        return;
    }
    if (DEBUG) {
        Log.d(TAG, "Recycling " + Math.abs(startIndex - endIndex) + " items");
    }
    if (endIndex > startIndex) {
        for (int i = endIndex - 1; i >= startIndex; i--) {
            removeAndRecycleViewAt(i, recycler);
        }
    } else {
        for (int i = startIndex; i > endIndex; i--) {
            removeAndRecycleViewAt(i, recycler);
        }
    }
}

So far, we have seen one of the recycling methods mentioned at the beginning: removeAndRecycleViewAt, and then the method will be called Recycler.recyclerView:

public void recycleView(@NonNull View view) {
    // This public recycle method tries to make view recycle-able since layout manager
    // intended to recycle this view (e.g. even if it is in scrap or change cache)
    ViewHolder holder = getChildViewHolderInt(view);
    if (holder.isTmpDetached()) {
        removeDetachedView(view, false);
    }
    if (holder.isScrap()) {
        holder.unScrap();
    } else if (holder.wasReturnedFromScrap()) {
        holder.clearReturnedFromScrapFlag();
    }
    recycleViewHolderInternal(holder);
   
}

Then call Recyclerthe internal recycleViewHolderInternalmethod:

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)) {
            // Retire oldest cached view
            int cachedViewSize = mCachedViews.size();
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                // mCachedViews中满了之后,调用addViewHolderToRecycledViewPool,将第一个ViewHolder放入RecycledViewPool
                recycleCachedViewAt(0);
                cachedViewSize--;
            }

            int targetCacheIndex = cachedViewSize;
            if (ALLOW_THREAD_GAP_WORK
                    && cachedViewSize > 0
                    && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                // when adding the view, skip past most recently prefetched views
                int cacheIndex = cachedViewSize - 1;
                while (cacheIndex >= 0) {
                    int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                    if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                        break;
                    }
                    cacheIndex--;
                }
                targetCacheIndex = cacheIndex + 1;
            }
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
        }
        if (!cached) {
        
            addViewHolderToRecycledViewPool(holder, true);
            recycled = true;
        }
}

Usually it will viewHolderbe added to the second-level cache mCachedViews, mCachedViewsand when it is full, the first passing function will be viewHolderadded addViewHolderToRecycledViewPoolto the fourth-level cacheRecycledViewPool

Guess you like

Origin blog.csdn.net/maniuT/article/details/131095250