Android: deeply understand the caching mechanism of RecyclerView

preface

This article records the author's journey of learning the recolerview caching mechanism

1, Overview

We all know that RecyclerView will not lead to OOM in any case, and this depends on its own powerful recycling and reuse mechanism. How to realize its recycling and reuse mechanism? The author records the analysis process below

2, Basic knowledge

1. Reused content

Before understanding the cache reuse mechanism of RecyclerView, we must first understand what the cache reuse mechanism is for reuse. There is no doubt that it can not be the controls in the layout we write for each itemViews. The answer is given directly here. The content of cache reuse mechanism is ViewHolder, which will be analyzed in the following source code analysis

2. When to call

In the process of customizing the RecyclerView adapter, we will inevitably encounter two methods: onCreateViewHolder() and onBindViewHolder(). The former will call the method to create ViewHolder() and the latter will call relevant methods for data binding.
It should be noted that if the View is newly created and filled with data, onCreateViewHolder() and onBindViewHolder() methods will be called, which usually occurs during the process of RecyclerView creating the View and filling in data for the first time;
If new ItemView displays appear continuously during screen sliding, the existing ViewHolder will be reused and onBindViewHolder() will be called to bind the data

3. Corresponding status of each Item

The following describes the status of ViewHolder. The related methods are as follows

3, Level 4 cache of RecyclerView

Before understanding the level cache of RecyclerView, we first understand the data structure of its level cache through the official source code constructor of RecyclerView

final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();//Invalid, not removed, not updated holder of the item separated from RecyclerView when saving the re layout
        ArrayList<ViewHolder> mChangedScrap = null;//Invalid item when saving re layout, Holder not removed

        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();//Save the newly removed ViewHolder and recycle it in a rolling manner

        private final List<ViewHolder>
                mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);

        private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
        int mViewCacheMax = DEFAULT_CACHE_SIZE;

        RecycledViewPool mRecyclerPool;

        private ViewCacheExtension mViewCacheExtension;

        static final int DEFAULT_CACHE_SIZE = 2;//By default, the number of item s recycled by rolling recycling is 2

Scrap

Scrap is the lightest cache of RecyclerView, including attachedsrap and mchangedsrap. It does not participate in the recycling and reuse during list scrolling. As a temporary cache during re layout, it is used to cache the viewholders that appear before and after interface re layout. These viewholders are invalid, not removed and not marked. Among these invalid, unremoved and unmarked viewholders, attachedsrap is responsible for saving the unchanged ViewHolder; The rest is saved by mchangedsrap. Attachedsrap and mchangedsrap are just sharing their work and saving different viewholders.

Note: Scrap is only used as a temporary cache for layout. It has nothing to do with the cache during sliding. Its detach and atach only exist temporarily in the layout process. At the end of the layout, the Scrap list should be empty, and the cached data should either be rearranged or emptied; In short, there should be nothing in the Scrap list after the layout is finished.

The above description is inevitably abstract. Let's elaborate and interpret it through a specific example

In this case, we delete the data itemA and itemB of a RecyclerView, and then move itemC, itemD and itemE up in turn. There is no doubt that this process will affect the layout parameters of the latter itemView, that is, the parameters required by onLayout(). In this process, the relevant parameters before and after itemA and itemB have not changed, so itemA itemB is stored in attachedsrap, and itemC and itemD are stored in mchangedsrap.

It should be noted that in this process, only the items appearing on the screen are operated. If item e does not appear on the screen, it is thrown into any list

Detailed analysis is as follows

