In the use of RecyclerView will think about how it shows so much data is how to achieve smooth sliding, it is how to do internal cache, cache what things and other problems, this article on the cache mechanism of this piece of the discussion, will the problem listed in a directory

  • RecyclerViewHow many levels of caching are there, and what does each level of caching do?
  • adapter.setHasStableIds(true)What does it do?
  • notifyHow does a series of functions update data?

1. An overview of the

In the previous article RecyclerView source code analysis 1- draw process has found the entry to get ItemView, a brief review, take the LinearLayoutManager as an example, Call onLayoutChildren()-> fill()->layoutChunk()->View View = layoutState. Next (Recycler) where layoutChunk() is recycled until the screen is filled in a certain direction, The next() inside is the entry to get the ItemView, but our Adapter creates a ViewHolder, and RecyclerView caches a ViewHolder. ItemView is just a member variable of the ViewHolder. Track the next () method is called to tryGetViewHolderForPositionByDeadline () to obtain a ViewHolder, So tryGetViewHolderForPositionByDeadline () is to use the cache entry, we analysis the method

2. Level 4 cache

I will RecyclerView cache is divided into four levels, some people are divided into three levels, this mainly depends on personal views, as long as we know the specific role of all levels of cache, here will be four levels of cache are listed, and a simple description of their role, if you do not understand, there are more detailed description below.

The Recycler internal class in RecyclerView is responsible for caching, so look directly at its member variables

// These two represent level 1 cache, used for layout
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;

// Level 2 cache, no need to rebind, mainly used for sliding
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

/ / l3 cache, abstract classes need our own implementation getViewForPositionAndType (), returns a View
private ViewCacheExtension mViewCacheExtension;

// Cache level 4, which needs to be rebound, can be used when none of the above is found
RecycledViewPool mRecyclerPool;
Copy the code

Call adapter.onbindViewholder (). The first two levels of cache use ArrayList to hold the ViewHolder. The third level cache needs to be implemented by ourselves and is not often used. The level 4 cache stores different Viewholders based on itemViewType, with each itemViewType holding five Viewholders by default, as shown in the figure below

We’re tryGetViewHolderForPositionByDeadline () to see how the level 4 cache is work

ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
    ViewHolder holder = null;
    // Select mChangedScrap from mChangedScrap by position and ID respectively
    if(mState.isPreLayout()) { holder = getChangedScrapViewForPosition(position); fromScrapOrHiddenOrCache = holder ! =null;
    }
    // Get from position
    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
    }
    if (holder == null) {
    // Obtain by id
    if (mAdapter.hasStableIds()) {
        holder =getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun);
    }
    // From the custom cache
    if (holder == null&& mViewCacheExtension ! =null) {
        final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
    }
    // Use RecycledViewPool
    if (holder == null) {
        holder = getRecycledViewPool().getRecycledView(type);
    }
    Call onCreateViewHolder to create a new ViewHolder
    if (holder == null) {
        holder = mAdapter.createViewHolder(RecyclerView.this, type);
    }
    // Call onBindViewHolder if binding is required
    if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
}

Copy the code

The above code is not difficult to understand, is obtained from all levels of cache, the current cache can not find the next level of cache, the detailed logic to look at the source code is clear. There are some things to note

  • parameterdeadlineNsHas to do with the prefetch mechanism when sliding
  • There is also a reference frommHiddenViewsGet the cache,mHiddenViewsThere are elements only during the animation, when the animation is over, they are naturally empty (this conclusion is based on checking other blogs, so I won’t discuss it yet)mHiddenViews)
  • In addition, according topositionIn addition, also according toidTo find the cacheViewHolderAnd this withadapter.setHasStableIds(true)There is a direct relationship, which will be discussed in detail below

This is the logic for retrieving the cache. Let’s look at when to add a ViewHolder to the cache and the details of the cache levels. But before we do that, we need to give a brief introduction to the ViewHolder

3. ViewHolder

To recyclerView.adapter, we need to rewrite onCreateViewHolder() and onBindViewHolder(), and cache is also ViewHolder. So take a look at what information is stored in the ViewHolder. The most important ones are (just a few of them are listed)

  1. itemViewOur layout view
  2. mPositionAdapterThe position of
  3. mItemIdrewriteAdapter.getItemId(int position)Can be used asViewHolderUnique identifier of
  4. mItemViewType itemViewThe type of
  5. mFlagssaidViewHolderThe current state of

MFlags has the following important flags (just to name a few)

state describe
FLAG_BOUND Bound, it’s already calledonBindViewHolder
FLAG_UPDATE Have update
FLAG_INVALID It’s no longer valid
FLAG_REMOVED Been deleted

4. Level 1 cache mAttachedScrap/mChangedScrap

