The Caching Principle of RecycleView

Posted by TheBurtle on Thu, 09 May 2019 10:00:03 +0200

In the previous article, when we went to the layoutChunk method of the layout process of the sub-View, we used View view = layoutState.next(recycler); the method obtained the sub-View to be laid out, and then proceeded to follow up. Now let's see how this sub-View was obtained.

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

You can see that it was obtained through the recycler.getViewForPosition method

  public View getViewForPosition(int position) {
            return getViewForPosition(position, false);
        }
  View getViewForPosition(int position, boolean dryRun) {
            return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
        }

The tryGetViewHolderForPositionByDeadline method returns a ViewHolder object, and then directly returns the member variable itemView of the ViewHolder, which is the child view currently being laid out.

So far, there is no code for caching. We can also guess that the cache of RecycleView is not only the View cache, but also the View Holder cache.

TryGetViewHolder ForPositionByDeadline is the entrance to the real caching mechanism. It's the method in RecycleView.Recycler. According to our normal thinking, it must be from the cache first, but we can't get a new one. So what's the cache? Before entering the tryGetViewHolder ForPositionByDeadline method, let's look at some members in Recycler first. Variables make it easy to see the following code

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

These are the lists for caching

  • mAttachedScrap: Not participating in reuse, save only the currently displayed ViewHolder list that is stripped from RecycleView at the time of repositioning. For example, when we insert or delete a piece of data, we need to re-layout. What can we do? The way is to save the view displayed on the current screen into a list and then re-layout it. This list is mAttached Scrap. So it just stores the ViewHolder that was stripped from RecycleView before the repositioning and does not participate in reuse.
  • MUnmodifiable Attached Scrap: Put mAttached Scrap in through Collections. unmodifiable List (mAttached Scrap) and return an immutable list for external retrieval.
  • mChangedScrap: Do not participate in reuse from the new layout to modify put in this, the rest put in mAttachedScrap
  • mCachedViews: As you can see from the name, this is the list that participates in the cache
  • mRecyclerPool: Participates in the cache, and the ViewHolder information inside it is reset, equivalent to a new ViewHolder for later use
  • MViewCache Extension: This is a caching strategy that allows us to extend ourselves. Normally we don't write this on our own.

So, mCachedViews, mRecyclerPool, mViewCacheExtension make up a three-tier cache. When RecyclerView wants to take a reusable ViewHolder, the order of search is mCachedViews - > mViewCacheExtension - > mRecyclerPool. Because we don't normally write mViewCacheExtension, we generally cache mCachedViews - > mRecyclerPool at two levels

In fact, mCachedViews is not involved in real recycling. The function of mCachedViews is to save the latest removed ViewHolder, through the removeAndRecycleView(view, recycler) method. Its function is, when you need to update the ViewHoder, whether the exact match just removed that, if it is taken out directly to make the RecycleView layout, if not, even if it has View. Holder, instead of returning, goes to mRecyclerPool to find a new ViewHolder and reassign it. The exact matching step in mAttachedScrap is the same as mCachedViews.

Under OK, let's go into the tryGetViewHolderForPositionByDeadline method and see how it works.

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;
            // 0) Whether it is in the pre-layout state is found in mChangedScrap. The pre-layout state is the time of re-layout.
            if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }
            // 1) Look in mAttachedScrap first, but not in mCachedViews.
            if (holder == null) {
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                if (holder != null) {
                    if (!validateViewHolderForOffsetPosition(holder)) {
                        // Check if it's the viewholder we're looking for, or if it's not removed?
                        if (!dryRun) {
                            // we would like to recycle this but need to make sure it is not used by
                            // animation logic etc.
                            holder.addFlags(ViewHolder.FLAG_INVALID);
                            if (holder.isScrap()) {
                                removeDetachedView(holder.itemView, false);
                                holder.unScrap();
                            } else if (holder.wasReturnedFromScrap()) {
                                holder.clearReturnedFromScrapFlag();
                            }
                            //Recycle this holder into mCachedViews or mRecyclerPool
                            recycleViewHolderInternal(holder);
                        }
                        holder = null;
                    } else {
                        fromScrapOrHiddenOrCache = true;
                    }
                }
            }
            if (holder == null) {
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
                    throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
                            + "position " + position + "(offset:" + offsetPosition + ")."
                            + "state:" + mState.getItemCount() + exceptionLabel());
                }

                final int type = mAdapter.getItemViewType(offsetPosition);
                // 2) Find from mAttachedScrap accurately by id
                if (mAdapter.hasStableIds()) {
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    if (holder != null) {
                        // update position
                        holder.mPosition = offsetPosition;
                        fromScrapOrHiddenOrCache = true;
                    }
                }
                //If we customize the caching policy
                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 (view != null) {
                        holder = getChildViewHolder(view);
                        if (holder == null) {
                            throw new IllegalArgumentException("getViewForPositionAndType returned"
                                    + " a view which does not have a ViewHolder"
                                    + exceptionLabel());
                        } else if (holder.shouldIgnore()) {
                            throw new IllegalArgumentException("getViewForPositionAndType returned"
                                    + " a view that is ignored. You must call stopIgnoring before"
                                    + " returning this view." + exceptionLabel());
                        }
                    }
                }
                if (holder == null) { // fallback to pool
                    if (DEBUG) {
                        Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
                                + position + ") fetching from shared pool");
                    }
                    //Go to mRecyclerPool to find different holder s according to different type s.
                    //type is the getItemViewType we wrote in adapter
                    holder = getRecycledViewPool().getRecycledView(type);
                    if (holder != null) {
                        holder.resetInternal();
                        if (FORCE_INVALIDATE_DISPLAY_LIST) {
                            invalidateDisplayListInt(holder);
                        }
                    }
                }
                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;
                    }
                    //createViewHolder creation cannot be invoked in the cache
                    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 there is no bound data
                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);
                //Call bindViewHolder
                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            }
            //Set Layout Params for itemView in holder
            final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
            final LayoutParams rvLayoutParams;
            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;
        }

