In the previous article RecyclerView source code analysis (a) — drawing process analysis introduced the RecyclerView drawing process, RecyclerView by drawing process extracted from View, into LayoutManager, Make RecyclerView in different LayoutManager, have different styles, make RecyclerView unusually flexible, greatly strengthen the use of RecyclerView scene.

Of course, RecyclerView cache mechanism is also a unique advantage of it, reduce the occupation of memory and repeated drawing work, therefore, this paper aims to introduce and learn the design idea of RecyclerView cache.

When we talk about muddling, we must go through the create-cache-reuse process. Therefore, the RecyclerView cache mechanism is also carried out according to the following steps.

Create ViewHolder (VH)

When talking about the measurement of sub-ItemView, layoutChunk method will first obtain each itemView, and then add it to RecyclerView after obtaining it. So let’s look at the creation process first:

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

Next is to call the getViewForPosition method of RecyclerView to get a View. GetViewForPosition method will call to RecyclerView tryGetViewHolderForPositionByDeadline method.

tryGetViewHolderForPositionByDeadline

This method is long, but the logic is simple. The first part of the process is to try to fetch the VH from the cache. If not, a new VH is created, the data is bound, and the VH is bound to LayoutParams (LP).

ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) { if (position < 0 || position >= mState.getItemCount()) { throw new IndexOutOfBoundsException("Invalid  item position " + position + "(" + position + "). Item count:" + mState.getItemCount() + exceptionLabel()); } boolean fromScrapOrHiddenOrCache = false; ViewHolder holder = null; 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; } / / create the VH 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"); } } } // This is very ugly but the only place we can grab this information // before the View is rebound and returned to  the LayoutManager for post layout ops. // We don't need this in pre-layout since the VH is not updated by the LM. if (fromScrapOrHiddenOrCache && ! mState.isPreLayout() && holder .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) { holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST); if (mState.mRunSimpleAnimations) { int changeFlags = ItemAnimator .buildAdapterChangeFlagsForAnimations(holder); changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT; final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState, holder, changeFlags, holder.getUnmodifiedPayloads()); recordAnimationInfoIfBouncedHiddenView(holder, info); } } boolean bound = false; if (mState.isPreLayout() && holder.isBound()) { // do not update unless we absolutely have to. holder.mPreLayoutPosition  = position; } else if (! holder.isBound() || holder.needsUpdate() || holder.isInvalid()) { if (DEBUG && holder.isRemoved()) { throw new IllegalStateException("Removed holder should be bound and it should" + " come here only in pre-layout. Holder: " + holder + exceptionLabel()); } final int offsetPosition = mAdapterHelper. FindPositionOffset (position); / / data binding bound = tryBindViewHolderByDeadline (holder, offsetPosition, position, deadlineNs); } final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); Final LayoutParams rvLayoutParams; // Bind VH to LP, LP and Settings to ItemView if (LP = = null) {rvLayoutParams = (LayoutParams) generateDefaultLayoutParams (); holder.itemView.setLayoutParams(rvLayoutParams); } else if (! checkLayoutParams(lp)) { rvLayoutParams = (LayoutParams) generateLayoutParams(lp); holder.itemView.setLayoutParams(rvLayoutParams); } else { rvLayoutParams = (LayoutParams) lp; } rvLayoutParams.mViewHolder = holder; rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound; return holder; }Copy the code

Even if you omit the intermediate logic to look up VH from the cache, the rest of the code is still long. I’ll summarize tryGetViewHolderForPositionByDeadline method do:

  1. Lookup VH from cache;
  2. The cache does not, so create a VH;
  3. Determine the VH need not to need to update the data, if need will call tryBindViewHolderByDeadline binding data;
  4. Bind VH to LP, which in turn is set to ItemView.

This concludes the logic for creating VH.

The cache

While introducing the logic for adding to the cache, it is also necessary to introduce cache-specific classes and variables.

Overall cache design

As can be seen from the figure, RecyclerView cache is a four-level cache architecture. Of course, according to the RecyclerView code comments, there are only three levels of cache, that is, mCachedViews is level 1 cache, mViewCacheExtension is level 2 cache, and mRecyclerPool is level 3 cache. From the developer’s point of view, mAttachedScrap and mChangedScrap are opaque to developers, and there is no official way to change their behavior.

Caching mechanism Recycler