Level 1 cache is used to cache elements on the screen when a data update request is reconfigured. We’ve already seen these two elements in the cache above. Now let’s look at when to add a ViewHolder to these two brothers. Global search adds ViewHolder to his two only scrapView(View View) methods

void scrapView(View view) {
    final ViewHolder holder = getChildViewHolderInt(view);
    if(holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID) || ! holder.isUpdated() || canReuseUpdatedViwHolder(holder)) {// Add ViweHolder marked invalid or deleted or not updated to mAttachedScrap
        holder.setScrapContainer(this.false);
        mAttachedScrap.add(holder);
    } else {
        if (mChangedScrap == null) {
            mChangedScrap = new ArrayList<ViewHolder>();
        }
        // Updated ViweHolder added to mChangedScrap
        holder.setScrapContainer(this.true); mChangedScrap.add(holder); }}Copy the code

CanReuseUpdatedViwHolder (holder) is related to mItemAnimator, which by default is equivalent to calling holder.isinvalid ()

So here’s what happens

  • FLAG_REMOVED or FLAG_INVALID or no change -> mAttachedScrap
  • FLAG_UPDATE -> mChangedScrap

Continuing to trace where scrapView() is called, there are two

  • When retrieving the cache from abovemHiddenViewsTo get toViewHolderAfter the callscrapView()This is not a common case involvingmHiddenViewsDon’t discuss
  • scrapOrRecycleView()

ScrapOrRecycleView is also an important method that determines whether the ViewHolder goes to level 1 or level 2 cache

private void scrapOrRecycleView(Recycler recycler, int index, View view) {
            final ViewHolder viewHolder = getChildViewHolderInt(view);
    if (viewHolder.shouldIgnore()) {
        return;
    }
    if(viewHolder.isInvalid() && ! viewHolder.isRemoved() && ! mRecyclerView.mAdapter.hasStableIds()) { removeViewAt(index);// mCachedViews is marked as invalid, not deleted, and has no ID
        recycler.recycleViewHolderInternal(viewHolder);
    } else {
        / / into the level 1 cache mAttachedScrap/mChangedScrapdetachViewAt(index); recycler.scrapView(view); mRecyclerView.mViewInfoStore.onViewDetached(viewHolder); }}Copy the code

And find out who’s calling scrapOrRecycleView, Found that if the user does not manually call related recycling method (generally do not call) eventually call source is detachAndScrapAttachedViews (recycler), in LinearLayoutManager. OnLayoutChildren () method, Called before fill(), the View on the screen needs to be stored in the cache as a ViewHolder before the layout, in the following case

For cache reclaim paths, different states of the ViewHolder go into different caches. We need to be aware of these three methods, which are mentioned several times in the following examples:

detachAndScrapAttachedViews(recycler)->scrapOrRecycleView() -> scrapView(View view)

Roughly remember the functions of the three methods as follows:

  1. detachAndScrapAttachedViewsAfter requestLayout, reclaim the on-screen data to the cache entry
  2. scrapOrRecycleView()Recycle to level 1 cache or level 2 cache
  3. scrapView(View view)Reclaim to level 1 cachemAttachedScrapormChangedScrap

Some diagrams are covered in the detailed examples below, and the elements in the diagrams are explained

  • notifyItemInserted

NotifyItemInserted and notifyItemRangeChanged both work the same way, so we’ll just say one. As shown in figure we will be “a” is inserted into the location of the index = = 1, then calls the adapter. NotifyItemInserted (1). Compared with before and after adding data, A has not changed at all, while B, C and D have only changed their positions, so we can directly calculate their changed positions, that is, add their viewholder. mPosition + 1, These can be found in the source offsetPositionRecordsForInsert

MFlags of A, B, C, and D do not change. ScrapOrRecycleView () does not allow viewholder.isinvalid (). A, B, C, and D are not changed in scrapView(View View), so they are added to mAttachedScrap

Cache reuse A, B and C can take out is used directly, do not need binding, because there is no any changes in their content, and “A”, certainly not in the second level cache mCachedViews, because the second level cache is coming in the screen or just left of the screen buffer, so from the Pool Pool or create A new, All must be bound again via onBindViewHolder

NotifyItemInserted Caches the contents of the screen to level 1 cachemAttachedScrapAnd can be reused directly without binding. Only new elements that enter the screen need to be bound

  • notifyItemRemoved

adapter.notifyItemRemoved(1)
ViewHolder.mPosition - 1
FLAG_REMOVED

According to the cache recovery path analysis mentioned above, A, B, C, and D were all valid, but B was tagged FLAG_REMOVED, so they were all put into level1 cache. In the judgment of level1 cache, viewholder. mFlags of A, C, and D remained unchanged and put into mAttachedScrap. B is FLAG_REMOVED so it’s going to be added to mAttachedScrap

