The use of RecyclerView scenarios are very rich, and this source code analysis based on the sliding up and down a list of scenarios to observe its reuse – recycling mechanism. This article is analyzed based on 27.0.0 version, as shown in the Demo below:

RecyclerView inherited from ViewGroup, belongs to the system level custom control, and its source code is more than 12000 lines, not including extraction of other auxiliary classes, management classes, etc., can be imagined its complexity, the analysis of this paper is mainly focused on the RecyclerView cache mechanism, Through sliding events combined with source code analysis of its reuse and recycling mechanism, and RecyclerView drawing process, ItemDecoration, LayoutManager, State, Recycler and so on.

Custom control trilogy: onMeasure – onLayout – onDraw, RecyclerView is no exception. View the source code can see that part of the logic of RecyclerView measurement is entrusted to LayoutManager, the source code is shown below, to determine whether there is a LayoutManager instance, if not, call defaultOnMeasure for default measurement. And then there’s an if… else… Determine whether AutoMeasure, LinearLayoutManager and GridLayoutManager use this mode, while StaggerLayoutManager will use custom measurement mode under certain conditions.

@Override
protected void onMeasure(int widthSpec, int heightSpec) {
    if (mLayout == null) {
        defaultOnMeasure(widthSpec, heightSpec);
        return;
    }
    //LinearLayoutManager and GridLayoutManager use this pattern
    if(mLayout.mAutoMeasure) { ... mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); . }else {
        if (mHasFixedSize) {
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            return; }...StaggerLayoutManager will use custom measurements under certain conditions}}Copy the code

OnLayout will be executed after the measurement. Here we analyze the LinearLayoutManager with vertical layout, and the layout logic will go through the following three methods: DispatchLayoutStep1 – dispatchLayoutStep2 – dispatchLayoutStep3.

DispatchLayoutStep1: deal with the renewal of the Adapter and animation related dispatchLayoutStep2: real execution LayoutManager. OnLayoutChildren, The implementation of this function determines how ChildView will be handled by layout dispatchLayoutStep3: Save animation-related information and do the necessary cleanup

So we focus on LayoutManager. OnLayoutChildren, directly into the LinearLayoutManager onLayoutChildren, found that the code is very long, there are annotation information, the layout of the logic is as follows: 1. First look for the anchor point. 2. Start from the anchor point and fill the bottom up and the top down. The following LinearLayoutManager works with the vertical layout onLayout snippet:

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {... ensureLayoutState(); mLayoutState.mRecycle =false;
    // Determine the layout direction
    resolveShouldLayoutReverse();

    // find the anchor point
    final View focused = getFocusedChild();
    if(! mAnchorInfo.mValid || mPendingScrollPosition ! = NO_POSITION || mPendingSavedState ! =null) {
        mAnchorInfo.reset();
        mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
        // Calculate the position and coordinates of the anchor points
        updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
        mAnchorInfo.mValid = true; }... detachAndScrapAttachedViews(recycler);/ / recycling view
    
    // Below is the code for the LinearLayoutManager with the vertical layout
    // Draw down first
    updateLayoutStateToFillEnd(mAnchorInfo);
    / / fill the view
    fill(recycler, mLayoutState, state, false); .// Draw up again
    updateLayoutStateToFillStart(mAnchorInfo);
    / / fill the view
    fill(recycler, mLayoutState, state, false);

    // There is still space available
    if (mLayoutState.mAvailable > 0) {...// Repopulate the view
        fill(recycler, mLayoutState, state, false); }... }Copy the code

So far we have a general understanding of the layout algorithm logic: first find the anchor point and then fill in different directions for many times, and RecyclerView reuse process and recycling process are initiated in this method, so onLayout is the entrance to analyze the cache mechanism. The reuse process is the fill, recycling process is detachAndScrapAttachedViews. Here we summarize the content of onMeasure and onLayout:

RecyclerView is to hand over the drawing process to LayoutManager. If there is no setting, sub-View 2 will not be measured. 3. Anchor points are first determined and then filled in different directions for many times. Fill () will be executed at least twice. 4.LayoutManager obtains the View from onLayout in RecyclerView (fill), which is related to the cache strategy of RecyclerView. If you do not get the cache, I’m going to go onCreateView method that we rewrote ourselves, Call again onBindViewHolder 5. LayoutManager recycling View entry is RecyclerView began the onLayout (detachAndScrapAttachedViews), RecyclerView cache strategy is involved

