Level 4 cache?

A lot of articles in the introduction of RecyclerView cache will mention RecyclerView four levels of cache, first look at this is what level?

  • Level 1 cache:mAttachedScrap,mChangedScrap
  • Level 2 cache:mCachedViews
  • Level 3 cache:ViewCacheExtensionCustom cache
  • Level 4 cache:RecycledViewPoolBuffer pool

So is RecyclerView really made up of four levels? Slowly to see

The four levels of caching described above are defined by RecyclerView’s internal class Recycler. It is also the main class to implement RecyclerView cache logic.

/ mViewCacheMax = DEFAULT_CACHE_SIZE final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>(); ArrayList<ViewHolder> mChangedScrap = null; final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>(); /** * private ViewCacheExtension mViewCacheExtension; /** * RecycledViewPool mRecyclerPool;Copy the code

The mViewCacheMax variable determines the maximum capacity of mCachedViews, and its final value is composed of two parts:

void updateViewCacheSize() { int extraCache = mLayout ! = null ? mLayout.mPrefetchMaxCountObserved : 0; mViewCacheMax = mRequestedCacheMax + extraCache; }Copy the code

MLayout. MPrefetchMaxCountObserved in RecyclerView: It represents the number of viewholders to be prefetched each time. You can imagine that different LayoutManager values will be different. For example, the LinearLayout only needs to prefetch one ViewHolder at a time. The GridLayoutManager prefetches SpanCount ViewHolder at a time. The value of mRequestedCacheMax can be set by calling the set method. So if you don’t have any special modification mViewCacheMax is equal to 3 in LinearLayoutManager, mViewCacheMax is equal to 2 plus SpanCount in GridLayoutManager.

Get the ViewHolder cache

And then let’s look at how RecyclerView is evaluated from the cache. RecyclerView in the layout will call the following method to obtain the View.

public View getViewForPosition(int position) {
    return getViewForPosition(position, false);
}

View getViewForPosition(int position, boolean dryRun) {
    return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
Copy the code

In tryGetViewHolderForPositionByDeadline () method contains the ViewHolder was obtained from the cache and create new ViewHolder and binding data logic.

/** * @param position gets the position of the ViewHolder in the list * @param dryRun whether the next frame refresh time from @param deadlineNs is used for the preloading process * @return */ RecyclerView.ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) { RecyclerView.ViewHolder holder = null; // 0) If there is a changed scrap, ViewHolder if (mstate.ispreLayout ()) {holder = getChangedScrapViewForPosition(position); } // 1) Find by position from Scrap /hidden list/cache // 1) Obtain cache ViewHolder if (holder) from mAttachedScrap/mCachedViews == null) { holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); } if (holder == null) { final int type = mAdapter.getItemViewType(offsetPosition); // 2) Find from scrap/cache via stable ids, If exists // 2) Obtain ViewHolder if (madapter.hasstableids ()) {holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun); if (holder ! = null) { // update position holder.mPosition = offsetPosition; }}} ViewHolder 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); ViewHolder if (holder == null) {// Fallback to pool holder = getRecycledViewPool().getRecycledView(type); if (holder ! = null) {// Clean up information held in ViewHolder to rebind data holder.resetInternal(); If (holder == null) {long start = getNanoTime(); 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; } holder = mAdapter.createViewHolder(RecyclerView.this, type); long end = getNanoTime(); mRecyclerPool.factorInCreateTime(type, end - start); 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()) { final int offsetPosition = mAdapterHelper.findPositionOffset(position); bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs); } return holder; }Copy the code

Here the code in the method is simplified to make it easier to understand how RecyclerView gets the ViewHolder. As you can see, RecyclerView tries to get the ViewHolder of the cache from mChangedScrap, mAttachedScrap, mCachedViews, mViewCacheExtension, and mRecyclerPool in order. If not, create a new ViewHolder with createViewHolder(). And then determine whether ViewHolder instance needs to call tryBindViewHolderByDeadline () bindViewHolder binding data () method.

Cache ViewHolder

How can RecyclerView retrieve a ViewHolder from the cache? How can a ViewHolder be cached? Let’s look at them one by one.

First, mViewCacheExtension and mRecyclerPool can be used to replace mRecyclerPool by mViewCacheExtension, so they belong to the same level of function. By default, ViewHolder removed from mCacheViews is cached by RecycledViewPool. To use ViewHolder in RecycledViewPool, call bindViewHolder() to bind data again.

What is the relationship between mChangedScrap, mAttachedScrap and mCachedViews?

First is mCacheView, its cache ViewHolder logic in recycleViewHolderInternal () method. In the prefetch article introduced after perform prefetch logic for the ViewHolder will pass recycleViewHolderInternal () method for caching.

See RecyclerView: Prefetch for more information about prefetch logic.

MChangedScrap and mAttachedScrap will cache ViewHolder only when scrapView() is called and it will be called when RecyclerView layout is used. OnLayout () can be triggered by calling the notifyItem***() method to track its call logic with breakpoints.

There are two important ways to RecyclerView cache:

  • recycleViewHolderInternal()
  • scrapView()

First see recycleViewHolderInternal () method:

Void recycleViewHolderInternal (ViewHolder holder) {/ / determine whether elements in mCacheViews fill int cachedViewSize = mCachedViews. The size ();  If (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {// If (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {// If (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {// If (cachedViewSize >= mViewCacheMax && cachedViewSize > 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 the ViewHolder of the new cache to mCacheViews McAchedviews. add(targetCacheIndex, holder); cached = true; } if (! cached) { addViewHolderToRecycledViewPool(holder, true); recycled = true; }}Copy the code

As you can see, the ViewHolder of the cache is first added to the mCacheViews collection. When the mCacheViews is filled, the first element is removed and added to the cache pool.

ScrapView ()

void scrapView(View view) { final ViewHolder holder = getChildViewHolderInt(view); / / ViewHolder removed, invalid data, data not update, whether to use the same ViewHolder execution when data updated animation if (holder. HasAnyOfTheFlags (ViewHolder. FLAG_REMOVED | ViewHolder.FLAG_INVALID) || ! holder.isUpdated() || canReuseUpdatedViewHolder(holder)) { holder.setScrapContainer(this, false); mAttachedScrap.add(holder); } else { if (mChangedScrap == null) { mChangedScrap = new ArrayList<ViewHolder>(); } holder.setScrapContainer(this, true); mChangedScrap.add(holder); }}Copy the code

Both methods require less code and are easy to understand rather than explain, just look at the comments. It’s important to note that the only way I’ve found to implement the mChangedScrap. Add (holder) logic is to call notifyItemChange(int), and the target ViewHolder is added to the mChangedScrap collection, The reason for this will be explained below.

So how do these two methods get triggered?

Known prefetching is triggered recycleViewHolderInternal () one of the ways. ScrapView () is invoked during the Layout phase to cache all viewholders visible on the screen into the mAttachedScrap/mChangedScrap collection. It is retrieved from the cache at layout time.

Call addViewInt() during layout to add a child View to the layout as follows:

if (holder.isScrap()) {
    holder.unScrap();
}
Copy the code

Used to clean ViewHolder reused in mAttachedScrap. To reuse the ViewHolder will perform at the end of the layout recycleViewHolderInternal () to be added to the mCachedViews cache. Consider the ViewHolder removed from the screen by calling notifyItemAdd(), notifyItemRemoved().

Private void dispatchLayoutStep3() {... . / / recycling not reuse the ViewHolder mLayout removeAndRecycleScrapInt (mRecycler); . // Remove mCangedScrap Cache collection if (mrecycler. mChangedScrap! = null) { mRecycler.mChangedScrap.clear(); }... } void removeAndRecycleScrapInt(Recycler recycler) { final int scrapCount = recycler.getScrapCount(); for (int i = scrapCount - 1; i >= 0; i--) { recycler.quickRecycleScrapView(scrap); } recycler.clearScrap(); }Copy the code

But if the recycler. QuickRecycleScrapView breakpoint (), will find that the code doesn’t go here. This is because of the default animation in RecyclerView. The ViewHolder in mAttachedScrap in ItemAnimator is used to execute the animation. When the animation is finished, the ViewHolder will be cached or used for other operations. Cache is still by unScrap () and recycleViewHolderInternal () method. The following Settings make breakpoints work:

recyclerView.itemAnimator = null
Copy the code

RecyclerView can cache ViewHolder by RecyclerView.

Figure in onLayout caching logic in the process of setting the recyclerView. ItemAnimator = null

conclusion

So now that’s the end of RecyclerView cache, is RecyclerView really a tier 4 cache? I think we have answers to our questions. RecyclerView is a three-level cache, but there are four kinds of cache methods, divided into the following two situations:

  • Slide: ExecutemCachedViews + RecycledViewPoolA secondary cache scheme to optimize sliding
  • Relayout: Execute(mAttachedScrap + mChangedScrap) + mCachedViews + RecycledViewPoolA three-level caching scheme to optimize the relayout process.

Unanswered questions

One more question, why do we need mChangeScrap and mAttachedScrap to save data when we rearrange? From the code it’s because mChangeScrap is only used during pre-layout, and when pre-layout is done, The corresponding ViewHolder cannot be retrieved from either mAttachedScrap or mCacheViews when the actual layout is performed. Instead, the ViewHolder can only be retrieved from the cache pool (or createViewHolder() is called) to perform the data update operation. But why not use the mFlags field to differentiate? Not more convenient. Welcome to discuss this question.