In the case of cache reuse, A, C and D can be taken out and used directly without binding. E may come from the second-level cache mCachedViews, because it is the element that is going to enter the screen. In this case, no binding is required. All must be bound again via onBindViewHolder

Summary: notifyItemRemoved caches on-screen content to level 1 cachemAttachedScrap, and can be reused directly without binding, which may or may not be required for new elements entering the screen

  • notifyDataSetChanged

notifyDataSetChanged
markKnownViewsInvalid()
ViewHolder
FLAG_UPDATE
FLAG_INVALID
The second level cachemCachedViewsData is emptied and moved toRecycledViewPool

In scrapOrRecycleView() viewholder.isinvalid () &&! viewHolder.isRemoved() && ! MRecyclerView. MAdapter. HasStableIds () these three conditions are met, the ViewHolder into the second level cache, but the second level cache is not what all want, throw into RecycledViewPool for invalid directly. Therefore, the state of the cache at this time is that the level-1 and level-2 caches have no data, and level-3 caches have not been implemented. All caches are in RecycledViewPool and need to be re-bound to be displayed on the page

In the preceding figure, we deleted the first element, but there are still four elements in the RecycledViewPool, so there must be four caches in the RecycledViewPool, so we can reuse it and bind it again. We don’t need onCreateViewHolder to create it

Conclusion: NotifyDataSetChanged will flag the content on the screen as invalid, cache it to level 4 cache RecycledViewPool, and all elements on the screen need to be rebound. By the way, The setAdapter and swapAdapter caches are handled similarly to notifyDataSetChanged when called multiple times

  • notifyDataSetChanged — setHasStableIds(true)

notifyDataSetChanged
adapter.setHasStableIds(true)
getItemId(int position)
Cache reclamation path
scrapOrRecycleView()

    if(viewHolder.isInvalid() && ! viewHolder.isRemoved() && ! mRecyclerView.mAdapter.hasStableIds()) { removeViewAt(index);// Enter level 2 cache mCachedViews
        recycler.recycleViewHolderInternal(viewHolder);
    } else {
        / / into the level 1 cache mAttachedScrap/mChangedScrap
        detachViewAt(index);
        recycler.scrapView(view);
        mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
    }

Copy the code

Because the first if does not satisfy! MRecyclerView. MAdapter. HasStableIds () this condition, so will the ViewHolder cache on the screen to level cache, in the primary cache and because each ViewHolder is invalid in the judgment of the state, So put it in the mAttachedScrap

When cache reuse occurs, B, C, and D can all be retrieved from the cache by Id, but the onBindViewHolder binding is required because the state is inValid. For E, there is no data in the secondary cache, but it must be bound from the pool or a new one.

Conclusion: SetHasStableIds (true) saves the contents of the screen to level 1 cache, but it still needs to go through the binding process, which is faster than the default, but not much faster. I think setHasStableIds(True) is more useful for animating notifyDataSetChanged. Which judge logic in processAdapterUpdatesAndSetAnimationFlags (),

mState.mRunSimpleAnimations = mFirstLayoutComplete && mItemAnimator ! =null&& (mDataSetHasChangedAfterLayout || animationTypeSupported || mLayout.mRequestedSimpleAnimations) && (! mDataSetHasChangedAfterLayout || mAdapter.hasStableIds());Copy the code

And then the last one is, you know, you just have to try it out, so I’m not going to talk about it

  • notifyItemChanged

We analyzed the above cases and found that mChangedScrap in level 1 cache was not used at all. Since it is mentioned here, it must be used in this case, as shown in the figure

Here we change the first element, A, to A, and I’ve made mAttachedScrap and mChangedScrap two different colors,

A, B, C, and D will all be put into tier 1 cache according to the cache path analysis mentioned above. For tier 1 cache, B, C, and D will be put into Tier 1 cache unchanged, while A will be put into Tier 1 cache with FLAG_UPDATE. Not satisfied in scrapView(View View)! Holder.isupdated () so will be added to mChangedScrap

B, C, and D can be used directly in cache reuse. A needs to be bound again because it has been modified

Summary: notifyItemChanged saves the elements on the screen to the level 1 cache and the ones that have changed tomChangedScrapAnd needs to be rebound, unchanged saved tomAttachedScrapIn the.

The notifyItemChanged(int position, @nullable Object Payload) method may not be used very often, but it may be used if there is a refresh problem. Specific can refer to this article Android RecyclerView local refresh principle