In a mobile phone screen, delete itemB and call notifyItemRemoved() method. If the item is invalid and REMOVED, it will be recycled to other caches. Otherwise, it will be cached in scratch. Then attachedsrap and mchangedsrap will store itemView respectively. itemA has no change and is stored in attachedsrap. Although itemB has been REMOVED, it is still valid, It is also stored in attachedsrap (but it will be marked REMOVED and then REMOVED); itemC and itemD are changed, and the position is moved upward, which will be stored in mchangedsrap. When deleting, the ABCD will enter the Scrap; After deletion, the ACD will come back. A has not changed, but the location of the CD has changed and the content has not changed.
The local refresh of RecyclerView depends on the temporary cache of scratch. When we notify the item of change through notifyItemRemoved(), notifyItemChanged(), the ViewHolder that has not changed is cached through attachedsrap, and the rest is cached by mchangedsrap. When adding itemView, it is quickly taken out from it to complete the local refresh. Note that if we use notifyDataSetChanged() to notify RecyclerView to refresh, the itemView on the screen is marked as FLAG_INVALID and not removed, so instead of using the scratch cache, it is directly thrown into the CacheView or RecycledViewPool pool. When you come back, you can bind the data again.

CacheView

CacheView is used to recycle and reuse the view just moved out of the screen when the position of the RecyclerView list changes. Accurately match the original item according to position/id. if yes, it will be returned directly for use without rebinding the data; If not, go to the RecycledViewPool to find the holder instance to return and rebind the data.
The maximum capacity of the CacheView is 2. When caching a new ViewHolder, if the maximum limit is exceeded, the first data cached by the CacheView will be added to the RecycledViewPool and then removed. Finally, the new ViewHolder will be added. When we slide the RecyclerView, the Recycler will constantly cache the View that has just moved out of the screen and is not visible into the CacheView. When the CacheView reaches the upper limit, it will continue to replace the old ViewHolder in the CacheView and throw them into the RecycledViewPool. If you keep scrolling in one direction, the CacheView does not help in efficiency. It just caches the ViewHolder that slides behind and caches it into the RecycledViewPool. If you often slide back and forth, you can reuse it directly from the CacheView according to the item of the corresponding position without rebinding the data, which will be well used.

Let's take a look at the application of a CacheView, as shown in the figure. itemA moves out of the screen first, and then moves into the CacheView. In the process of sliding down or up, judge the moved item. If it is determined to be the previous itemA according to position/id, move it directly from the CacheView

ViewCacheExtension

ViewCacheExtension is a help class for cache expansion, which provides an additional layer of cache pool for developers. Depending on the situation, whether the developer uses ViewCacheExtension to add a layer of cache pool. Recycler first looks for the reused view in scratch and CacheView. If not, it looks for the view in ViewCacheExtension. If not, it finally looks for the reused view in RecycledViewPool.

In daily development, we generally can't use ViewCacheExtension, so we've briefly covered it here

RecycledViewPool

When Scrap, CacheView and ViewCacheExtension are unwilling to recycle, they will be thrown into the RecycledViewPool for recycling, so the RecycledViewPool is the ultimate recycle bin of Recycler.
RecycledViewPool actually saves viewholders in the form of an ArrayList nested in SparseArray, because the viewholders saved by RecycledViewPool are distinguished by itemType. This makes it convenient for different itemtypes to save different viewholders. When recycling, it only recycles the ViewHolder object of the viewType and does not save the original data information. When reusing, it needs to go through the onBindViewHolder() method to rebind the data.

The source code of RecycledViewPool is as follows

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

It can be seen that the RecycledViewPool defines sparearray mscrap, which is a sparearray that saves static class ScrapData objects according to different itemtypes. The ScrapData contains ArrayList mscrapheader, which is the ArrayList that saves the ViewHolder under the itemType.
The cache pool defines the default cache size, DEFAULT_MAX_SCRAP = 5. This number does not mean that the entire cache pool can only cache these multiple viewholders, but the cache number of ViewHolder list s of different itemtypes, that is, the number of mScrap, indicating that there are only 5 groups of mscrapheaders of different types at most. mMaxScrap = DEFAULT_MAX_SCRAP indicates that five viewholders of different types are saved by default. Of course, the value of maxscrap can be set. In this way, the RecycledViewPool caches the viewholders of different ViewType s by type.

4, Source code analysis part

Drawing part of RecyclerView