Recycler is an internal class of RecyclerView. Let’s look at its main member variables.

  • MAttachedScrap ViewHolder of the visible range in the cache screen

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

  • MChangedScrap Cache ViewHolder to be separated from RecyclerView when sliding, according to the position or ID of the child View cache, the default maximum store 2

    ArrayList<ViewHolder> mChangedScrap = null;

  • MCachedViews ViewHolder Cache list. The size is determined by mViewCacheMax. DEFAULT_CACHE_SIZE is 2 and can be set dynamically.

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

  • ViewCacheExtension a layer of caching that developers can customize and is an instance of the virtual ViewCacheExtension class, Developers can implement methods getViewForPositionAndType (Recycler Recycler, int position, int type) to implement its own cache.

    private ViewCacheExtension mViewCacheExtension;

  • RecycledViewPool ViewHolder Cache pool. If the ViewHolder cannot be stored in the limited mCachedViews, the ViewHolder will be stored in the RecyclerViewPool.

    RecycledViewPool mRecyclerPool; 

Add to cache

VHS are created to be cached and reused, so when are they added to the cache? Again, the LinearLayoutManager is used here. In RecyclerView source code analysis (a) – drawing process analysis has mentioned a method:

Public void onLayoutChildren(RecyclerView. Recyclerer Recycler, recyclerview. State State) {//... DetachAndScrapAttachedViews (recycler); / /... }Copy the code

OnLayoutChildren is drawing the child view. Pair in the view will call detachAndScrapAttachedViews method first, take a look at this method.

detachAndScrapAttachedViews

Let’s take a look at this method:

// recyclerview public void detachAndScrapAttachedViews(@NonNull Recycler recycler) { final int childCount = getChildCount(); for (int i = childCount - 1; i >= 0; i--) { final View v = getChildAt(i); // Every view can be scrapOrRecycleView(Recycler, I, V); } } 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 the VH isInvalid and has been removed, another logic is taken if (viewholder.isinvalid () &&! viewHolder.isRemoved() && ! mRecyclerView.mAdapter.hasStableIds()) { removeViewAt(index); recycler.recycleViewHolderInternal(viewHolder); } else {// detachViewAt(index) detachViewAt(index); recycler.scrapView(view); mRecyclerView.mViewInfoStore.onViewDetached(viewHolder); }}Copy the code

That is, in the logic above, is put in cache. You can see it right here

  1. If it is remove, it will be executedrecycleViewHolderInternal(viewHolder) Method, which will eventually add the ViewHolder to the CacheView and Pool,
  2. Detach, on the other hand, adds views to ScrapViews

One thing to point out is that you need to distinguish between two concepts, Detach and Remove

  1. detach: The implementation in ViewGroup is simple, just remove ChildView from ParentView’s ChildView array, The mParent of ChildView is set to null, which is a lightweight temporary remove function. This function is often used to change the order of childViews in the ChildView array. View detach is usually temporary and will be reattached later.
  2. remove: Real removed, not only be deleted from ChildView array, contact the View tree each other will be cut off completely (regardless of the Animation/LayoutTransition this special circumstances), focus is cleared, for example, has been removed from TouchTarget etc.

recycleViewHolderInternal

Here are two logical ways to Recycler:

/** * internal implementation checks if view is scrapped or attached and throws an exception * if so. * Public version UN - scraps before calling recycle. * / void recycleViewHolderInternal (ViewHolder holder) {/ /... Omit the previous code, the front is doing inspection final Boolean transientStatePreventsRecycling = holder. DoesTransientStatePreventRecycling (); @SuppressWarnings("unchecked") final boolean forceRecycle = mAdapter ! = null && transientStatePreventsRecycling && mAdapter.onFailedToRecycleView(holder); boolean cached = false; boolean recycled = false; if (DEBUG && mCachedViews.contains(holder)) { throw new IllegalArgumentException("cached view received recycle internal?  " + holder + exceptionLabel()); } if (forceRecycle || holder.isRecyclable()) { if (mViewCacheMax > 0 && ! holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE | Viewholder.flag_adapter_position_unknown)) {// Retire cached view Int cachedViewSize = McAchedviews.size (); if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) { recycleCachedViewAt(0); cachedViewSize--; } int targetCacheIndex = cachedViewSize; if (ALLOW_THREAD_GAP_WORK && cachedViewSize > 0 && ! mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) { // when adding the view, skip past most recently prefetched views int cacheIndex = cachedViewSize - 1; while (cacheIndex >= 0) { int cachedPos = mCachedViews.get(cacheIndex).mPosition; if (! mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) { break; } cacheIndex--; } targetCacheIndex = cacheIndex + 1; } // add to cache McAchedviews. add(targetCacheIndex, holder); cached = true; } if (! cached) { addViewHolderToRecycledViewPool(holder, true); recycled = true; } } else { } // even if the holder is not removed, we still call this method so that it is removed // from view holder lists. mViewInfoStore.removeViewHolder(holder); if (! cached && ! recycled && transientStatePreventsRecycling) { holder.mOwnerRecyclerView = null; }}Copy the code

This method does the following:

  1. Verify the validity of the VH to ensure it is no longer in use;
  2. Determine the size of the cache, remove it when it exceeds it, and then find a suitable place to add it.
  3. If it cannot be added to CacheViews, it is added to a Pool.

mCachedViews

The data structure corresponding to mCachedViews is also an ArrayList, but the cache has a limit on the size of the collection, which is 2 by default. The properties of the ViewHolder in this cache are the same as those of the mAttachedScrap; as long as the position or itemId matches, it is clean and does not need to be rebound. Developers can call the setItemViewCacheSize(size) method to change the size of the cache. A common scenario for this level of cache triggering is a sliding RV. Of course, notifyXXX also triggers the cache. This cache is as efficient as mAttachedScrap.

RecyclerViewPool

RecyclerViewPool Cache Size can be set for multiple itemTypes. The default number of caches per ItemType is 5. And the cache can be shared to multiple RecyclerView. The default cache number is 5. Suppose a news App can display 10 news items on each screen, then the cache hit will inevitably fail and the frequent creation of ViewHolder will affect performance. So you need to increase the cache size.

scrapView

Let’s move on to scrapView:

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); // here false mattachedscrape.add (holder); } else { if (mChangedScrap == null) { mChangedScrap = new ArrayList<ViewHolder>(); } holder.setScrapContainer(this, true); // here is true mChangedScrap. Add (holder); }}Copy the code

 

This approach is simpler, with less logic to check. There are two types of cache that you can choose from, depending on the conditions, but I won’t expand the details, so you can see. Here are two scrapView caches.

mAttachedScrap

The corresponding data structure of mAttachedScrap is ArrayList. In LayoutManager#onLayoutChildren method, all views of RecyclerView will be temporarily stored in this collection when the views are laid out. For future use, the property of the ViewHolder in the cache is that if it matches the position or itemId on the RV, it is considered a clean ViewHolder and can be used directly without calling the onBindViewHolder method. There is no limit to the size of the ArrayList, and as many views as there are on the screen, the collection will be created.

The typical scenario that triggers this level of caching is to call the notifyItemXXX method. Calling the notifyDataSetChanged method triggers the level of cache use only if Adapter hasStableIds returns true.

mChangedScrap

MChangedScrap and mAttachedScrap are the same level of cache, they are equal. However, the call scenarios for mChangedScrap are notifyItemChanged and notifyItemRangeChanged, and only the ViewHolder that has changed will be placed into mChangedScrap. The ViewHolder in the mChangedScrap cache needs to be rebound by calling onBindViewHolder. The question then arises, why do two different caches need to be designed for the same level of cache?

In stage dispatchLayoutStep2 LayoutManager onLayoutChildren method will eventually call layoutForPredictiveAnimations method, The remaining ViewHolder from mAttachedScrap is filled onto the screen, so the difference is that the ViewHolder from mChangedScrap is not forced onto the RV if the RV is filled. Is there a way to get the ViewHolder that changed into the mAttachedScrap cache? B: Sure. NotifyItemChanged (int Position, Object Payload) The ViewHolder that has changed will be separated into mAttachedScrap.

Use the cache

Let’s move on to the last section, which uses caching. This has also been mentioned in the previous drawing space, so let’s directly look at the corresponding method:

/ / according to the position of the incoming get ViewHolder ViewHolder tryGetViewHolderForPositionByDeadline (int position, Boolean dryRun. Long deadlineNs) {--------- omits ---------- Boolean fromScrapOrHiddenOrCache = false; ViewHolder holder = null; / / layout Belongs to the special situation Obtained from the mChangedScrap ViewHolder if (mState isPreLayout ()) {holder = getChangedScrapViewForPosition (position); fromScrapOrHiddenOrCache = holder ! = null; } if (holder == null) {// Select ViewHolder from mAttachedScrap. Continue to get ViewHolder from mCachedViews try holder = getScrapOrHiddenOrCachedHolderForPosition (position, dryRun); -- -- -- -- -- -- -- -- -- -- to omit -- -- -- -- -- -- -- -- -- --} the if (holder = = null) {final int offsetPosition = mAdapterHelper. FindPositionOffset (position); --------- omit ---------- final int type = madapter.getitemViewType (offsetPosition); // If the Adapter has an Id declared, try to get it from the Id, Here does not belong to cache the if (mAdapter hasStableIds ()) = {holder getScrapOrCachedViewForId (mAdapter. GetItemId (offsetPosition), type, dryRun); } if (holder == null && mViewCacheExtension ! {mViewCacheExtension = ViewHolder;} The cache requires developers to realize the final View View. = mViewCacheExtension getViewForPositionAndType (this, the position, type); if (view ! = null) { holder = getChildViewHolder(view); }} if (holder == null) {// fallback to pool // getRecycledViewPool().getRecycledView(type); if (holder ! = null) {// the ViewHolder state will be reset if it is successfully retrieved, so we need to re-execute Adapter#onBindViewHolder binding holder.resetinternal (); if (FORCE_INVALIDATE_DISPLAY_LIST) { invalidateDisplayListInt(holder); }}} the if (holder = = null) {-- -- -- -- -- -- -- -- -- to omit -- -- -- -- -- -- -- -- -- -- / / 5, if the cache didn't find the corresponding ViewHolder, Will call in the Adapter onCreateViewHolder create a holder = mAdapter. CreateViewHolder (RecyclerView. This type); } } boolean bound = false; if (mState.isPreLayout() && holder.isBound()) { holder.mPreLayoutPosition = position; } else if (! holder.isBound() || holder.needsUpdate() || holder.isInvalid()) { final int offsetPosition = mAdapterHelper.findPositionOffset(position); // if you need to bind data, Invokes the Adapter# onBindViewHolder to bind data bound = tryBindViewHolderByDeadline (holder, offsetPosition, position, deadlineNs); } ---------- omit ---------- return holder; }Copy the code

The above logic is represented by a flow chart:

 

To summarize the process, ViewHolder obtained with mAttachedScrap, mCachedViews, and mViewCacheExtension does not need to recreate the layout and bind data; ViewHolder retrieved from the cache pool mRecyclerPool does not need to recreate the layout, but rebind the data; If the target ViewHolder is not retrieved from either of the caches, then Adapter#onCreateViewHolder is called to create the layout and Adapter#onBindViewHolder is called to bind the data.

ViewCacheExtension

We already know that ViewCacheExtension is level 3 caching and needs to be implemented by developers themselves, so in what context can ViewCacheExtension be used? And how does that happen?

First, we need to be clear that Recycler already has several levels of caching, so why should we have an interface to recycle it?

For that, check out the other caches in Recycler:

  1. mAttachedScrap A cache for processing the visible screen;
  2. mCachedViews The data stored there is based onposition To cache, but the data in it can be replaced at any time;
  3. “`mRecyclerPool In the pressviewType Go to the storeArrayList< ViewHolder>, somRecyclerPool Do not press theposition Go to the storeViewHolderAnd, inmRecyclerPool Take out theView I have to go every timeAdapter#onBindViewHolder ‘to rebind data.

If I now need to display a View at a particular location (position=0) all the time, and the contents of the View remain the same, then the best case scenario is that I do not need to recreate the View and rebind the data every time at a particular location. The above caches are obviously not applicable. What to do about this situation? You can do this by customizing the cache ViewCacheExtension.

Comparison of RecyclerView and ListView cache mechanism

Conclusion cited from: Android ListView and RecyclerView comparison analysis – cache mechanism

ListView and RecyclerView cache mechanisms are basically the same:

  1. MActiveViews and mAttachedScrap are similar in function. The purpose of mActiveViews is to quickly reuse the list item visible on the screen, ItemView, without the need to recreate view and bindView.

  2. MScrapView is similar to mCachedViews + mReyclerViewPool. The purpose of mScrapView is to cache items that leave the screen and reuse items that are about to enter the screen.

  3. The advantage of RecyclerView is that

    1. The use of mCacheViews can make the off-screen list item ItemView into the screen without bindView rapid reuse;
    2. MRecyclerPool can be used for multiple RecyclerView, in specific scenarios, such as viewpaper+ multiple list pages have the advantage. Objectively speaking, RecyclerView has strengthened and improved the cache mechanism of ListView in specific scenarios.

Different use scenarios: list page display interface, need to support animation, or frequent update, local refresh, RecyclerView is recommended, more powerful and perfect, easy to expand; In other cases (such as wechat card package list page), both are OK, but ListView will be more convenient and fast to use.

 

Refer to the article

www.jianshu.com/p/2b19e9bcd…

www.jianshu.com/p/6e6bf58b7…

www.jianshu.com/p/e1b257484…

RecyclerView loads so many graphs, why doesn’t it crash?