2

We know that RecyclerView can still slide smoothly when there is a large amount of data. How is it achieved? The reason why RecyclerView is easy to use is due to its excellent caching mechanism.

We know that RecyclerView itself is a ViewGroup, so you cannot avoid adding or removing child Views when sliding (the child View is created by onCreateViewHolder in RecyclerView#Adapter). If you use the child View every time you have to recreate it, it will definitely affect The smoothness of sliding, so the RecyclerView caches the ViewHolder through the Recycler (contains child views inside), so that the child views can be reused when sliding, and the data bound to the child views can also be reused under certain conditions. So in essence, the reason why RecyclerView can achieve a smooth sliding effect is because of the caching mechanism, because the cache reduces the time to repeatedly draw the View and bind the data, thereby improving the performance during sliding.

One, cache

1.1, four-level cache

The Recycler caches ViewHolder objects in 4 levels, and the priorities are as follows:

  • mAttachedScrap: cache the ViewHolder of the visible range on the screen;
  • mCachedViews: ViewHolder that will be separated from RecyclerView when the cache slides, the default is 2 at most;
  • ViewCacheExtension: custom-implemented cache;
  • RecycledViewPool: ViewHolder buffer pool, which can support different ViewType;

1.1.1 mAttachedScrap

mAttachedScrap stores the ViewHolder in the current screen, and the corresponding data structure of mAttachedScrap is ArrayList. The views are laid out when the LayoutManager#onLayoutChildren method is called. At this time, all the Views on the RecyclerView will be temporarily stored in this collection. The feature of ViewHolder is that if it matches the position or itemId on the RV, it can be used directly without calling the onBindViewHolder method.

1.1.2 mChangedScrap

mChangedScrap and mAttachedScrap belong to the same level of cache, but the calling scenarios of mChangedScrap are notifyItemChanged and notifyItemRangeChanged, and only the changed ViewHolder will be put into mChangedScrap. The ViewHolder in the mChangedScrap cache needs to call the onBindViewHolder method to rebind the data.

1.1.3 mCachedViews

When the mCachedViews cache slides, the ViewHolder that will be separated from the RecyclerView is cached according to the position or id of the child View, and the default is to store up to two. The data structure corresponding to mCachedViews is ArrayList, but the cache has a limit on the size of the collection.

The characteristics of ViewHolder in the cache are the same as those in mAttachedScrap, and there is no need to re-bind data as long as the position or itemId corresponds. Developers can call the setItemViewCacheSize(size) method to change the size of the cache. A common scenario triggered by this level of cache is sliding RecyclerView. Of course, calling notify() will also trigger the cache.

1.1.4 ViewCacheExtension

ViewCacheExtension is a cache that needs to be implemented by the developer. Basically all data on the page can be implemented through it.

1.1.5 RecyclerViewPool

The ViewHolder buffer pool is essentially a SparseArray, where the key is ViewType (int type), and the value stores ArrayList<ViewHolder>. By default, each ArrayList stores up to 5 ViewHolders.

1.2 Comparison of Level 4 Cache

Cache levelInvolvedinstructionWhether to recreate the view ViewWhether to rebind the data
Level 1 cachemAttachedScrap mChangedScrapViewHolder of the visible range in the cache screenfalsefalse
Secondary cachemCachedViewsThe ViewHolder that will be separated from the RecyclerView when the cache slides is cached by the position or id of the child Viewfalsefalse
Level 3 cachemViewCacheExtensionCaching implemented by the developer
Level 4 cachemRecyclerPoolThe ViewHolder buffer pool is essentially a SparseArray, where the key is ViewType (int type), and the value stores ArrayList<ViewHolder>. By default, each ArrayList stores up to 5 ViewHolders.falsetrue

1.3 Calling process

Usually, onTouchEvent#onMove is triggered when RecyclerView slides, recycling and reuse of ViewHolder will start here. We know that LayoutManager needs to be set when setting RecyclerView, LayoutManager is responsible for the layout of RecyclerView, including the acquisition and reuse of ItemView. Taking LinearLayoutManager as an example, the following methods will be executed in sequence when RecyclerView is re-layout:

  • onLayoutChildren(): The entry method for layout of RecyclerView
  • fill(): Responsible for continuously filling the remaining space, the method called is layoutChunk()
  • layoutChunk(): Responsible for filling the View, the View is finally found by finding a suitable View in the cache class Recycler

The entire call chain mentioned above: onLayoutChildren()->fill()->layoutChunk()->next()->getViewForPosition(), getViewForPosition() is to obtain the appropriate View from the Recycler implementation class of RecyclerView's recycling mechanism.

Second, the reuse process

RecyclerView reuses ViewHolder from the next() method of LayoutState. When LayoutManager lays out itemView, it needs to obtain a ViewHolder object, as shown below.

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

The next method calls the getViewForPosition method of RecyclerView to get a View, and the getViewForPosition method will eventually call the tryGetViewHolderForPositionByDeadline method of RecyclerView, and the core of RecyclerView's real reuse is here.

    @Nullable
    ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
        ViewHolder holder = null;
        // 0) 如果它是改变的废弃的ViewHolder,在scrap的mChangedScrap找
        if (mState.isPreLayout()) {
            holder = getChangedScrapViewForPosition(position);
            fromScrapOrHiddenOrCache = holder != null;
        }
        // 1)根据position分别在scrap的mAttachedScrap、mChildHelper、mCachedViews中查找
        if (holder == null) {
            holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        }

        if (holder == null) {
            final int type = mAdapter.getItemViewType(offsetPosition);
            // 2)根据id在scrap的mAttachedScrap、mCachedViews中查找
            if (mAdapter.hasStableIds()) {
                holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
            }
            if (holder == null && mViewCacheExtension != null) {
                //3)在ViewCacheExtension中查找,一般不用到,所以没有缓存
                final View view = mViewCacheExtension
                        .getViewForPositionAndType(this, position, type);
                if (view != null) {
                    holder = getChildViewHolder(view);
                }
            }
            //4)在RecycledViewPool中查找
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                holder.resetInternal();
                if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
                }
            }
        }
        //5)到最后如果还没有找到复用的ViewHolder,则新建一个
        holder = mAdapter.createViewHolder(RecyclerView.this, type);
    }