The reuse process and recycle process will be analyzed in detail below, where the entry point of the process is the onLayout method. The onDraw code is no longer analyzed here. Here according to the execution order of the source code will be recycled and reused first, so the following analysis of the recycling process.

1. Recycling process

The entry of the recycling process method is LinearLayoutManager – onLayoutChildren – detachAndScrapAttachedViews – scrapOrRecycleView, finally a method name translation is: Scrap or RecycleView; scrapOrRecycleView; scrapOrRecycleView;

private void scrapOrRecycleView(Recycler recycler, int index, View view) {
    // Get the viewHolder from the specified view
    final ViewHolder viewHolder = getChildViewHolderInt(view);
    if (viewHolder.shouldIgnore()) {
        if (DEBUG) {
            Log.d(TAG, "ignoring view " + viewHolder);
        }
        return;
    }
    //viewHolder is invalid, has not been removed, and has no specified stableId
    if(viewHolder.isInvalid() && ! viewHolder.isRemoved() && ! mRecyclerView.mAdapter.hasStableIds()) {/ / remove the
        removeViewAt(index);
        // Recycle internal process through Recycler, mainly to recycle older into RecycledViewPool
        recycler.recycleViewHolderInternal(viewHolder);
    } else {
        / / detach the
        detachViewAt(index);
        // Remove view from the Scrap array by recyclerrecycler.scrapView(view); mRecyclerView.mViewInfoStore.onViewDetached(viewHolder); }}Copy the code

Two important concepts emerge from the above code: remove and detach.

detach: Remove ChildView from ParentView’s ChildView array. The mParent of ChildView is set to NULL. Because the View is still disconnected from the View tree, this function is often used to change the order of childViews in the ChildView array. View detach is usually temporary and will be reattached later.

Remove: real removed, not only be deleted from ChildView array, contact the View tree each other will be cut off completely (regardless of the Animation/LayoutTransition this special circumstances), such as focus is cleared, removed from TouchTarget etc.

So we can match scrapOrRecycleView to scraper-detach, recycler-remove. If the following three conditions are met either recycler or viewHolder is recycled:

3. The Adapter does not specify a stableId, because if you specify a stableId, there is no possibility that the contents of the View binding will be invalid

In the case of practice, sliding up and down can trigger recycler; Scrap is triggered when an element is inserted or deleted, or essentially notifyDataSetChanged is called.

We can recycle recycler internally. The general logic is to identify some markers of The Recycler and cache the Recycler into mCachedViews if the Recycler is full. Delete the oldest element in mCachedViews and add it to RecycledViewPool. Next, place the elements to be recycled into mCachedViews. If the condition is not met, place the viewHolder directly into the RecycledViewPool. The following code is the edited source code, which describes the above logic:

void recycleViewHolderInternal(ViewHolder holder) {
    // Perform the necessary validation, otherwise an exception is thrown
    if(holder.isScrap() || holder.itemView.getParent() ! =null) {
        throw new IllegalArgumentException(
                "Scrapped or attached views may not be recycled. isScrap:"
                        + holder.isScrap() + " isAttached:"+ (holder.itemView.getParent() ! =null) + exceptionLabel());
    }

    // Perform the necessary validation, otherwise an exception is thrown
    if (holder.isTmpDetached()) {
        throw new IllegalArgumentException("Tmp detached view should be removed "
                + "from RecyclerView before it can be recycled: " + holder
                + exceptionLabel());
    }

    // Perform the necessary validation, otherwise an exception is thrown
    if (holder.shouldIgnore()) {
        throw new IllegalArgumentException("Trying to recycle an ignored view holder. You"
                + " should first call stopIgnoringView(view) before calling recycle."+ exceptionLabel()); }...if (forceRecycle || holder.isRecyclable()) {
        // Check valid conditions
        if (mViewCacheMax > 0
                && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                | ViewHolder.FLAG_REMOVED
                | ViewHolder.FLAG_UPDATE
                | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
            int cachedViewSize = mCachedViews.size();
            // Check whether cachedViewSize is greater than the maximum cache size
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                // Reclaim the oldest element, element 0
                recycleCachedViewAt(0);
                cachedViewSize--;
            }

            int targetCacheIndex = cachedViewSize;
            if (ALLOW_THREAD_GAP_WORK
                    && cachedViewSize > 0
                    && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                int cacheIndex = cachedViewSize - 1;
                while (cacheIndex >= 0) {
                    int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                    if(! mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {break;
                    }
                    cacheIndex--;
                }
                // Calculate the index value of the cached element
                targetCacheIndex = cacheIndex + 1;
            }
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
        }
        // Add RecycledViewPool to RecycledViewPool
        if(! cached) { addViewHolderToRecycledViewPool(holder,true);
            recycled = true; }}... }Copy the code

According to the source, we debug method invocation path is: recycleViewHolderInternal recycleCachedViewAt — addViewHolderToRecycledViewPool. In the addViewHolderToRecycledViewPool is below the critical source code, the element into RecycledViewPool, you can see here to distinguish between the type, each type corresponds to an ArrayList, The viewHolder that enters here will be reset, primarily position and flags.

public void putRecycledView(ViewHolder scrap) {
    //拿到type
    final int viewType = scrap.getItemViewType();
    // Get the ViewHolder set for type
    final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
    if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
        return;
    }
    if (DEBUG && scrapHeap.contains(scrap)) {
        throw new IllegalArgumentException("this scrap item already exists");
    }
    / / reset viewHolder
    scrap.resetInternal();
    // Add viewHolder to the collection
    scrapHeap.add(scrap);
}
Copy the code

After the recycler process is recyclable, there is another recycler scenario, which involves more global variables such as mChildHelper, mAttachedScrap, and mChangedScrap. First, mAttachedScrap and mChangedScrap are both ArrayList cache viewHolder variables. MChildHelper is an instance of ChildHelper, and RecyclerView is a ViewGroup, but it gives ChildHelper carte te to manage ChildView. All operations on ChildView are performed indirectly through ChildHelper, which becomes the middle layer of a ChildView operation. GetChildCount /getChildAt and other functions are processed by ChildHelper and then sent to the corresponding function of RecyclerView. Its parameters or return results will be modified according to the actual ChildView information. Detach to scrapOrRecycleView Detach to scrapOrRecycleView detach to scrapOrRecycleView

//detach the view with index subscript
detachViewAt(index);
// Maintain this scrapView in recycler
recycler.scrapView(view);
Copy the code

In detachViewAt, mChildHelper handles the relationship between view and parentView. While in scrapView, by judging whether the viewHolder was removed, is invalid, is to determine whether canReuseUpdatedViewHolder conditions into mAttachedScrap or mChangedScrap. The source code is as follows:

void scrapView(View view) {
    / / get the viewHolder
    final ViewHolder holder = getChildViewHolderInt(view);
    / / will be removed, or invalid, or canReuseUpdatedViewHolder
    if(holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID) || ! holder.isUpdated() || canReuseUpdatedViewHolder(holder)) { ... holder.setScrapContainer(this.false);
        // Put it into mAttachedScrap
        mAttachedScrap.add(holder);
    } else {
        if (mChangedScrap == null) {
            mChangedScrap = new ArrayList<ViewHolder>();
        }
        holder.setScrapContainer(this.true);
        // Otherwise put it into mChangedScrapmChangedScrap.add(holder); }}Copy the code

Detach views can be reused directly (the ViewHolder corresponding to the Data Position is still valid, You just need to rebind the data, or even not rebind the data if it hasn’t changed either; The View is still valid. The data bound to the View may be valid. For example, if one of the N items in the list is deleted, the ViewHolder corresponding to the remaining N-1 items can be reused without any other changes. Unnecessary binding is avoided (compared to, say, ListView), and the granularity of item processing is refined from the whole to individual items, including reuse of the View as well as the current binding content of the View. The removed View is less reusable because its Position is invalid. This reuse level is only the View level compared to Scrap level (a ViewHolder can be reused, but the information in the ViewHolder needs to be reset, but at least a new one is not needed).

So far, the process of recycling mechanism is basically completed. To review, first of all, in the onLayout method of RecyclerView, the right of layout will be transferred to LayoutManger in dispatchLayoutStep2, which corresponds to the LinearLayoutManager in the Demo. OnLayoutChildren LinearLayoutManager take over after the call itself, then the view for recycling (detachAndScrapAttachedViews) and filling (fill). According to certain conditions in detachAndScrapAttachedViews decided the view was scrap (corresponding to detach) was recycler (corresponding to remove). Recycler views can be recycled through mCachedViews and RecyclerViewPool, and recycled elements can be recycled into mAttachedScrap or mChangedScrap.

2. Reuse processes

According to the execution sequence of the onLayoutChildren code in the LinearLayoutManager, the recycling mechanism process is analyzed first. Then the reuse mechanism process is analyzed. Following the above idea, the entry method is determined first, and then a method call process is determined, and then the detailed analysis is performed. The LinearLayoutManager matches the onLayout segment of the vertical layout to find the anchor point, draw down and fill again, draw up and fill again. The fill method is the entry method to analyze the reuse mechanism.

// Below is the code for the LinearLayoutManager with the vertical layout
// Draw down first
updateLayoutStateToFillEnd(mAnchorInfo);
/ / fill the view
fill(recycler, mLayoutState, state, false); .// Draw up again
updateLayoutStateToFillStart(mAnchorInfo);
/ / fill the view
fill(recycler, mLayoutState, state, false);

// There is still space available
if (mLayoutState.mAvailable > 0) {...// Repopulate the view
    fill(recycler, mLayoutState, state, false);
}
Copy the code

After entering fill, the layoutChunk method is looped based on whether there are more items to be filled in layoutState. The layoutChunk method is supposed to be used to layout blocks based on the name of layoutChunk. Each piece corresponds to an item. In layoutChunk, the view is first found through the next method, and then the view is remeasured and laid out, as well as the border is determined. The focus of this article is on caching, so we’ll skip over drawing layouts and focus on the Next method. Here is the source code for the next method:

View next(RecyclerView.Recycler recycler) {
    if(mScrapList ! =null) {
        return nextViewFromScrapList();
    }
    // Find a suitable view through recycler objects
    final View view = recycler.getViewForPosition(mCurrentPosition);
    mCurrentPosition += mItemDirection;
    return view;
}
Copy the code

The code above the key sentence is recycler. GetViewForPosition (mCurrentPosition), through the given position to obtain an instance of the view object, May end get a viewHolder tryGetViewHolderForPositionByDeadline method, and then through the inside of the viewHolder itemView attribute will view instance object returned. The source code is as follows:

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

View getViewForPosition(int position, boolean dryRun) {
    // Get the viewHolder and the itemView property to get the view instance
    return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
Copy the code

Recycler can’t be used by View directly, it can be used by ViewHolder. If you have your own debug code and look at the relationship between the View and viewHolder, you will see that they are bidirectionally bound. The View holds the viewHolder via the mViewHolder property of LayoutParams. The viewHolder holds the view through the itemView property. In tryGetViewHolderForPositionByDeadline overall train of thought is, in turn, after RecyclerView of 4 class cache, cascaded find, found it returns viewHolder, if not, Call back the user’s onCreateViewHolder and onBindViewHolder. RecyclerView four levels of cache more detailed say should be Recycler four levels of cache, respectively: mAttachedScrap – mCachedViews – mViewCacheExtension – mRecyclerPool.

MAttachedScrap: Saves the Scrap View in mAttachedScrap or mChangedScrap for quick reuse of in-screen itemView.

MCachedViews: corresponds to remove Views in the reclamation mechanism. By default, two remove Views are available.

MViewCacheExtension: a user extension that gives users control over the cache.

MRecyclerPool: Corresponding to the view overflow after remove View is put into mCachedViews in the recycle mechanism above, and can also use the cache pool shared with RecyclerView ViewHolder.

To understand the above four cached, then see tryGetViewHolderForPositionByDeadline code will be a lot easier, the following source:

@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {... ViewHolder holder =null;
    
    // 1) Find by position from scrap/hidden list/cache
    if (holder == null) {
        // Find viewHolder from Scrap or hidden or cache
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        if(holder ! =null) {
            // Check if the holder found can be used by the current location. If not, reclaim the viewHolder
            if(! validateViewHolderForOffsetPosition(holder)) {// dryRun is usually false, indicating that it can be removed from scrap or cache
                if(! dryRun) { holder.addFlags(ViewHolder.FLAG_INVALID);if (holder.isScrap()) {
                        removeDetachedView(holder.itemView, false);
                        holder.unScrap();
                    } else if (holder.wasReturnedFromScrap()) {
                        holder.clearReturnedFromScrapFlag();
                    }
                    // Execute internal recycle logic
                    recycleViewHolderInternal(holder);
                }
                holder = null;
            } else {
                fromScrapOrHiddenOrCache = true; }}}if (holder == null) {.../ / get the type
        final int type = mAdapter.getItemViewType(offsetPosition);
        // 2) Find from scrap/cache via stable ids, if exists
        if (mAdapter.hasStableIds()) {
            // If stableId is set, try to get it from scrap or cache
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
            if(holder ! =null) {
                // update position
                holder.mPosition = offsetPosition;
                fromScrapOrHiddenOrCache = true; }}// Determine whether external extensions are set
        if (holder == null&& mViewCacheExtension ! =null) {
            // Look for external extensions
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if(view ! =null) { holder = getChildViewHolder(view); . }}if (holder == null) { // fallback to pool.// Select RecycledViewPool as required by tYEPholder = getRecycledViewPool().getRecycledView(type); . }if (holder == null) {
            // Call back the user's onCreateViewHolder method
            holder = mAdapter.createViewHolder(RecyclerView.this, type); . }}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()) { ...final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        // Call back the user's onBindViewHolder method
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }

    / / get LayoutParams
    final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
    final LayoutParams rvLayoutParams;
    if (lp == null) {
        // Convert LayoutParams to RecyclerView
        rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
        holder.itemView.setLayoutParams(rvLayoutParams);
    } else if(! checkLayoutParams(lp)) { rvLayoutParams = (LayoutParams) generateLayoutParams(lp); holder.itemView.setLayoutParams(rvLayoutParams); }else {
        rvLayoutParams = (LayoutParams) lp;
    }
    // Save the viewHolder to the mViewHolder property
    rvLayoutParams.mViewHolder = holder;
    rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
    return holder;
}
Copy the code

I deleted the above source code and retained the core process code. From the top, I searched from the four-level cache one by one. If there was no one, I created one.

At this point, the process of reuse mechanism is basically completed. To sum up, the method invocation process is as follows: LinearLayoutManager – onLayoutChildren – fill() – layoutChunk() – layoutState.next() – getViewForPosition() – TryGetViewHolderForPositionByDeadline () – level cache or onCreateViewHolder – onBindViewHolder (without binding triggers bind callback). There are two things to look at in this process. One is the fill method, which is called multiple times; One is tryGetViewHolderForPositionByDeadline, involves RecyclerView multiplexing mechanism inside the core logic: four levels of cache.

3. Advantages of RecyclerView

3.1 comparison between RecyclerView and ListView

RecyclerView enforces the Use of a ViewHolder by RecyclerView. Use a custom ViewHolder to create a ListView and use findViewById to create a ListView. However, RecyclerView defines many flags to indicate the availability state of the current ViewHolder based on the ViewHolder, which is richer than the custom ViewHolder in ListView.

When dealing with the off-screen cache, RecyclerView is very different from ListView. RecyclerView obtains a viewHolder from mCachedViews, and then determines whether the viewHolder is bound, whether it does not need to be updated, and whether it is valid. If any of the conditions is met, onBindViewHolder will not be triggered. The source code is as follows:

// Handle preloading
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 have been binding, or need to update or not effective, won't trigger tryBindViewHolderByDeadline method.final int offsetPosition = mAdapterHelper.findPositionOffset(position);
    bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
Copy the code

If we can recycle a cached view, then recycle it and onBind can be used to recycle it. The source code for the AbsListView is as follows:

View obtainView(int position, boolean[] outMetadata) {...// Get the cached view
    final View scrapView = mRecycler.getScrapView(position);
    // call getView every time, which means call onBind every time
    final View child = mAdapter.getView(position, scrapView, this);
    if(scrapView ! =null) {
        if(child ! = scrapView) {// Failed to re-bind the data, return scrap to the heap.
            mRecycler.addScrapView(scrapView, position);
        } else if (child.isTemporarilyDetached()) {
            outMetadata[0] = true;

            // Finish the temporary detach started in addScrapView().child.dispatchFinishTemporaryDetach(); }}... }Copy the code

The following GIF shows the implementation of onBind when handling off-screen cache in RecyclerView. When the user slightly slides in and out of the item, the viewHolder that gets the cache from mCachedViews can be reused directly without triggering onBind.

3.2. Partial refresh function

In the case of local refresh, ListView is a whole heap, and all mActiveViews are moved into the secondary cache mScrapViews, and RecyclerView is more flexible to modify the flag bit of each View, to distinguish whether to re-bindView. Many useless BindViews can be avoided by a partial refresh. The GIF below shows a partial refresh of position at 4, and we can see what happens to onBind in the second transparent box.


Reference: RecyclerView mechanism analysis: Recyer Android ListView and RecyclerView comparison analysis – caching mechanism