From the above analysis, the application scenario of level 1 cache is only to cache the existing elements on the screen when the data changes and the layout needs to be rearranged. However, in the last stage of dispatchLayoutStep3, Call the mLayout. RemoveAndRecycleScrapInt (mRecycler) will mAttachedScrap and mChangedScrap content to empty.

Level 2 cache mCachedViews

Find mCachedViews add recycling ViewHolder place, found only in recycleViewHolderInternal (), the code is as follows

void recycleViewHolderInternal(ViewHolder holder) {...if (forceRecycle || holder.isRecyclable()) {
        if (mViewCacheMax > 0
                && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                        | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE)){
            int cachedViewSize = mCachedViews.size();
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                // If mCachedViews is full, the first collection will be put into the Pool
                recycleCachedViewAt(0); cachedViewSize--; } mCachedViews.add(targetCacheIndex, holder); }... }Copy the code

If we look at all the places where this method is called, we find that mCachedViews has many paths to recycle. Some are put in from the first level cache positon or Type pair, some are put in after the animation, etc. I think it is more important to recycle during the slide process, let’s take a look at this situation

The level 2 cache is not represented by a graph. The contents of the yellow dotted box in the figure above indicate that the level 2 cache mCachedViews is cached

As shown in the figure, we swipe up, the leftmost state means that the slide has not started yet, and the middle state means that the slide has started a little bit. If LayoutManager turns on the prefetch mechanism (default) and succeeds in obtaining the prefetch element (E in the figure means that the prefetch element is obtained through level 4 cache, The next element to be displayed on the screen will be placed into the second-level cache mCachedViews, (prefetching in the source code in the entry for RecyclerView onTouchEvent mGapWorker. PostFromTraversal (this, dx, dy)). The rightmost state E does not need to be bound when it enters the screen, because E has already been put into mCachedViews. When A has just drawn the screen, it will put A into mCachedViews. The recycleByLayoutState(layoutState) can be used in fill() to view the source code. Then, if you slide down again, the recycler can be displayed on the screen again. A will be retrieved from the cache mCachedViews without binding

Conclusion:mCachedViewsCacheView caches off-screen elements when sliding, and puts the first ones in the Pool when it reaches the upper limit. If we keep sliding in the same direction, CacheView doesn’t actually help us with efficiency. It just keeps caching ViewHolder that we’ve slid behind. If we slide up and down a lot, caching in CacheView is a good use because it doesn’t need to be created or bound to be reused.

Level 3 cache ViewCacheExtension

The need to implement our own getViewForPositionAndType (@ NonNull Recycler Recycler, int position, int type), it is important to note when calling this method, We return a View, and we should have created the View in advance and returned it when we called it, rather than creating the View when we called the method. I won’t talk about it if I don’t use it enough.

Level 4 Cache RecycledViewPool

The RecycledViewPool structure has been introduced previously. Different ItemViewTypes can cache 5 items respectively, and the default size can also be changed. The elements that are not cached in the first two levels of cache are cached here. Call onBindViewHolder() to re-bind the RecycledViewPool. Another RecycledViewPool feature is that multiple RecycleDViewPools can share the same Pool

conclusion

After the above analysis, we can know that RecyclerView has four levels of cache

  1. mAttachedScrapandmChangedScrapOnly when the data is updated, the data on the screen will be cached. After the layout is finished, the cache will be cleared. The cache is not used directly without binding as stated in other articles, but on a case-by-case basisadapter.setHasStableIds(true)andmChangedScrapThere are two specific cases where you need to bind. When the slidingRecyclerViewThe interior is throughscrollToNo layout is required, so no level 1 caching is required.
  2. mCachedViewsMainly in the swipe to the off-screen elements for recycling and reuse, here the cache of things can be used directly, no binding
  3. ViewCacheExtensionCustom caching, not used much
  4. RecycledViewPoolThe cacheViewHolderYou need to bind it again to use it

For data update method notify series functions:

  1. notifyItemXXX()The requirements are implemented with minimal changes, the cache is fully utilized, and the animation is also included, so we can usually update data in this way as much as possible.
  2. notifyDataSetChanged()Invalidates all elements on the screen, and every element needs to be rebound to appear on the screen, a heavyweight update.

The role of the adapter. SetHasStableIds (true)

  1. A new layer has been added to the cache that can be looked up by IdViewHolder, increase cache hit ratio, improve performance (but according to the abovenotifyDataSetChanged()There may be other ways to use ids to find caches, but I’m not aware of that yet.)
  2. notifyDataSetChanged()We have animation

Tip: RecyclerView can break point debugging, encountered do not understand more break point to try

Finally, due to the author’s limited level, if there is any mistake in the above analysis, welcome to put forward, I will correct it in time, so as not to mislead others

The resources

  1. Android RecyclerView local refresh principle
  2. RecyclerView (a) : prefetch mechanism