RecyclerView as a very attractive control, part of the credit is due to its excellent cache mechanism. The cache mechanism of RecyclerView is the core part of RecyclerView, but also the difficult part. As difficult as the caching mechanism is, it can’t resist our curiosity 😂. Today we’re going to look at some of its wonders.

References for this article:

  1. RecyclerView cache principle, there are figures and the truth
  2. RecyclerView source code analysis (two) — cache mechanism
  3. Deep RecyclerView source code explore four: recycling and animation
  4. Hand touch second play, visual RecyclerView cache mechanism
  5. RecyclerView source code analysis (a) – RecyclerView three processes

Since this article is related to the first two articles in this series, you can refer to the author’s first two articles in this series for ease of understanding.

Note that all the code in this article is from 27.1.1.

1. An overview of the

Before formally analyzing the source code, I will give an overview of the caching mechanism, but also a unified explanation of some concepts, which will be very helpful for the analysis of the following, because if you do not understand these concepts, it is easy to see the rain and fog behind.

(1) level 4 cache

First of all, I divided RecyclerView cache into four levels, maybe some people will divide it into three levels, these depends on personal understanding. Here’s a general explanation of what each level of caching means.

The cache level Real variables meaning
Level 1 cache mAttachedScrapandmChangedScrap This is the highest priority cache,RecyclerViewIn obtainingViewHolder, the priority will be to find these two caches. Among themmAttachedScrapStore what’s currently on the screenViewHolder.mChangedScrapThe store is where the data is updatedViewHolder, for example calledAdapterthenotifyItemChangedMethods. There may be some confusion about these two caches, but don’t worry, I’ll explain them in detail later.
The second level cache mCachedViews The default size is 2 and is usually used to store prefetchViewHolderAt the same time in the recyclingViewHolderIt is also possible to store some of themViewHolderThis partViewHolderIn general, the meaning is similar to level 1 cache.
Three levels of cache ViewCacheExtension Custom caches, which are not usually used, are ignored in this article
Four levels of cache RecyclerViewPool According to theViewTypeTo cacheViewHolder, eachViewTypeThe array size is 5 and can be changed dynamically.

As shown in the table above, the meanings and functions of each cache are uniformly explained. Here, I’ll explain some of these caches in more detail.

  1. mAttachedScrapIn the table above, it means that the storage is currently on the screenViewHolder. It’s actually detached from the screenViewHolder“, but will soon be added to the screenViewHolder. For instance,RecyclerViewSlide up and down, slide out a new oneItem, will be called againLayoutManagertheonLayoutChildrenMethod, thus will put all of the screenViewHolderFirst,scrapDrop (meaning discard), add tomAttachedScrapGo inside and then rearrange eachItemView, will take precedencemAttachedScrapSo it’s very efficient. The process will not be repeatedonBindViewHolder.
  2. mCachedViews: The default size is 2, but it is usually 3. 3 consists of the default size 2 + the number of prefetches 1. So in theRecyclerViewOn the first load,mCachedViewsthesizeIs 3LinearLayoutManagerTake the vertical layout of the. Generally speaking, yesRecyclerViewthesetItemViewCacheSizeMethod sets the size, but this does not include the prefetch size; The prefetch size passesLayoutManagerthesetItemPrefetchEnabledMethod to control.

(2). Several state values of ViewHolder

Call ViewHolder isInvalid, isRemoved, isBound, isTmpDetached, isScrap and isUpdated. I’m going to explain it uniformly here.

