Illustrate the caching mechanism of RecyclerView

The ideas and conclusions of this article are based on the two articles Anatomy of RecyclerView: a Search for a ViewHolder and  Anatomy of RecyclerView: a Search for a ViewHolder (continued) in the reference document  . Since some of the views of these two articles are inconsistent with those of other articles, after my own understanding, inference and debugging, I support the views of these two articles, so when you read the reference documents, you need to consider for yourself.

First, let's take a look at  RecyclerView as a whole

RecyclerView has five generals:

class name effect
RecyclerView.LayoutManager Responsible for the display management of the layout of the Item view
RecyclerView.ItemDecoration Add child Views to each Item view, for example, you can draw dividers and the like
RecyclerView.ItemAnimator Responsible for processing animation effects when data is added or deleted
RecyclerView.Adapter Create a view for each item
RecyclerView.ViewHolder A child layout that hosts the Item view
RecyclerView.Recycler Responsible for handling View caching

The responsibility of the RecyclerView is to display the data in the Datas on it with certain rules, but the RecyclerView is just a ViewGroup, it only knows the View and does not know the specific structure of the Data data, so two strangers want to build a call , we can easily think of the adapter pattern , therefore, RecyclerView needs an Adapter to communicate with Datas:

insert image description here

As shown above, RecyclerView indicates that it will only contact ViewHolder, and the job of Adapter is to convert Data into ViewHolder recognized by RecyclerView , so RecyclerView indirectly recognizes Datas.

Although things are going well, RecyclerView is a very lazy person. Although the Adapter has converted Datas into Views that RecyclerView knows, RecyclerView doesn't want to manage some child Views by itself, so it hires a high priest called LayoutManager to help it Complete the layout, now the icon looks like this:

insert image description here

As shown in the figure above, LayoutManager assists RecyclerView to complete the layout. But the high priest of LayoutManager also has a weakness, that is, it only knows how to lay out Views one by one on the RecyclerView, but it does not know how to manage these Views. If the high priest plays with Views recklessly, something will definitely happen, so there must be A guardian for managing View, it is Recycler . LayoutManager returns to the guardian to ask for it when it needs View. When LayoutManager does not need View (trying to slide out), it directly throws the abandoned View to Recycler, as shown below:

insert image description here

At this point, there is an Adapter responsible for translating data, a LayoutManager responsible for layout, and a Recycler responsible for managing Views. Everything is perfect, but RecyclerView is such a god. It orders that when the child View changes, the posture should be elegant (animation) , so a dancer is hired by ItemAnimator , so the dancer also enters this icon:

insert image description here

As above, we have a general understanding of RecylerView from a macro level. As you can see, RecyclerView, as a View, is only responsible for receiving various information from users, and then distributing the information according to its duties.

And the last one, ItemDecoration is to display the style of separation between each item. Its essence is actually a Drawable . When the RecyclerView executes the onDraw() method, its onDraw () will be called . At this time, if you rewrite this method, it is equivalent to drawing a Drawable representation directly on the RecyclerView . And finally, there is a method called getItemOffsets() in it, which can be understood literally, it is used to offset each item view. When we forcibly insert and draw a Drawable between each item view, then if we draw the item view according to the original logic, the Decoration will be overwritten, so the method getItemOffsets() is needed to make each item go back Offset a little so that it doesn't overwrite the divider style you drew earlier.

PS:

In fact , the width and height of the ItemDecoration are calculated in the itemview, but the drawing area of ​​the itemview itself is not so large, and the reserved place is just transparent, so the ItemDecoration is displayed through the itemview. Then it is very interesting. If I deliberately write 0 in the offset of ItemDecoration, then itemview will block ItemDecoration, and when itemview is added or deleted, it will disappear briefly (transparent), and then it will be transparent again. See what ItemDecoration looks like through itemview. Using this combination can also make unexpected animation effects.

Although Google has tried its best to decouple, there are still places where logic is mixed in the source code, such as animation processing.

In order to better understand the following content, let's first introduce what pre-layout and post-layout are.

There is such a scenario: we have 3 items [a, b, c], where a and b are displayed on the screen, when we delete b, c will be displayed.

insert image description here
What we would like to see is that c slides smoothly from the bottom to its new position.

But how does this happen?

We know where c will end up in the new layout, but how do we know where it should start sliding?

Google's solution provides the following:

After the adapter is changed, the RecyclerView requests two layouts from the LayoutManager.

The first one - pre-layout, because we can receive changes from the adapter, so here we can do some special handling. In our case, since we now know that b is removed, we will additionally display c, even though it is out of bounds.