Set up layout manager

RecyclerView needs to set the layout manager before drawing. Otherwise, RecyclerView doesn't know how to draw. Let's start with this part of the source code

 recyclerView.setLayoutManager(manager);//Set up layout manager
 
 public void setLayoutManager(@Nullable LayoutManager layout) {
        if (layout == mLayout) {
            return;
        }
        stopScroll();//Stop scrolling first to prevent the cache View from being affected
        // TODO We should do this switch a dispatchLayout pass and animate children. There is a good
        // chance that LayoutManagers will re-use views.
        if (mLayout != null) {//Reset the parameters of all RecyclerView
            // end all running animations
            if (mItemAnimator != null) {
                mItemAnimator.endAnimations();//End animation
            }
            mLayout.removeAndRecycleAllViews(mRecycler);//Remove all recycled itemviews
            mLayout.removeAndRecycleScrapInt(mRecycler);//Overflow all abandoned views
            mRecycler.clear();//Clear cache

            if (mIsAttached) {
                mLayout.dispatchDetachedFromWindow(this, mRecycler);
            }
            mLayout.setRecyclerView(null);
            mLayout = null;
        } else {
            mRecycler.clear();
        }
        // this is just a defensive measure for faulty item animators.
        mChildHelper.removeAllViewsUnfiltered();
        mLayout = layout;
        if (layout != null) {
            if (layout.mRecyclerView != null) {
                throw new IllegalArgumentException("LayoutManager " + layout
                        + " is already attached to a RecyclerView:"
                        + layout.mRecyclerView.exceptionLabel());
            }
            mLayout.setRecyclerView(this);//In this step, you can see that a LayoutManager can only be managed with a RecyclerView
            if (mIsAttached) {
                mLayout.dispatchAttachedToWindow(this);
            }
        }
        mRecycler.updateViewCacheSize();//Update cache size
        requestLayout();
    }

It can be seen here that before setting the layout manager, RecyclerView first performs relevant reset and recycling work, then associates LayoutManaer with RecyclerView, and finally requests redrawing. Call the = = requestLayout() = = method requesting redrawing, which will call the onMeasure(), onLayout(), onDraw() methods of RecyclerView to draw trilogy

Recycling part of RecyclerView

Here, the LinearLayoutManager will analyze and observe its = = onlayoutchildren() = = method for layout of child views

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
            if (state.getItemCount() == 0) {
                removeAndRecycleAllViews(recycler);//Remove all child views
                return;
            }
        }
        ensureLayoutState();
        mLayoutState.mRecycle = false;//No recycling
        //Draw layout upside down
        resolveShouldLayoutReverse();
        onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);

        //Temporarily separate the attached view s, that is, all child detach es and recycle them through scratch
        detachAndScrapAttachedViews(recycler);
    }

When = = onLayoutChildren() layout, first remove all child views according to the actual situation. Those viewholders are unavailable; Then, the attached itemviews are temporarily separated by detachandscrapatachedviews() = = and cached in the List.

The function of detachandscrapatachedviews() is to separate all items of the current screen from the screen, take them from the layout of recyclerview, save them to the list, and put viewholders in new positions one by one when re layout. The ViewHolder on the screen is removed from the layout of RecyclerView, stored in Scrap, Scrap includes mAttachedScrap and mChangedScrap, they are list, which is used to save ViewHolder list from RecyclerView layout, ==detachAndScrapAttachedViews() will only be invoked in onLayoutChildren() = =, only when layout is done, will it be eliminated. Then add comes in and rearranges the layout. However, you should note that scrap only saves the viewholder of the item currently displayed on the screen in the recyclerview layout, and does not participate in recycling and reuse. It is simply to take it out of the recyclerview and rearrange it. Items that have not been saved will be put into the cache of mCachedViews or RecycledViewPool for recycling.

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

   private void scrapOrRecycleView(Recycler recycler, int index, View view) {
        final ViewHolder viewHolder = getChildViewHolderInt(view);
        if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                && !mRecyclerView.mAdapter.hasStableIds()) {
            removeViewAt(index);//Remove VIew
            recycler.recycleViewHolderInternal(viewHolder);//Cache to CacheView or RecycledViewPool
        } else {
            detachViewAt(index);//Detach View
            recycler.scrapView(view);//Scratch cache
            mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
        }
    }

    void scrapView(View view) {
        final ViewHolder holder = getChildViewHolderInt(view);
        if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
            holder.setScrapContainer(this, false);
            mAttachedScrap.add(holder);//Save to attachedsrap
        } else {
            if (mChangedScrap == null) {
                mChangedScrap = new ArrayList<ViewHolder>();
            }
            holder.setScrapContainer(this, true);
            mChangedScrap.add(holder);//Save to mchangedsrap
        }
    }