The method name The corresponding Flag Meaning or timing of state setting
isInvalid FLAG_INVALID Said that the currentViewHolderWhether it is invalid. In general, this happens in three cases: 1AdapterthenotifyDataSetChangedMethods; 2. Manually invoke the callRecyclerViewtheinvalidateItemDecorationsMethods; 3. CallRecyclerViewthesetAdapterMethods orswapAdapterMethods.
isRemoved FLAG_REMOVED Represents the currentViewHolderWhether to remove. Typically, the data source is partially removed and then calledAdapterthenotifyItemRemovedMethods.
isBound FLAG_BOUND Said that the currentViewHolderWhether it has already been calledonBindViewHolder.
isTmpDetached FLAG_TMP_DETACHED Represents the currentItemViewWhether fromRecyclerView(i.e., the parentView)detachIt off. Generally speaking, there are two situations where this happens: 1RecyclerViewthedetachViewCorrelation method; 2. In the frommHideViewsTo get insideViewHolder, will firstdetachOff theViewHolderThe associatedItemView. Here’s another onemHideViewsI’ll explain what it is in more detail later.
isScrap No Flag is used to indicate the statusmScrapContainerCheck whether the value is null Indicates whether themAttachedScrapormChangedScrapArray, in turn represents the currentViewHolderWhether to be abandoned.
isUpdated FLAG_UPDATE Said that the currentViewHolderWhether it has been updated. Generally speaking, there are three situations that can happen: 1.isInvalidThere are three cases of method existence; 2. CallAdaptertheonBindViewHolderMethods; 3. The callAdapterthenotifyItemChangedmethods

(3). ChildHelperçš„mHiddenViews

In the four-level cache, we do not count mHiddenViews. Because mHiddenViews only have elements during the animation, when the animation is over, they are naturally empty. So mHiddenViews does not count in the level 4 cache.

Another problem with mChangedScrap is that when the Adapter notifyItemChanged method is called, the updated ViewHolder is reversed into the mChangedScrap array. MChangedScrap or mHiddenViews? While some people may have questions about mChangedScrap and mAttachedScrap, here is a unified explanation:

First, if the notifyItemChanged method of the Adapter is called, it is called back to the onLayoutChildren method of the LayoutManager, and in the onLayoutChildren method, All viewholders on the screen are recycled to mAttachedScrap and mChangedScrap. The ViewHolder is placed on mAttachedScrap and mChangedScrap respectively, and what condition is placed on mAttachedScrap and what condition is placed on mChangedScrap is the difference between them.

Let’s look at some code to tell the difference between mAttachedScrap and mChangedScrap

        void scrapView(View view) {
            final ViewHolder holder = getChildViewHolderInt(view);
            if(holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID) || ! holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {if(holder.isInvalid() && ! holder.isRemoved() && ! mAdapter.hasStableIds()) { throw new IllegalArgumentException("Called scrap view with an invalid view."
                            + " Invalid views cannot be reused from scrap, they should rebound from"
                            + " recycler pool." + exceptionLabel());
                }
                holder.setScrapContainer(this, false);
                mAttachedScrap.add(holder);
            } else {
                if (mChangedScrap == null) {
                    mChangedScrap = new ArrayList<ViewHolder>();
                }
                holder.setScrapContainer(this, true); mChangedScrap.add(holder); }}Copy the code

A lot of people, when they see this for the first time, they’re kind of dumbstruck, and I was. Today we are going to look at this method. The fundamental purpose of this is to determine the ViewHolder flag state to decide whether to add mAttachedScrap or mChangedScrap. From the above code, we get:

  1. mAttachedScrapIt has two states in itViewHolder: 1. Is simultaneously marked asremoveandinvalid; 2. Completely unchangedViewHolder. There’s a third judgment here, which is the same asRecyclerViewtheItemAnimatorAbout, ifItemAnimatorIs empty orItemAnimatorthecanReuseUpdatedViewHolderMethod true will also be put intomAttachedScrap. So normally, when does it return true? fromSimpleItemAnimatorThe source code can be seen whenViewHoldertheisInvalidWhen the method returns true, it is put intomAttachedScrapThe inside. In other words, ifViewHolderIf it’s not working, it’s going to bemAttachedScrapThe inside.
  2. thenmChangedScrapWhat type of flag is in itViewHolder? Is, of course,ViewHoldertheisUpdatedWhen the method returns true, it is put intomChangedScrapGo inside. So, callAdapterthenotifyItemChangedMethod, andRecyclerViewtheItemAnimatorIt’s not empty, it puts intomChangedScrapThe inside.

Now that we know the difference between mAttachedScrap and mChangedScrap, let’s look at the difference between The Scrap array and mHiddenViews.

