When you use a ScrollView, all of its child views are loaded at once. And the correct use of RecyclerView can be loaded on demand, binding on demand, and reuse. This paper mainly analyzes the principle of RecyclerView cache reuse.

Get the ViewHolder process overview from the cache

The general flow from the cache is shown below:

Description:

Before creating ViewHolder RecyclerView will first try to obtain from the cache to see if there is conform to the requirements of the ViewHolder, see Recycler# tryGetViewHolderForPositionByDeadline method

  • The first time, try to get it from mChangedScrap.
    • This attempt will only be made if mstate.ispreLayout () is true, which is the pre-layout phase.
    • The concept of “pre-layout” will be introduced later.
  • Second, getScrapOrHiddenOrCachedHolderForPosition ViewHolder ().
    • Try to find ViewHolder from 1. mAttachedScrap 2.mHiddenViews 3.mCachedViews
      • MAttachedScrap and mCachedViews are both member variables of Recycler
      • If the ViewHolder is successfully obtained, its validity is verified,
        • If the inspection fails, recycle it to RecyclerViewPool
        • Successful test can be directly used
  • The third time, if give Adapter set stableId, call getScrapOrCachedViewForId attempts to acquire ViewHolder.
    • The difference is that the search was performed by position, and now the search is performed by ID
  • Fourth, mViewCacheExtension is not null, the call ViewCacheExtension# getViewForPositionAndType method attempts to acquireView
    • Note: ViewCacheExtension is set by the developer and is null by default, and we generally don’t set it. This layer of caching can be ignored for the most part.
  • The fifth time. Try to obtain it from RecyclerViewPool. Compared with mCachedViews, the ViewHolder object obtained from mRecyclerPool successfully does not do legitimacy and item position check, but only check whether the viewType is consistent.
    • A ViewHolder removed from the RecyclerViewPool needs to be bind again to use it.
  • If the previous five attempts fail, create a new ViewHolder by calling recyclerView.adapter #createViewHolder
  • Finally, depending on the state of the ViewHolder, determine whether to call bindViewHolder for data binding.

The problem

What is pre-layout, predictive animation?

Understanding “pre-layout” requires understanding “predictive animation”. Consider this scenario:

The user has three items A, B and C. A and B are just displayed on the screen. At this time, the user deletes B, and finally C will be displayed in the original position of B

It will be more intuitive if C slides smoothly from the bottom to where B was before. But it’s actually not that easy to do. Because we only know where C ends up, but we don’t know where C starts, so we don’t know where C should slide from. If you assume that C should slide from the bottom, based on the final state, there’s probably something wrong with that. Because in other layoutManagers, it might slide in from the side or somewhere else.

So, based on the difference between the original state and the final state, can we figure out what kind of switch animation we should do? Again, the answer is no. Because in the original state, C doesn’t exist at all. (At this point, we didn’t know that B was going to be deleted, and loading C would probably be a waste of resources.)

The engineers who designed RecyclerView did it this way. When the Adapter changes, RecyclerView causes LayoutManager to do the layout twice.

  • The first is pre-layout. Layout all the items in their original state. And according to the Notify information of the Adapter, we know which items are about to change, so we can load another View. In the above example, knowing that B has been deleted, you can load C off screen as well.
  • Second, the final layout, that is, the layout after the changes are made.

This way you can figure out what animation to perform by comparing the layout changes before and after.

An animation where the view executing the animation does not exist in the original or new layout is called a predictive animation.

Prelayout is a step towards predictive animation.

The following two giFs show the difference between normal animation and predictive animation:

Normal animation 👇

Prediction animation 👇

Those interested in predictive animation can read this article further.

About the Scrap

Scrap cache list (mChangedScrap, mAttachedScrap) is the first place that RecyclerView looks up ViewHolder. It is very different from RecyclerViewPool or ViewCache.

MChangedScrap and mAttachedScrap are only used in the layout phase. Other times they are empty. After the layout is complete, the viewHolder in these two caches will be moved to mCacheView or RecyclerViewPool.