In the else branch, detachViewAt() separates the view, and then caches it into the scratch through scrap view()

In the scrapView() method, the ViewHolder of the if() branch is saved to the attachedsrap, and the ViewHolder of the else branch is saved to the mchangedsrap.

You can see that attachedsrap is removed (isInvalid()) or the parameter has not changed,
Mchangedsrap is the other case

Go back to = = scrapOrRecycleView(), and enter the if() branch. If the viewHolder is invalid, not removed and not marked, it will be put into the recycleViewHolderInternal() cache, and removeViewAt() = = the viewHolder is removed

   void recycleViewHolderInternal(ViewHolder holder) {
           ·····
        if (forceRecycle || holder.isRecyclable()) {
            if (mViewCacheMax > 0
                    && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                    | ViewHolder.FLAG_REMOVED
                    | ViewHolder.FLAG_UPDATE
                    | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {

                int cachedViewSize = mCachedViews.size();
                if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {//If the capacity limit is exceeded, remove the first one
                    recycleCachedViewAt(0);
                    cachedViewSize--;
                }
                 	·····
                mCachedViews.add(targetCacheIndex, holder);//Mviewedcaches recycle
                cached = true;
            }
            if (!cached) {
                addViewHolderToRecycledViewPool(holder, true);//Put it into RecycledViewPool for recycling
                recycled = true;
            }
        }
    }

If the conditions are met, it will be preferentially cached in mCachedViews. If the maximum limit of mCachedViews is exceeded, add the first data cached by CacheView to the ultimate recycling pool RecycledViewPool through recycleCachedViewAt(), and then remove it. Finally, add() a new ViewHolder to mCachedViews.

The rest that do not meet the conditions will be cached in the RecycledViewPool through = = addViewHolderToRecycledViewPool() = =.

void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
    clearNestedRecyclerViewIfNotNested(holder);
    View itemView = holder.itemView;
    ······
    holder.mOwnerRecyclerView = null;
    getRecycledViewPool().putRecycledView(holder);//Add holder to RecycledViewPool
}

Another is that when filling the layout fill(), it will recycle the views out of the screen into mCachedViews or RecycledViewPool:

 int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
              recycleByLayoutState(recycler, layoutState);//Remove the view from the screen
        }
    }

After tracing through recycleByLayoutState(), you will come to recycler In the public recycling method of recycleview (view) recycler:

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

Recycle the separated views into the cache pool for later rebinding and reuse. Here comes the recycleViewHolderInternal(holder). As above, cache mcachedviews > recycledviewpool according to priority.

Reuse process of RecyclerView