The second - post-layout, a normal layout, corresponding to the changed adapter state.

insert image description here

Now, by comparing the position of c in pre-layout and post-layout, we can animate it correctly.

Thinking about this animation, This kind of animation — when the animated view is not present either in previous layout or in the new one — is called predictive animation.

Consider another scenario: what if b was only changed, not deleted?

insert image description here

The answer is that C will still be placed behind in the pre-layout stage! why? Because it is impossible to predict what the animation of C is, maybe the animation makes the height of b smaller, then c needs to be displayed, if not, then C will be put into the cache.

Introduction to caching

Just look at the code first:

public final class Recycler {
    final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
    ArrayList<ViewHolder> mChangedScrap = null;

    final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

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

scrap

The scrap cache is the cache that recyclerView searches first. There are many cache call graphs on the Internet. The first cache call is scrap.

scrap is not empty only during layout. When LayoutManager starts layout (pre-layout or post-layout), it will put all viewHolders into scrap. Then one by one is retrieved, unless some views have changed.

insert image description here

 Here is a digression, that is, some people may ask, why should I put it in the scrap first, and then take it out?

My point of view is: LayoutManager manages the layout, and Recycler manages the cache. LayoutManager should not know which viewHolder is valid or not, this is a separation of duties design.

scrap is divided into two collections: mAttachedScrap and mChangedScrap . These two are special, I tracked the timing of adding mAttachScrap, in the onLayoutChildren method of LinearLayoutManager:

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...
        onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
        // 这里调用了 mAttachedScrap.add(holder);
        // 这里也调用了 mChangedScrap.add(holder);
        detachAndScrapAttachedViews(recycler);
    ...
}

The timing of the call here is worth thinking about. It is put into the cache when it is laid out. Here it is explained that the cache is for the View.

Then the question arises, why should the display on the screen be cached? My thinking tends to be to reduce the impact of layout method calls.

For example, when we call the notifyItemRangeChanged method, the requestLayout method will be triggered, and the layout will be rearranged. If the layout is rearranged, the viewHolder will be placed in the scrap first (the changes on the screen are placed in mChangedScrap, and the rest are placed in mAttachedScrap. ), and then when filling the layout, take it out of mAttachedScrap and use it directly. The viewHolder in mChangedScrap will be moved to RecycledViewPool, so the item corresponding to mChangedScrap needs to take the corresponding viewHolder from the pool, and then re-bind.

insert image description here

 Now think about it again, why do you need two caches, mChangedScrap and mAttachedScrap?

Because mChangedScrap indicates that the item has changed, it may be a data change, or a type change, so its viewHolder cannot be reused, and it can only go to RecycledViewPool to re-fetch the corresponding, and then re-bind.

Then there is one thing to note: mChangedScrap can only be used in pre-layout, mAttachedScrap can be used in pre-layout and post-layout.

Before continuing the discussion, there are also a few differences between the methods that need to be explained:

  • Detach and remove in View

1. The implementation of detach in ViewGroup is very simple. It just removes the current View from the ChildView array of ParentView and sets the mParent of the current View to null, which can be understood as a lightweight temporary remove.
2.remove represents the real removal, not only is it removed from the ChildView array, but other connections with the View tree will be completely cut off.

  • Scrap View in Recycled View

1. Scrap View refers to the cache that has undergone the detach operation in RecyclerView. Such caches are matched by position and do not need to be re-bindView.
2. Recycled View refers to the cache after the real removal operation, and it needs to be used again by bindView when it is removed.

cache and pool

Both cache and pool are stored in Recycled View and need to be added to the list again.

mCachedViews , this is relatively simple.

It is an ArrayList type, does not distinguish the type of viewHolder, the size is limited to 2, but you can use the method setItemViewCacheSize () to adjust its size.

Since it does not distinguish the type of viewHolder, it can only get viewHolder according to position.

RecycledViewPool , which stores each type of viewHolder

The maximum number is 5, and the storage capacity of each type can be set by the setMaxRecycledViews() method.

Another important point is that multiple lists can share a RecycledViewPool using the setRecycledViewPool() method.

By the way, the difference in the use of each cache, it is also good to have a general understanding of each cache pool:

  • If the viewHolder is not found in all caches, the create and bind methods are called.
  • If found in the pool (RecycledViewPool), re-add and display, and then call the bind method.
  • If you find it in the cache (mCachedViews), you don't need to do anything, just add it again and display it, no bind is needed.

Therefore, you need to pay attention to their differences. A viewHolder entering the cache is different from entering the pool.

Now, let's think about the next question: the size of mCachedViews is limited, what if it can't be saved?