MHiddenViews only stores the ViewHolder of the animation, which is emptied when the animation is finished. The reason mHiddenViews exists, I guess, is that there is the possibility of reuse during animation, and then you can reuse it in mHiddenViews. The Scrap array and mHiddenViews do not conflict at all, so it is possible to have a ViewHolder in both the Scrap array and mHiddenViews. But it doesn’t matter because it will be removed from mHiddenViews at the end of the animation.

In this paper, the analysis of RecyclerView exchange mechanism, intends to start from two major aspects: 1. Reuse; 2. Recycle.

Let’s take a look at some of the logic of reuse, because only by understanding how RecyclerView is used, can we understand recycling more clearly.

2. Reuse

To reuse ViewHolder by RecyclerView, we need to start with LayoutState’s next method. When LayoutManager lays out the itemView, it needs to get a ViewHolder object. This is where the reuse logic is invoked. Let’s take a look:

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

The next method didn’t actually do anything, just call the getViewForPosition method of RecyclerView to get a View. GetViewForPosition method will call to RecyclerView tryGetViewHolderForPositionByDeadline method. So, RecyclerView is really the core of reuse in this method, we will analyze this method in detail today.

(1). Obtain the ViewHolder by Position

This is a higher priority because each ViewHolder has not been changed. In this case, the ViewHolder corresponding to an ItemView has been updated, so other Viewholers on the screen can quickly correspond to the original ItemView. Let’s look at the relevant source code.

            if(mState.isPreLayout()) { holder = getChangedScrapViewForPosition(position); fromScrapOrHiddenOrCache = holder ! = null; } // 1) Find by position from scrap/hidden list/cacheif (holder == null) {
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                if(holder ! = null) {if(! validateViewHolderForOffsetPosition(holder)) { // recycle holder (and unscrapif 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; }}}Copy the code

The above code is divided into two steps:

  1. frommChangedScrapInside to getViewHolderThis store is updatedViewHolder.
  2. , respectively,mAttachedScrap,mHiddenViews,mCachedViewsTo obtainViewHolder

Let’s briefly analyze these two steps. Let’s look at step one.

            if(mState.isPreLayout()) { holder = getChangedScrapViewForPosition(position); fromScrapOrHiddenOrCache = holder ! = null; }Copy the code

If the current layout phase is pre-layout, get the ViewHolder from the mChangedScrap. So what is the pre-layout stage? Here IS a brief explanation of the concept of pre-layout.

Pre-layout can also be called preLayout, when the current RecyclerView is in the dispatchLayoutStep1 stage, called pre-layout; DispatchLayoutStep2 is called the stage of the real layout; DispatchLayoutStep3 is called the postLayout stage. To enable pre-layout, you must have an ItemAnimator, and the LayoutManager of each RecyclerView must have pre-animation enabled.

Do you feel even more confused after hearing the explanation? In order to explain one concept, it leads to more concepts? As for animation, unsurprisingly, I will analyze it in the next article. This article will not explain animation too much. Here, for simplicity, as long as RecyclerView is dispatchLayoutStep1, we treat it as in the pre-layout stage.

Why only take it from mChangedScrap during pre-layout? First, we need to know what type of ViewHolder is in the mChangedScrap array. From the previous analysis, we know that the ViewHolder changed will be placed in the mChangedScrap array only if the ItemAnimator is not empty. Since the ViewHolder at the same location is different before and after the Chang animation, the mChangedScrap cache is used for pre-layout, and the mChangedScrap cache is not used for formal layout, ensuring that the ViewHolder at the same location is different before and after the chang animation. Why make sure the ViewHolder is different before and after the animation? This is the knowledge related to RecyclerView animation mechanism, which will not be explained in detail here. There will be special articles to analyze it in the future. Here, we only need to remember that the premise of chang animation execution is that the ViewHolder before and after the animation is different.

Then, let’s look at step two.

            if (holder == null) {
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                if(holder ! = null) {if(! validateViewHolderForOffsetPosition(holder)) { // recycle holder (and unscrapif 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; }}}Copy the code

This step is easy to understand. Obtain ViewHolder from mAttachedScrap, mHiddenViews, and mCachedViews respectively. If the ViewHolder is invalid, we need to clean it up and put it back into the cache. The corresponding cache is mCacheViews and RecyclerViewPool. RecycleViewHolderInternal method is the method of recycling ViewHolder later recovery related logic will focus on analysis the method, here is not to pursue.

(2). Obtain the ViewHolder by viewType

In the previous section, we examined the method of obtaining a ViewHolder by Position. Here we examine the second method –ViewType. But here, I first make a simple summary of the previous way, RecyclerView through Position to obtain ViewHolder, there is no need to judge whether the ViewType is legal, because if you can obtain ViewHolder through Position, The ViewType itself corresponds correctly.

Here the ViewHolder representation is retrieved by ViewType, at which point the Position cached by the ViewHolder is invalid. The process of getting a ViewHolder in ViewType is divided into 3 steps:

  1. ifAdapterthehasStableIdsMethod returns true and is preferredViewTypeandidTwo conditions to find. If not, proceed to step 2.
  2. ifAdapterthehasStableIdsThe method returns false, in which case the first value of theViewCacheExtensionInside. If you haven’t found it yet, you’ll end up inRecyclerViewPoolInside to get the ViewHolder.
  3. If none of the above reuse steps are found appropriateViewHolder, will be called at lastAdaptertheonCreateViewHolderMethod to create a new oneViewHolder.

Here, we need to note that steps 1 and 2 above have the prerequisite that both must compare viewTypes. Next, I’ll briefly examine each step through the code.

A. Find the ViewHolder by id

By id to find suitable ViewHolder is mainly done by call getScrapOrCachedViewForId method, we simply look at the code:

                // 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; }}Copy the code

While getScrapOrCachedViewForId method itself has no necessary, is respectively from mAttachedScrap and looking for the right ViewHolder mCachedViews array.

B. Obtain the ViewHolder from RecyclerViewPool

ViewCacheExtension exists in very rare cases, so I won’t expand it here for the sake of simplicity (actually I don’t understand it either!). So, here, we directly look at the RecyclerViewPool method.

Here, we need to understand the array structure of RecyclerViewPool. We simply analyze the RecyclerViewPool class.

        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<>();
Copy the code

Inside the RecyclerViewPool, SparseArray is used to store ViewHolder arrays corresponding to each ViewType, where the maximum size of each array is 5. Isn’t this data structure very simple?

Simple understanding of the RecyclerViewPool data structure, next we look at the reuse of the relevant code:

                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); }}}Copy the code

Trust me, this code doesn’t need me to analyze it. It’s very simple.

C. Create a ViewHolder by calling the onCreateViewHolder method of the Adapter
                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"); }}Copy the code

The main purpose of the above code is to create a ViewHolder by calling the createViewHolder method of the Adapter, simply counting the time at which a ViewHolder was created.

So much for our understanding of reuse mechanisms. The RecyclerView reuse mechanism is not complicated at all, and I think one of the things that makes people shy away is that we don’t know why we’re doing it, and if we know why we’re doing it, it’s all taken for granted.

Analysis of RecyclerView reuse part, next, we will analyze the recycling part.

3. The recycling

Recycling is very important within RecyclerView reuse mechanism. First of all, if there is a process of reuse, there must be a process of recycling; Secondly, understand the reuse and recycling two processes at the same time, which can help us understand the working principle of RecyclerView in macro; Finally, it is helpful to understand when RecyclerView recycles ViewHolder.

In fact, the recycling mechanism is not so difficult as imagined, this paper intends to analyze the recycling process of RecyclerView from several aspects.

  1. Scrap array
  2. MCacheViews array
  3. MHiddenViews array
  4. RecyclerViewPool array

Next, we will analyze them one by one.

(1). Scrap array

Recycle ViewHolder into Scrap. We have already analyzed the Recycler’s scrapView method. Let’s see where scrapView is called. There are two places:

  1. ingetScrapOrHiddenOrCachedHolderForPositionMethod, if frommHiddenViewsTo obtain aViewHolderI would have put this firstViewHolderfrommHiddenViewsRemove from the array and callRecyclerthescrapViewMethod takes thisViewHolderInto thescrapArray, and tagFLAG_RETURNED_FROM_SCRAPandFLAG_BOUNCED_FROM_HIDDEN_LISTTwo flag.
  2. inLayoutManagerThe inside of thescrapOrRecycleViewMethod is also calledRecyclerthescrapViewMethods. There are two cases where this happens: 1. It is called manuallyLayoutManagerRelated methods; 2.RecyclerViewA layout is made (calledrequestLayoutMethods)

(2). MCacheViews array

The mCacheViews array serves as a level 2 cache, with more reclaimed paths than level 1 cache. About mCacheViews array, the emphasis is on Recycler recycleViewHolderInternal method. I have divided the collection paths of mCacheViews array into three categories. Let’s take a look:

  1. Recycle out in the relayout. This happens mainly in callsAdapterthenotifyDataSetChangeMethod, and at this timeAdapterthehasStableIdsMethod returns false. You can see it here. WhynotifyDataSetChangeWhy is the method so inefficient, but also know why rewritehasStableIdsMethods can improve efficiency. becausenotifyDataSetChangeApproach makesRecyclerViewIt recyclesViewHolderIn the second level cache, the efficiency is naturally lower.
  2. When you reuse it, you get it from level 1 cacheViewHolderBut now thisViewHolderIt is removed from the level 1 cache if the Position is invalid and does not match the ViewTypeViewHolder, from add tomCacheViewsinside
  3. When callingremoveAnimatingViewMethod if the currentViewHolderLabeled remove, will callrecycleViewHolderInternalMethod to retrieve the correspondingViewHolder. callremoveAnimatingViewThe timing of the method represents the currentItemAnimatorIt’s done.

(3). MHiddenViews array

The condition for a ViewHolder to recycle into the mHiddenView array is relatively simple. If the current operation supports animation, the addAnimatingView method of RecyclerView will be called. And in this method we’re going to add the View that we’re animating to the mHiddenView array. This is usually done during animation because mHiddenViews only has elements during animation.

(4). RecyclerViewPool

RecyclerViewPool with mCacheViews, by recycleViewHolderInternal ways to recycle, so the scene with mCacheViews about, only when not satisfied in the mCacheViews, Will put into RecyclerViewPool inside.

(5). Why does hasStableIds returning true improve efficiency?

Once you understand the reuse and recycling mechanism of RecyclerView, the problem becomes simple. I explain the reasons from two aspects.

A. Reuse aspect

Let’s start by looking at how reuse shows that hasStableIds can improve efficiency. Let’s look at the code:

                if (mAdapter.hasStableIds()) {
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    if(holder ! = null) { // update position holder.mPosition = offsetPosition; fromScrapOrHiddenOrCache =true; }}Copy the code

If the Adapter hasStableIds method returns true when attempting to retrieve a ViewHolder using Position, the ViewHolder will be retrieved from the level 1 or level 2 cache first. Instead of going directly to RecyclerViewPool. From here, we can see that the hasStableIds approach improves efficiency in terms of reuse.

B. Recycling
        private void scrapOrRecycleView(Recycler recycler, int index, View view) {
            final ViewHolder viewHolder = getChildViewHolderInt(view);
            if (viewHolder.shouldIgnore()) {
                if (DEBUG) {
                    Log.d(TAG, "ignoring view " + viewHolder);
                }
                return;
            }
            if(viewHolder.isInvalid() && ! viewHolder.isRemoved() && ! mRecyclerView.mAdapter.hasStableIds()) { removeViewAt(index); recycler.recycleViewHolderInternal(viewHolder); }else{ detachViewAt(index); recycler.scrapView(view); mRecyclerView.mViewInfoStore.onViewDetached(viewHolder); }}Copy the code

From the above code, we can see that if the hasStableIds method returns true, all of the collection will go into the Scrap array. So this is exactly what we saw before.

It’s easy to see why the hasStableIds method returns true to improve efficiency.

4. To summarize

RecyclerView recycling and reuse mechanism is almost analyzed here. So here’s a little summary.

  1. inRecyclerViewThere are 4 levels of internal cache, the meaning of each level of cache is not the same, and the priority of reuse is also from top to bottom, their recycling is not the same.
  2. mHideenViewsThe web exists to solve the problem of reuse during animation.
  3. ViewHolderThere are a number of internal flags, but before you understand the recycling and reuse mechanisms, it is a good idea to use themViewHolderClear the flag of.

Finally, a picture is used to conclude the introduction of this article.

RecyclerView