As you can see, the tryGetViewHolderForPositionByDeadline() method obtains the ViewHolder from scrap, CacheView, ViewCacheExtension, and RecycledViewPool respectively, and creates a new ViewHolder if not.

2.1 getChangedScrapViewForPosition

In general, when we call the notifyItemChanged() method of the adapter, when the data changes, the item is cached in mChangedScrap, and the ViewHolder obtained subsequently needs to be re-bound to the data. At this time, the ViewHolder will be searched in the mChangedScrap of scrap by position and id respectively.

   ViewHolder getChangedScrapViewForPosition(int position) {
        //通过position
        for (int i = 0; i < changedScrapSize; i++) {
            final ViewHolder holder = mChangedScrap.get(i);
            return holder;
        }
        // 通过id
        if (mAdapter.hasStableIds()) {
            final long id = mAdapter.getItemId(offsetPosition);
            for (int i = 0; i < changedScrapSize; i++) {
                final ViewHolder holder = mChangedScrap.get(i);
                return holder;
            }
        }
        return null;
    }

2.2 getScrapOrHiddenOrCachedHolderForPosition

If the view is not found, search in the mAttachedScrap, mChildHelper, and mCachedViews of the scrap according to the position. The methods involved are as follows.

    ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
        final int scrapCount = mAttachedScrap.size();

        // 首先从mAttachedScrap中查找,精准匹配有效的ViewHolder
        for (int i = 0; i < scrapCount; i++) {
            final ViewHolder holder = mAttachedScrap.get(i);
            return holder;
        }
        //接着在mChildHelper中mHiddenViews查找隐藏的ViewHolder
        if (!dryRun) {
            View view = mChildHelper.findHiddenNonRemovedView(position);
            if (view != null) {
                final ViewHolder vh = getChildViewHolderInt(view);
                scrapView(view);
                return vh;
            }
        }
        //最后从我们的一级缓存中mCachedViews查找。
        final int cacheSize = mCachedViews.size();
        for (int i = 0; i < cacheSize; i++) {
            final ViewHolder holder = mCachedViews.get(i);
            return holder;
        }
    }

