RecyclerView's cache acquisition mechanism

RecyclerView's cache acquisition mechanism

RecyclerView is a control that we have more contact with in development. The official definition for him is "A flexible view for providing a limited window into a large data set." There is a large data in the definition that is very eye-catching, so how does RecyclerView handle a large amount of data without oom and lag? This is the caching mechanism in RecyclerView. First, let's look at a few basic concepts:

  • Binding: The subview displays the data corresponding to its location in the adapter.
  • Recycle (view): The itemview previously used to display data at a certain location in the adapter is cached and will be reused later to display the same type of data, which can avoid inflation and construction of the layout.
  • Scrap (view): ItemView that enters the temporary detached state during layout. The scrap view can be reused without completely detach the parent view RecyclerView, if it does not need to be re-bound, it can not be modified; if the view is considered dirty, it can be bound by the adapter Binding.
  • Dirty (view): Dirty's ItemView refers to the View that needs to be re-Binded by the adapter.

Recycler class

There is an internal class Recycler in RecyclerView to manage discarded or separated item views for reuse. Let's first look at the source code of Recycler, as follows:

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

It can be seen from the source code that this class has 5 member variables

  1. mAttachedScrap: Stores the Scrap view of the attached parent view RecycleView, which can be reused without rebinding. There is no capacity limit.
  2. mChangedScrap: Stores the changed Scrap view. If it is reused, it needs to be bound by the adapter again.

mChangedScrap 和 mAttachedScrap 只在布局阶段使用。其他时候它们是空的。布局完成之后,这两个缓存中的 viewHolder,会移到 mCacheView 或者 RecyclerViewPool 中。

  1. mCachedViews: Stores the removed view, the view that has been separated from the RecyclerView, but still stores the data information of position and Binding, and the default capacity is 2.
  2. mRecyclerPool: Stores the views that have been restored to factory settings, without any traces of Binding. Let's take a look at the source code of RecycledViewPool, we can see that the default capacity is 5. There is a ScrapData internal class, which stores ViewHolder, indicating that it is stored separately according to different types of data. This is how the multi-seed layout of RecyclerView is implemented.
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;
            long mCreateRunningAverageNs = 0;
            long mBindRunningAverageNs = 0;
        }
        SparseArray<ScrapData> mScrap = new SparseArray<>();
}
  1. mViewCacheExtension: This level of cache developers can extend the cache, ViewCacheExtension is an abstract class, if necessary, you can define your own implementation.
    public abstract static class ViewCacheExtension {

        @Nullable
        public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position, int type);
    }

Read cache rules

After talking about the storage type and form of the cache, let's take a look at the storage rules of the cache. There are a total of 38 class files under the RecyclerView package, and only RecyclerView itself has 13,501 lines of code. It is unrealistic to read all of them, so we look at the specific caching strategy by looking up references.

First of all, we checked the use of mAttachedScrap, as shown in the figure below, we can see that the main adding and deleting methods are: void scrapView (View view) and void unscrapView (ViewHolder holder). We continue to explore in terms of finding usage references.

 Finally found the method tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs), and found that the superior call of this method is getViewForPosition(int position, boolean dryRun), let's take a look at its source code,

ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {

            // 0) If there is a changed scrap, try to find from there
         	if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }
            // 1) Find by position from scrap/hidden list/cache
            if (holder == null) {
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
			}
           // 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;
                    }
           }
			if (holder == null && mViewCacheExtension != null) {
                    // We are NOT sending the offsetPosition because LayoutManager does not
                    // know it.
                    final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
                }
           if (holder == null) { // fallback to pool       
                    holder = getRecycledViewPool().getRecycledView(type);
           }
			//3 creating it directly
           if (holder == null) {
          
               holder = mAdapter.createViewHolder(RecyclerView.this, type);
           }

           return holder;
        }

From this method, we can see that the notes written by Google engineers are really good, and they are directly marked with the serial number 0123. Step 1: If it is in the pre-layout state, it will be obtained from changed scrap, that is, mChangedScrap. Specifically, first obtain it through position, if it is empty, then obtain it through stableid, if stableid is set.

        ViewHolder getChangedScrapViewForPosition(int position) {
            // If pre-layout, check the changed scrap for an exact match.
            final int changedScrapSize;
            if (mChangedScrap == null || (changedScrapSize = mChangedScrap.size()) == 0) {
                return null;
            }
            // find by position
            for (int i = 0; i < changedScrapSize; i++) {
                final ViewHolder holder = mChangedScrap.get(i);
                if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
                    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                    return holder;
                }
            }
            // find by id
            if (mAdapter.hasStableIds()) {
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                if (offsetPosition > 0 && offsetPosition < mAdapter.getItemCount()) {
                    final long id = mAdapter.getItemId(offsetPosition);
                    for (int i = 0; i < changedScrapSize; i++) {
                        final ViewHolder holder = mChangedScrap.get(i);
                        if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) {
                            holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                            return holder;
                        }
                    }
                }
            }
            return null;
        }

Step 2: From the scrap/hidden list/cache, call the getScrapOrHiddenOrCachedHolderForPosition method to obtain the view. This method obtains the view from the attach scrap, hidden children, and cache in sequence through the position. We know that attach scrap is obtained from the mAttachedScrap cache, and cache is obtained from the mCachedViews cache. So what are hidden children? By digging into the code, we have the variable final List mHiddenViews in the class ChildHelper; hidden views refer to those views that are breaking away from the RecyclerView boundary. In order for these views to perform the corresponding separation animations correctly, they are still retained as subviews of the RecyclerView.

Step 3: If the cache was not obtained in the previous step, if the adapter has set a stableId, it will be obtained in mAttachedScrap through the stableId.

Step 4, look in mViewCacheExtension, we said that this object is null by default, it is a layer of cache strategy customized by our developers, so if you have not defined it, you cannot find View here.

Step 5, get it from RecycledViewPool.

Step 6: If none of the above 5 steps are obtained, create it directly through the createViewHolder method of the adapter.

Supplementary knowledge StableID

If the Adapter doesn't set hasStableId when notifyDataSetChanged is called, RecyclerView doesn't know what happened, which things changed, so it assumes that everything has changed, and every ViewHolder is invalid, so they should be placed in RecyclerViewPool instead of scrap. If there is a stableId ViewHolder will enter the scrap instead of the pool. Then search the ViewHolder in the scrap through a specific Id (the id obtained by getItemId in the Adapter) instead of the postion.

optimization suggestion

  •  Try to use notifyItem***() related methods for partial notification updates instead of notifyDataSetChanged()
  •  It is recommended to use DiffUtil for data set changes. If the data set is relatively large, you can also combine AsyncListDiffer to perform diff operations in sub-threads.
  •  If there is only one item of a specific viewType, you can use RecyclerView.getRecycledViewPool() .setMaxRecycledViews(viewType,1); to adjust the size of the cache area to reduce memory usage
  •  If RecyclerView nests RecyclerView, you can use RecyclerView.setRecycledViewPool(@Nullable RecycledViewPool pool); to reuse the cache pool of RecyclerView.

Guess you like

Origin blog.csdn.net/challenge51all/article/details/120137666