In fact, although mCachedViews is an ArrayList, it works a bit like a linked list. When mCachedViews is full, it will remove the first stored element and put it into the pool, as shown below:

insert image description here

When we slide the list, once the item exceeds the screen, it will be put into mCachedViews. If it is full, the "tail" element will be moved to the pool. If the pool is also full, it will be Discard and wait for recycling.

Here are a few scenarios to consolidate what we've learned:

scene one:

insert image description here

 Look at the left side of the picture first (assuming there is nothing in the cache and pool at this time), when sliding down, 3 first enters mCachedViews, followed by 4 and 5, 5 will squeeze 3 out, and 3 will run into the pool. .

Look at the right side of the picture again, and when you continue to slide down, 4 is squeezed out by 6 and placed in the pool. At the same time, 8 needs to be displayed, so it will be taken from the pool first, and there is exactly one 3, then it will be taken out, Bring 3 back to the screen.

Scenario two:
insert image description here

 If, after swiping down 7 is displayed, instead of continuing to slide down, but swiping up, what will happen?

Looking at the right side of the figure, it is obvious that 5 is taken out of the cache and reused directly without rebinding, and 7 is put into the cache.

Think about it, how should we take advantage of this situation?

For example, we have a list of wallpaper libraries, and users often slide up and down (left and right), then we increase the cache capacity to get better performance. However, for lists such as feed streams, users rarely return, so increasing the cache capacity does not make much sense.

Going deeper, we continue to slide up, then, 4 and 7 will be put into the cache, and 3 will be taken out of the pool, however, we need to pay attention here, because 3 is taken out of the pool, so it needs to be re-bound Yes, but logically, if the data in position 3 has not changed, it does not need to be re-bound and is valid. So, you can also use this as an optimization point, in the onBindViewHolder() method, check it.

Going a little deeper, in the process of sliding, there should always be only one viewHolder of a type in the pool (unless you use GridLayoutManager) , so if there are multiple viewHolders in your pool, they are scrolling. is basically useless.

Scenario three:

insert image description here

 When we call notifyDataSetChanged() or notifyItemRangeChanged(i, c) (when the range of c is very large), then many viewHolders will eventually be put into the pool, because the pool can only put 5, so the excess will be discarded , waiting for recycling. The most important thing is that recreate and bind will have a great impact on performance. If your list can hold many rows and use the notifyDataSetChanged method frequently, then you should consider setting the size.

recyclerView.getRecycledViewPool().setMaxRecycledViews(0, 20);

ViewCacheExtension

This needs to be customized, and the use is very limited, so it will not be introduced in depth.

Because it requires you to create the viewHolder yourself and cache it, then the problem comes. When we delete or add an item, the AdapterHelper calls back to the RecyclerView to notify it that it needs to handle the change. RecyclerView iterates over the currently displayed viewHolders and moves them. But there is a bug here, RecyclerView doesn't know the viewHolder you created at all, so it doesn't care about your own cached viewHolder.

So, if you want to use it, you need to meet some conditions:

  • Fixed location, for example, advertising space.
  • will not change
  • A reasonable amount, it doesn't matter if it is kept in memory.

Stable Ids

We said before that when notifyDataSetChanged is called, the recyclerView doesn't know what happened, so it can only think that everything has changed, that is, put all the viewHolders into the pool.

insert image description here

 However, if we set the stable ids, then it will be different:

insert image description here

 viewHolder is put into scrap, not pool. Note, here, its performance has improved a lot!

  • No need to re-bind, re-create a new viewHolder, no need to re-addView. addView causes remeasurement...
  • It turns out that we need to call notifyItemMoved(4, 6), but now it's fine to call notifyDataSetChanged() directly, but I tested it without animation.

At this point, you should be able to answer the following questions:

  • The difference between notifyDataSetChanged and notifyItemRangeChanged?
  • Difference between RecyclerView and ListView cache? This question, even if you don't know the caching mechanism of ListView, should be able to say something.

How to perform performance optimization on a list? The reason for the flickering when calling notifyDataSetChanged?

Reference documentation

In-depth RecyclerView | Open Source Lab

RecyclerView caching principle, there are pictures and the truth - Nuggets

RecyclerView source code analysis | Magic

Anatomy of RecyclerView: a Search for a ViewHolder (continued)

RecyclerView mechanism analysis: ChildHelper

RecyclerView notifyDataSetChanged The real culprit that causes the picture to flicker

Anatomy of RecyclerView: a Search for a ViewHolder

Guess you like

Origin blog.csdn.net/ahou2468/article/details/122991610