As you can see, the order in which getScrapOrHiddenOrCachedHolderForPosition finds ViewHolder is as follows:

  • First, search from mAttachedScrap to accurately match the effective ViewHolder;
  • Next, find the hidden ViewHolder in mHiddenViews in mChildHelper;
  • Finally, look up from mCachedViews in the first-level cache.

2.3 getScrapOrCachedViewForId

If the view is not found in getScrapOrHiddenOrCachedHolderForPosition, Ze will find it in mAttachedScrap and mCachedViews of scrap by id, the code is as follows.

  ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {
        //在Scrap的mAttachedScrap中查找
        final int count = mAttachedScrap.size();
        for (int i = count - 1; i >= 0; i--) {
            final ViewHolder holder = mAttachedScrap.get(i);
            return holder;
        }

        //在一级缓存mCachedViews中查找
        final int cacheSize = mCachedViews.size();
        for (int i = cacheSize - 1; i >= 0; i--) {
            final ViewHolder holder = mCachedViews.get(i);
            return holder;
        }
    }        

The search order of the getScrapOrCachedViewForId() method is as follows:

  • First, search from mAttachedScrap to accurately match the effective ViewHolder;
  • Next, look up mCachedViews from the first-level cache;

2.4 mViewCacheExtension

mViewCacheExtension is a layer of caching strategy defined by the developer, and Recycler does not cache any views here.

if (holder == null && mViewCacheExtension != null) {
        final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
        if (view != null) {
            holder = getChildViewHolder(view);
        }
    }

There is no custom caching strategy here, then the corresponding view cannot be found.

2.5 RecycledViewPool

In the four-level cache of ViewHolder, we have mentioned RecycledViewPool, which caches the List of ViewHolder into SparseArray through itemType, and obtains ScrapData from SparseArray according to itemType in getRecycledViewPool().getRecycledView(type), and then obtains it from it. ArrayList<ViewHolder> to obtain ViewHolder.

    @Nullable
    public ViewHolder getRecycledView(int viewType) {
        final ScrapData scrapData = mScrap.get(viewType);//根据viewType获取对应的ScrapData 
        if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
            final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
            for (int i = scrapHeap.size() - 1; i >= 0; i--) {
                if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
                    return scrapHeap.remove(i);
                }
            }
        }
        return null;
    }

2.6 Create a new ViewHolder

If the ViewHolder has not been obtained, create a new ViewHolder through mAdapter.createViewHolder() and return.

  // 如果还没有找到复用的ViewHolder,则新建一个
  holder = mAdapter.createViewHolder(RecyclerView.this, type);

The following is a complete flow chart for finding ViewHolder:
在这里插入图片描述

3. Recycling process

There are many entrances for RecyclerView recycling, but no matter what the operation is, the recycling or reuse of RecyclerView will inevitably involve add View and remove View operations, so we start with the onLayout process to analyze the recycling and reuse mechanism.

First, in LinearLayoutManager, we come to the method onLayoutChildren() of the itemView layout entry, as shown below.

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
            if (state.getItemCount() == 0) {
                removeAndRecycleAllViews(recycler);//移除所有子View
                return;
            }
        }
        ensureLayoutState();
        mLayoutState.mRecycle = false;//禁止回收
        //颠倒绘制布局
        resolveShouldLayoutReverse();
        onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);

        //暂时分离已经附加的view,即将所有child detach并通过Scrap回收
        detachAndScrapAttachedViews(recycler);
    }

When laying out onLayoutChildren(), first remove all child Views and which ViewHolders are unavailable according to whether you need to removeAndRecycleAllViews() according to the actual situation; then temporarily detach the attached ItemViews through detachAndScrapAttachedViews() and cache them in the List.

The function of detachAndScrapAttachedViews() is to separate all the items on the current screen from the screen, remove them from the layout of the RecyclerView, save them in the list, and re-place the ViewHolder one by one in a new position when re-layout.