To view the layout entry of LinearLayoutManager = = onLayoutChildren() = = view

  @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
            if (state.getItemCount() == 0) {
                removeAndRecycleAllViews(recycler);//Remove all child views
                return;
            }
        }
    
        //Temporarily separate the attached view s, that is, all child detach es and recycle them through scratch
        detachAndScrapAttachedViews(recycler);
        
        if (mAnchorInfo.mLayoutFromEnd) {
            //The tracing point position fills the ItemView layout from the start position
            updateLayoutStateToFillStart(mAnchorInfo);
            fill(recycler, mLayoutState, state, false);//Populate all itemviews
           
 			//The stroke position fills the ItemView layout from the end position
            updateLayoutStateToFillEnd(mAnchorInfo);
            fill(recycler, mLayoutState, state, false);//Populate all itemviews
            endOffset = mLayoutState.mOffset;
        }else {
            //The stroke position fills the ItemView layout from the end position
            updateLayoutStateToFillEnd(mAnchorInfo);
            fill(recycler, mLayoutState, state, false);
 
            //The tracing point position fills the ItemView layout from the start position
            updateLayoutStateToFillStart(mAnchorInfo);
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
        }
    }

Here are two methods, which correspond to those caused by sliding RecyclerView from different directions;
However, in either direction, the = = fill() method is eventually called to fill the given layout defined by layoutState() = =

 int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
        recycleByLayoutState(recycler, layoutState);//Recycle the view that slides out of the screen
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {//Keep cycling until there is no data
            layoutChunkResult.resetInternal();
            layoutChunk(recycler, state, layoutState, layoutChunkResult);//Add a child
            ······
            if (layoutChunkResult.mFinished) {//The layout ends and the loop exits
                break;
            }
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;//Calculated based on the added child height offset   
        }
     	······
        return start - layoutState.mAvailable;//Returns the size of the filled area
    }

The core method is = = while() loop, and judge whether there is any space left in the visible area. If so, fill the view. The core method is to execute layoutChunk() = = fill an itemView to the screen through the while() loop, = = layoutChunk() = = complete the layout:

 void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);//Get the reused view
        ······
        }

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

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

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

tryGetViewHolderForPositionByDeadline() is the method to get the view. It will scrap e, cache, RecycledViewPool, or create and get a ViewHolder according to the given position/id

summary

Recycling principle of RecyclerVIew

When the RecyclerView rearranges onLayoutChildren() or fills the layout fill(), it will first separate or remove the necessary item s from the screen, mark them and save them in the list. When the RecyclerView rearranges, it will take out the viewholder and put it in a new position one by one.

  • If the RecyclerView is cached without scrolling (for example, deleting items), when relocating, separate the ViewHolder on the screen from the screen and store it in the scratch, that is, the changed ViewHolder is cached in the mchangedsrap, and the unchanged ViewHolder is stored in the matachedsrap; The remaining viewholders will be cached into mCachedViews or RecycledViewPool according to the priority of mCachedViews > RecycledViewPool.
  • If it is cached in the case of RecyclerVIew scrolling (such as sliding list), fill the layout when sliding, remove the items that slide out of the screen first, and the first level cache mCachedViews takes priority to cache these viewholders, but the maximum capacity of mCachedViews is 2. When mCachedViews is full, the old viewholders will be stored in the RecycledViewPool and removed by using the first in first out principle, Make room, and then add a new ViewHolder to mCachedViews. Finally, the remaining viewholders will be cached in the ultimate recycling pool RecycledViewPool. It caches different types of ArrayList s according to itemType, with a maximum capacity of 5.

Cache reuse process of RecycleView

  • When RecyclerView wants to get a reused ViewHolder, if it is preloaded, it will first accurately find the corresponding ViewHolder (according to position and ID respectively) in mchangedsrap, and return if any;
  • If not, go to attachedsrap and mCachedViews to find out whether the original ViewHolder is (position first and ID later). If so, it means that the ViewHolder has just been removed;
    If not, go to the mRecyclerPool finally. If the itemType type matches the corresponding ViewHolder, return the instance and let it rebind the data;
    (4) If mRecyclerPool does not return ViewHolder, createViewHolder() will be called to recreate one.

Reference blog
Deeply understand the recycling mechanism of RecyclerView

Deeply understand the drawing process and sliding principle of RecyclerView

Caching mechanism of RecyclerView

Keywords: Java Back-end Interview

Added by eon201 on Thu, 03 Mar 2022 07:23:11 +0200