When LayoutManager starts the layout (pre-layout or final layout), all views in the current layout, Will be dump into the scrap (concrete implementation visible LinearLayoutManager# onLayoutChildren () method calls the detachAndScrapAttachedViews ()), LayoutManager then retrieves the views one by one, and unless something happens to the views, it immediately returns to its original position from the Scrap.

In the example above, we remove B and call notifyItemRemove to trigger the layout rearrangement. Then a, B, and C are dumped into the Scrap. LayoutManager will retrieve A and C from the Scrap.

Off topic, where is B at this point? RecyclerView sees that B is not present in the final layout, unscrapes it, makes it perform a vanishing animation and then hides it. After the animation is executed, B is put into RecyclerViewPool.

Why does LayoutManager need to detach first and then reattach the views instead of just removing the changing child views? Scrap cache lists exist to separate concerns/responsibilities between LayoutManager and RecyclerView.Recycler. LayoutManager does not need to know which child views should be retained or recycled to the pool or anywhere else. It’s the Recycler’s job.

In addition to the fact that the layout is not empty, there is another rule related to scrap: All scrap views are separated from RecyclerView. The attachView and detachView methods in a ViewGroup are similar to the addView and removeView methods, but do not trigger events that require the layout to be redrawn. They simply remove the corresponding child view from the ViewGroup’s child view list and set the parent of that child view to NULL. The detached state must be temporary, followed by the attach or remove event

If you have added a bunch of subviews when calculating a new layout, you can detach them all and Recyclerview does this.

Attached vs Changed scrap

In the Recycler class we can see two separate scrap containers: mAttachedScrap and mChangedScrap. Why do you need two?

ViewHolder will only be added to mChangedScrap if: When the associated item changes (notifyItemChanged or notifyItemRangeChanged is called), And call ItemAnimator ViewHolder# canReuseUpdatedViewHolder method, return false. Otherwise, the ViewHolder is added to the AttachedScrap.

CanReuseUpdatedViewHolder returns “false” said we have to perform with a view to replace another view of animation, such as animation fades. “True” indicates that the animation takes place inside the View.

MAttachedScrap can be used throughout the layout, but Changed Scrap – can only be used during the pre-layout phase.

This makes sense: after the layout, the new ViewHolder should replace the “changed” view, so AttachedScrap is useless after the layout. After the change animation is complete, change Scrap will be transferred to the pool as expected

The default ItemAnimator can reuse an updated ViewHolder in three cases:

  • Call the setSupportsChangeAnimations (false).
  • NotifyDataSetChanged was called instead of notifyItemChanged or notifyItemRangeChanged.
  • Provides such changes content: adapter. NotifyItemChanged (index, anyObject).

The last case shows a good way to avoid creating/binding a new ViewHolder when you just want to change some internal elements.

What is Hidden Views?

As mentioned earlier, on the second attempt to retrieve the ViewHolder, there is a substep that searches from the hidden View. What is the hidden View here? Hidden Views are views that are moving away from RecyclerView boundaries. In order for these views to perform the corresponding separation animation correctly, they are still preserved as child views of RecyclerView.

From LayoutManager’s point of view, these views no longer exist and therefore should not be included in the calculation. For example, call LayoutManager#getChildAt while part of the view is performing a vanishing animation. These views are not included in the subscript. All method calls to getChildAt(), getChildCount(), addView(), and so on from LayoutManager are handled by ChildHelper before being applied to the actual recyclable view, ChildHelper’s job is to recalculate the index between the unhidden child view list and the full child view list.

Remember, we are searching for views to provide to LayoutManager, but LayoutManager should not know to hide views!

An actual 🌰 : The baffling “bouncing from hidden views” mechanism is necessary for bouncing from hidden views. Consider this scenario where we insert an item and then delete it immediately before the insert animation is complete:

What we want to see is that the position where B is when it’s removed from C starts shifting up. But at that time, B was a hidden view! If we ignore it (the “hidden” B), it will result in the creation of a new B underneath the existing B. Even worse, the two views will overlap, because the new B will go up, and the old B will go down. To avoid this error, in one of the earlier steps of searching ViewHolder, RecyclerView asks ChildHelper if it has a suitable Hidden View. “Appropriate” means that the view is associated with the desired location, has the correct view type, and is not hidden to remove it (we should not resurrect the removed view).

If I have a view like this, RecyclerView will return to the LayoutManager and add it to the preLayout to tag from the animation processing position (see recordAnimationInfoIfBouncedHiddenView method).

What? Shouldn’t it be LayoutManager’s responsibility to add content before and after a layout? How can RecyclerView add view to preLayout now? Yes, this mechanism may seem a bit of a duty, but it is also important to understand it.

What does a Stable Id do?

The most important thing to understand about stable Id is that it affects the behavior of RecyclerView only after the notifyDataSetChanged method is called.

If the Adapter doesn’t set hasStableId when notifyDataSetChanged, the RecyclerView doesn’t know what happened and which things changed, so it assumes that everything changed, Each ViewHolder is invalid, so use RecyclerViewPool instead of Scrap.

If we had a Stable Id, it would look like this:

The ViewHolder will go into scrap instead of pool. The ViewHolder is then looked up in scrap by the specified Id (the Id obtained by getItemId in the Adapter) instead of postion.

What are the benefits?

  1. It does not cause RecyclerViewPool overflow, so there is no need to create a new ViewHolder if it is not necessary. The previous ViewHolder is rebound, because just because the Id hasn’t changed doesn’t mean the content hasn’t changed
  2. The biggest benefit is support for animation. Move item4 to the position of Item6 above. Normally, we need to call notifyItemMoved(4,6) to get a move animation. But this is also supported by calling notifyDataSetChanged with a stable ID. Because RecyclerView can see where a view with a particular ID is on the old and new layouts,
    • Note that this animation only supports simple animations, not predictive animations. If we see some ID in the new layout but not in the old layout, how do we know if it’s a newly inserted item or an item moved in from somewhere, in which case it actually came from? Normally, the answers to these questions would be found in the pre-layout, which is out of RecyclerView based on adapter changes, but in this case, we don’t know what those changes are

Overall, the usage scenarios for stable IDS seem limited. However, there is a usage scenario where migrating from ListView to RecyclerView can be painful to convert all notifyDataSetChanged calls to notifications of specific changes. In this case, stable ID can give you a simple RecyclerView animation.

Cache optimization practices

  • Try to use the notifyItemXxx method for fine-grained notification updates instead of notifyDatasetChanged

    • If there are two data sets before and after the change and it is not clear which data items have changed, consider using DiffUtil.
    • If the data set is large, it is recommended to use AsyncListDiffer to perform diff operations on child threads.
  • RecyclerView#getRecycledViewPool()#setMaxRecycledViews(viewType,1); To adjust the size of the cache to reduce memory footprint

  • If there are too many items for a particular viewType and you have to update the data with notifyDataSetChange, you can increase the cache before the change and decrease the cache after the change. This also allows layout changes to maximize reuse of existing ViewHolder.

    mRecyclerView.getRecycledViewPool().setMaxRecycledViews(0, the total number of items displayed on the screen +7 );
    mAdapter.notifyDataSetChanged();
    new Handler().post(new Runnable() {
        @Override
        public void run(a) {
            mRecyclerView.getRecycledViewPool()
                    .setMaxRecycledViews(0.5); }});Copy the code
  • If every item in RecyclerView is a RecyclerView, and the item type of sub-recyclerView is the same, RecyclerView#setRecycledViewPool() can be used; Method to realize the reuse of cache pool.

References and learning resources are recommended

  • Android. Jlelse. Eu/anatomy – of -…
  • www.programmersought.com/article/455…
  • Juejin. Cn/post / 684490…
  • Juejin. Cn/post / 684490…

Due to my limited level, I may make mistakes due to misunderstanding or clerical errors. If you find any problems or have any questions about the content of this article, please let me know in the comments section below. Thank you!