After removing the ViewHolder on the screen from the layout of the RecyclerView, store it in Scrap. Scrap includes mAttachedScrap and mChangedScrap. They are a list that is used to save the ViewHolder list taken from the RecyclerView layout. detachAndScrapAttachedViews() only works in onLayoutChildren( ), only in the layout, will the ViewHolder be detached, and then add to re-layout, but everyone needs to pay attention to that Scrap only saves the ViewHolder of the item displayed on the current screen in the RecyclerView layout, and does not participate in recycling and reuse. It is simply to take it down from the RecyclerView and re-layout it. Items that have not been saved will be placed in the mCachedViews or RecycledViewPool cache to participate in recycling and reuse.

   public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
        final int childCount = getChildCount();
        for (int i = childCount - 1; i >= 0; i--) {
            final View v = getChildAt(i);
            scrapOrRecycleView(recycler, i, v);
        }
    }

The function of the above code is to traverse all the views and separate all the itemViews that have been added to the RecyclerView.

   private void scrapOrRecycleView(Recycler recycler, int index, View view) {
        final ViewHolder viewHolder = getChildViewHolderInt(view);
        if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                && !mRecyclerView.mAdapter.hasStableIds()) {
            removeViewAt(index);//移除VIew
            recycler.recycleViewHolderInternal(viewHolder);//缓存到CacheView或者RecycledViewPool中
        } else {
            detachViewAt(index);//分离View
            recycler.scrapView(view);//scrap缓存
            mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
        }
    }

Then, we look at the detachViewAt() method to detach the view, and then cache it in scrap through scrapView().

    void scrapView(View view) {
        final ViewHolder holder = getChildViewHolderInt(view);
        if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
            holder.setScrapContainer(this, false);
            mAttachedScrap.add(holder);//保存到mAttachedScrap中
        } else {
            if (mChangedScrap == null) {
                mChangedScrap = new ArrayList<ViewHolder>();
            }
            holder.setScrapContainer(this, true);
            mChangedScrap.add(holder);//保存到mChangedScrap中
        }
    }

Then, we return to the scrapOrRecycleView() method and enter the if() branch. If the viewHolder is invalid, not removed, or not marked, it is cached in recycleViewHolderInternal(), and at the same time, removeViewAt() removes the viewHolder.

   void recycleViewHolderInternal(ViewHolder holder) {
           ·····
        if (forceRecycle || holder.isRecyclable()) {
            if (mViewCacheMax > 0
                    && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                    | ViewHolder.FLAG_REMOVED
                    | ViewHolder.FLAG_UPDATE
                    | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {

                int cachedViewSize = mCachedViews.size();
                if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {//如果超出容量限制,把第一个移除
                    recycleCachedViewAt(0);
                    cachedViewSize--;
                }
                     ·····
                mCachedViews.add(targetCacheIndex, holder);//mCachedViews回收
                cached = true;
            }
            if (!cached) {
                addViewHolderToRecycledViewPool(holder, true);//放到RecycledViewPool回收
                recycled = true;
            }
        }
    }

If the conditions are met, it will be cached in mCachedViews first. If the maximum limit of mCachedViews is exceeded, the first data cached by CacheView is added to the ultimate recycling pool RecycledViewPool through recycleCachedViewAt() and then removed, and finally add() The new ViewHolder is added to mCachedViews.

The remaining unqualified ones are cached in RecycledViewPool through addViewHolderToRecycledViewPool().

    void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
        clearNestedRecyclerViewIfNotNested(holder);
        View itemView = holder.itemView;
        ······
        holder.mOwnerRecyclerView = null;
        getRecycledViewPool().putRecycledView(holder);//将holder添加到RecycledViewPool中
    }

Finally, when the fill() method is called to fill the layout, it will recycle the view that has moved off the screen to mCachedViews or RecycledViewPool.

 int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
              recycleByLayoutState(recycler, layoutState);//回收移出屏幕的view
        }
    }

The recycleByLayoutState() method is used to recycle the views that have moved out of the screen. The complete process is shown in the figure below.
在这里插入图片描述


xiangzhihong
5.9k 声望15.3k 粉丝

著有《React Native移动开发实战》1,2,3、《Kotlin入门与实战》《Weex跨平台开发实战》、《Flutter跨平台开发与实战》1,2和《Android应用开发实战》