From the above source code can summarize the process, first go to mAttachedScrap. To see if the View has just been peeled off, if it is returned directly, if not, to find in mCachedViews, mCachedViews is precise search, if found back, can not find or match, go to mRecyclerPool to find a new ViewHolder, if not found, can only call onCreateViewHolder to create a new one.

mAttachedScrap and mCachedViews are precise lookups. The ViewHolder found is already bound to the data and will not call onBindViewHolder to rebind the data. The ViewHolder in mRecyclerPool is a clean blank ViewHolder. After finding it, we need to call onBindViewHolder to rebind the data, which we can follow from the second step in the code above. Look at the getScrapOrCachedViewForId method

ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {
            // Look in our attached views first
            final int count = mAttachedScrap.size();
            for (int i = count - 1; i >= 0; i--) {
                final ViewHolder holder = mAttachedScrap.get(i);
                //Is the id consistent or not returned from Scrap?
                //Yes, not mCachedViews.
                if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
                    if (type == holder.getItemViewType()) {
                        holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                        if (holder.isRemoved()) {
                            if (!mState.isPreLayout()) {
                                holder.setFlags(ViewHolder.FLAG_UPDATE, ViewHolder.FLAG_UPDATE
                                        | ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED);
                            }
                        }
                        return holder;
                    } else if (!dryRun) {
                        mAttachedScrap.remove(i);
                        removeDetachedView(holder.itemView, false);
                        quickRecycleScrapView(holder.itemView);
                    }
                }
            }

            // Search the first-level cache
            final int cacheSize = mCachedViews.size();
            for (int i = cacheSize - 1; i >= 0; i--) {
                final ViewHolder holder = mCachedViews.get(i);
                //To determine whether the id is consistent, it is only returned that it is not placed in mRecyclerPool and removed from mCachedViews
                if (holder.getItemId() == id) {
                    if (type == holder.getItemViewType()) {
                        if (!dryRun) {
                            mCachedViews.remove(i);
                        }
                        return holder;
                    } else if (!dryRun) {
                        recycleCachedViewAt(i);
                        return null;
                    }
                }
            }
            return null;
        }

You can see that in the above code, when you find the holder from the corresponding cache, you will judge whether it is the holder you want, and if it is, you will return.

So how does RecycleView reuse? There are many entries, such as through the recycleView method (recycler.recycleView) in Recycler.

     public void recycleView(View view) {
            // This public recycle method tries to make view recycle-able since layout manager
            // intended to recycle this view (e.g. even if it is in scrap or change cache)
            ViewHolder holder = getChildViewHolderInt(view);
            if (holder.isTmpDetached()) {
                removeDetachedView(view, false);
            }
            if (holder.isScrap()) {
                holder.unScrap();
            } else if (holder.wasReturnedFromScrap()) {
                holder.clearReturnedFromScrapFlag();
            }
            recycleViewHolderInternal(holder);
        }

This method is used to reclaim separated views and place the specified views in the cache pool for rebinding and reuse. Finally, the recycleViewHolderInternal method is invoked, and the recycleViewHolderInternal method is the final method of recycleViewHolderInternal. Some entries call this method directly.

void recycleViewHolderInternal(ViewHolder 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 oldest cached view
                    int cachedViewSize = mCachedViews.size();
                    //The value of mViewCacheMax is 2, so there are at most two caches in mCachedViews
                    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                    //Put the oldest from mCachedViews into mRecyclerPool according to the first-in-first-out principle
                        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;
                    }
                    //Put the recently recycled ViewHolder in mCachedViews 
                    mCachedViews.add(targetCacheIndex, holder);
                    cached = true;
                }
                //If you don't set it to mCachedViews, put it in mRecyclerPool
                if (!cached) {
                    addViewHolderToRecycledViewPool(holder, true);
                    recycled = true;
                }
            } else {
                // NOTE: A view can fail to be recycled when it is scrolled off while an animation
                // runs. In this case, the item is eventually recycled by
                // ItemAnimatorRestoreListener#onAnimationFinished.

                // TODO: consider cancelling an animation when an item is removed scrollBy,
                // to return it to the pool faster
                if (DEBUG) {
                    Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "
                            + "re-visit here. We are still removing it from animation lists"
                            + exceptionLabel());
                }
            }
            // 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;
            }
        }

From this we see familiar mCachedViews and mRecyclerPool, which also shows that the RecycleView recycling mechanism has nothing to do with mAttachedScrap.

Where on earth did this recycle come from? The first place is to call detachAndScrapAttachedViews(recycler) in LayoutManager's onLayoutChildren method; the other is to call the removeAndRecycleView method when Recyclerview slides.

Detaach AndScrapAttached Views are used only before layout, stripping all child view s and placing them in mAttached Scrap for later re-layout.

When removeAndRecycleView scrolls, it marks ViewHolder as removed. It is cached in mCachedViews. The maximum capacity of mCachedViews is 2. If the mCachedViews is full, take out the first cached data and put it in mRecyclerPool. The default cache is 5 in mRecyclerPool. Then the latest is cached in mCachedViews.

OK, end.

Reference resources:

https://www.cnblogs.com/dasusu/p/7746946.html

https://www.jianshu.com/p/504e87089589

https://blog.csdn.net/harvic880925/article/details/84866486

Topics: REST