preface

Based on the 2021-01-22 revised edition, source code implementation ‘androidx. Recyclerview: recyclerview: 1.1.0’

In the last article, we talked about ItemDecoration. In this article, we talked about recycle logic of RecyclerView.

  • RecyclerView ItemDecoration (a)
  • RecyclerView Cache (2)
  • 【Android advanced 】RecyclerView drawing process (three)
  • RecyclerView group list add top effect (4)

The problem

How many Viewholders will be created when RecyclerView slides if there are 100 items and the first screen displays a maximum of 2 and a half items (a screen displays a maximum of 4 items at the same time)?

Before you answer, let’s write a demo

First, the layout of the item


      
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv_repeat"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:gravity="center" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="2dp"
        android:background="@color/colorAccent" />

</LinearLayout>
Copy the code

Then there is the RepeatAdapter, where the native Adapter is used

public class RepeatAdapter extends RecyclerView.Adapter<RepeatAdapter.RepeatViewHolder> {

    private List<String> list;
    private Context context;

    public RepeatAdapter(List<String> list, Context context) {
        this.list = list;
        this.context = context;
    }

    @NonNull
    @Override
    public RepeatViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
        View view = LayoutInflater.from(context).inflate(R.layout.item_repeat, viewGroup, false);

        Log.e("cheng"."onCreateViewHolder viewType=" + i);
        return new RepeatViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull RepeatViewHolder viewHolder, int i) {
        viewHolder.tv_repeat.setText(list.get(i));
        Log.e("cheng"."onBindViewHolder position=" + i);
    }

    @Override
    public int getItemCount(a) {
        return list.size();
    }


    class RepeatViewHolder extends RecyclerView.ViewHolder {

        public TextView tv_repeat;

        public RepeatViewHolder(@NonNull View itemView) {
            super(itemView);
            this.tv_repeat = (TextView) itemView.findViewById(R.id.tv_repeat); }}}Copy the code

Used in an Activity

        List<String> list = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            list.add("The first" + i + "Item");
        }
        RepeatAdapter repeatAdapter = new RepeatAdapter(list, this);
        rvRepeat.setLayoutManager(new LinearLayoutManager(this));
        rvRepeat.setAdapter(repeatAdapter);
Copy the code

When we swipe, log looks like this:As you can see, it was executed seven timesonCreateViewHolderThat is, out of a total of 100 items, only 7 were createdviewholder(The length is not 100, if you are interested, you can try it yourself.)

According to?

By reading the source code, we found that is viewholder RecyclerView cache unit, and ultimately the method called viewholder is Recycler# tryGetViewHolderForPositionByDeadline source code is as follows:

        @Nullable
        RecyclerView.ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {... Omit code... holder =this.getChangedScrapViewForPosition(position); . Omit code...if (holder == null) {
                holder = this.getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); }... Omit code...if (holder == null) {
                View view = this.mViewCacheExtension.getViewForPositionAndType(this, position, type);
                if(view ! =null) {
                    holder = RecyclerView.this.getChildViewHolder(view); }}... Omit code...if (holder == null) {
                holder = this.getRecycledViewPool().getRecycledView(type); }... Omit code...if (holder == null) {
                holder = RecyclerView.this.mAdapter.createViewHolder(RecyclerView.this, type); }... Omit code... }Copy the code

From top to bottom, mChangedScrap, mAttachedScrap, mCachedViews, mViewCacheExtension, mRecyclerPool, createViewHolder

        ArrayList<RecyclerView.ViewHolder> mChangedScrap = null;
        final ArrayList<RecyclerView.ViewHolder> mAttachedScrap = new ArrayList();
        final ArrayList<RecyclerView.ViewHolder> mCachedViews = new ArrayList();
        private RecyclerView.ViewCacheExtension mViewCacheExtension;
        RecyclerView.RecycledViewPool mRecyclerPool;
Copy the code
  • mChangedScrap

The complete source code is as follows:

                if (RecyclerView.this.mState.isPreLayout()) {
                    holder = this.getChangedScrapViewForPosition(position); fromScrapOrHiddenOrCache = holder ! =null;
                }
Copy the code

Since the isPreLayout method depends on mInPreLayout, mInPreLayout defaults to false, and when is mInPreLayout set to True? The answer is in onMeasure

               if (mAdapterUpdateDuringMeasure) {
                startInterceptRequestLayout();
                onEnterLayoutOrScroll();
                processAdapterUpdatesAndSetAnimationFlags();
                onExitLayoutOrScroll();

                if (mState.mRunPredictiveAnimations) {
                    mState.mInPreLayout = true;
                } else {
                    // consume remaining updates to provide a consistent state with the layout pass.
                    mAdapterHelper.consumeUpdatesInOnePass();
                    mState.mInPreLayout = false;
                }
                mAdapterUpdateDuringMeasure = false;
                stopInterceptRequestLayout(false);
            }
Copy the code

MAdapterUpdateDuringMeasure is increased in Adapter deletion method will only be set to true, the details can see Android advanced RecyclerView 】 the drawing process of (3)

When did he add it? Let’s move on

  • mAttachedScrap

When was the Viewholder added to mAttachedScrap?

Add Recycler#scrapView()

               /** * Mark an attached view as scrap. * * <p>"Scrap" views are still attached to their parent RecyclerView but are eligible * for rebinding and reuse. Requests for a view for a given position may return a * reused or rebound scrap view  instance.</p> * *@param view View to scrap
         */
        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);
                mAttachedScrap.add(holder);
            } else {
                if (mChangedScrap == null) {
                    mChangedScrap = new ArrayList<ViewHolder>();
                }
                holder.setScrapContainer(this.true); mChangedScrap.add(holder); }}Copy the code

When do I call scrapView()? Continue to global search, found that is ultimately Recycler# detachAndScrapAttachedViews () method, this method is when is called? The answer is LayoutManager#onLayoutChildren().

We know onLayoutChildren is responsible for the layout of the item behind this part (say), so the mAttachedScrap should deposit is the current viewhoder is displayed on the screen, we’ll look at the source code of detachAndScrapAttachedViews

        public void detachAndScrapAttachedViews(@NonNull RecyclerView.Recycler recycler) {
            int childCount = this.getChildCount();

            for(int i = childCount - 1; i >= 0; --i) {
                View v = this.getChildAt(i);
                this.scrapOrRecycleView(recycler, i, v); }}Copy the code

ChildCount is the number of items displayed on the screen. It performs the above series of judgments only when it is greater than 0. When was it added? It’s in RecyclerView#addViewInt, and its call chain is

RecyclerView#addView

->LinearLayoutManager#layoutChunk

->LinearLayoutManager#fill

->LinearLayoutManager#onLayoutChildren

Eventually returned to onLayoutChildren method, but embarrassed is detachAndScrapAttachedViews is before the fill method calls!!!!!!

That is to say, under normal circumstances the above two are not involved in RecycleView recycling and reuse.

  • mCachedViews

The complete code is as follows:

            cacheSize = this.mCachedViews.size();

            for(int i = 0; i < cacheSize; ++i) {
                RecyclerView.ViewHolder holder = (RecyclerView.ViewHolder)this.mCachedViews.get(i);
                if(! holder.isInvalid() && holder.getLayoutPosition() == position) {if(! dryRun) {this.mCachedViews.remove(i);
                    }

                    returnholder; }}Copy the code

Let’s first find out when the Viewholder was added to mCachedViews. In Recycler# recycleViewHolderInternal () method

        void recycleViewHolderInternal(RecyclerView.ViewHolder holder) {
            if(! holder.isScrap() && holder.itemView.getParent() ==null) {
                if (holder.isTmpDetached()) {
                    throw new IllegalArgumentException("Tmp detached view should be removed from RecyclerView before it can be recycled: " + holder + RecyclerView.this.exceptionLabel());
                } else if (holder.shouldIgnore()) {
                    throw new IllegalArgumentException("Trying to recycle an ignored view holder. You should first call stopIgnoringView(view) before calling recycle." + RecyclerView.this.exceptionLabel());
                } else {
                    boolean transientStatePreventsRecycling = holder.doesTransientStatePreventRecycling();
                    boolean forceRecycle = RecyclerView.this.mAdapter ! =null && transientStatePreventsRecycling && RecyclerView.this.mAdapter.onFailedToRecycleView(holder);
                    boolean cached = false;
                    boolean recycled = false;
                    if (forceRecycle || holder.isRecyclable()) {
                        if (this.mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(526)) {
                            int cachedViewSize = this.mCachedViews.size();
                            if (cachedViewSize >= this.mViewCacheMax && cachedViewSize > 0) {
                                this.recycleCachedViewAt(0);
                                --cachedViewSize;
                            }

                            int targetCacheIndex = cachedViewSize;
                            if (RecyclerView.ALLOW_THREAD_GAP_WORK && cachedViewSize > 0 && !RecyclerView.this.mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                                int cacheIndex;
                                for(cacheIndex = cachedViewSize - 1; cacheIndex >= 0; --cacheIndex) {
                                    int cachedPos = ((RecyclerView.ViewHolder)this.mCachedViews.get(cacheIndex)).mPosition;
                                    if(! RecyclerView.this.mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                                        break;
                                    }
                                }

                                targetCacheIndex = cacheIndex + 1;
                            }

                            this.mCachedViews.add(targetCacheIndex, holder);
                            cached = true;
                        }

                        if(! cached) {this.addViewHolderToRecycledViewPool(holder, true);
                            recycled = true;
                        }
                    }

                    RecyclerView.this.mViewInfoStore.removeViewHolder(holder);
                    if(! cached && ! recycled && transientStatePreventsRecycling) { holder.mOwnerRecyclerView =null; }}}else {
                throw new IllegalArgumentException("Scrapped or attached views may not be recycled. isScrap:" + holder.isScrap() + " isAttached:"+ (holder.itemView.getParent() ! =null) + RecyclerView.this.exceptionLabel()); }}Copy the code

At the top is RecyclerView#removeAndRecycleViewAt

        public void removeAndRecycleViewAt(int index, @NonNull RecyclerView.Recycler recycler) {
            View view = this.getChildAt(index);
            this.removeViewAt(index);
            recycler.recycleView(view);
        }
Copy the code

Where is this method called? So the answer is LayoutManager, so let’s do a little demo that looks pretty intuitive and define MyLayoutManager, rewrite removeAndRecycleViewAt, and then add log

    class MyLayoutManager extends LinearLayoutManager {
        public MyLayoutManager(Context context) {
            super(context);
        }

        @Override
        public void removeAndRecycleViewAt(int index, @NonNull RecyclerView.Recycler recycler) {
            super.removeAndRecycleViewAt(index, recycler);
            Log.e("cheng"."removeAndRecycleViewAt index="+ index); }}Copy the code

Set it toRecyclerView, and then swipe to see the log output

As you can see, this is called every time an item slides off the screenremoveAndRecycleViewAt()Method, it is important to note that thisindexThat’s what this isiteminchlidSubscript, which is in the current screen, not inRecyclerView.

Is that the case? The LinearLayoutManager defaults to vertical sliding, and the method that controls its sliding distance is scrollVerticallyBy(), which calls the scrollBy() method

    int scrollBy(int dy, Recycler recycler, State state) {
        if (this.getChildCount() ! =0&& dy ! =0) {
            this.mLayoutState.mRecycle = true;
            this.ensureLayoutState();
            int layoutDirection = dy > 0 ? 1 : -1;
            int absDy = Math.abs(dy);
            this.updateLayoutState(layoutDirection, absDy, true, state);
            int consumed = this.mLayoutState.mScrollingOffset + this.fill(recycler, this.mLayoutState, state, false);
            if (consumed < 0) {
                return 0;
            } else {
                int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
                this.mOrientationHelper.offsetChildren(-scrolled);
                this.mLayoutState.mLastScrollDelta = scrolled;
                returnscrolled; }}else {
            return 0; }}Copy the code

The key code is recycleByLayoutState() in the fill() method, which determines the slide direction and recycles from the first or last one.

    private void recycleByLayoutState(Recycler recycler, LinearLayoutManager.LayoutState layoutState) {
        if(layoutState.mRecycle && ! layoutState.mInfinite) {if (layoutState.mLayoutDirection == -1) {
                this.recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);
            } else {
                this.recycleViewsFromStart(recycler, layoutState.mScrollingOffset); }}}Copy the code

Pull some far, let’s review the recycleViewHolderInternal () method, when cachedViewSize > = this. MViewCacheMax, would remove the first, also is the first to join the viewholder, What’s mViewCacheMax?

        public Recycler(a) {
            this.mUnmodifiableAttachedScrap = Collections.unmodifiableList(this.mAttachedScrap);
            this.mRequestedCacheMax = 2;
            this.mViewCacheMax = 2;
        }
Copy the code

MViewCacheMax is 2, so the initial size of mCachedViews is 2, and beyond that, the viewholer will be removed, where will it go? With that in mind we move on

  • mViewCacheExtension

This class requires the user to pass in the setViewCacheExtension() method, and RecyclerView itself does not implement it, nor is it used for normal use.

  • mRecyclerPool

MCachedViews has an initial size of 2. After this size, the first viewholder to be added is removed. Where is the viewholder removed? Let’s look at the recycleCachedViewAt() method source

        void recycleCachedViewAt(int cachedViewIndex) {
            RecyclerView.ViewHolder viewHolder = (RecyclerView.ViewHolder)this.mCachedViews.get(cachedViewIndex);
            this.addViewHolderToRecycledViewPool(viewHolder, true);
            this.mCachedViews.remove(cachedViewIndex);
        }
Copy the code

AddViewHolderToRecycledViewPool () method

        void addViewHolderToRecycledViewPool(@NonNull RecyclerView.ViewHolder holder, boolean dispatchRecycled) {
            RecyclerView.clearNestedRecyclerViewIfNotNested(holder);
            if (holder.hasAnyOfTheFlags(16384)) {
                holder.setFlags(0.16384);
                ViewCompat.setAccessibilityDelegate(holder.itemView, (AccessibilityDelegateCompat)null);
            }

            if (dispatchRecycled) {
                this.dispatchViewRecycled(holder);
            }

            holder.mOwnerRecyclerView = null;
            this.getRecycledViewPool().putRecycledView(holder);
        }
Copy the code

As you can see, the Viewholder is added to mRecyclerPool

We continue to look at the RecycledViewPool source code

    public static class RecycledViewPool {
        private static final int DEFAULT_MAX_SCRAP = 5;
        SparseArray<RecyclerView.RecycledViewPool.ScrapData> mScrap = new SparseArray();
        private int mAttachCount = 0;

        public RecycledViewPool(a) {}... Omit code... }Copy the code
        static class ScrapData {
            final ArrayList<RecyclerView.ViewHolder> mScrapHeap = new ArrayList();
            int mMaxScrap = 5;
            long mCreateRunningAverageNs = 0L;
            long mBindRunningAverageNs = 0L;

            ScrapData() {
            }
        }
Copy the code

As you can see, there is a SparseArray inside that holds the Viewholder.

conclusion

  • A total ofmAttachedScrap,mCachedViews,mViewCacheExtension,mRecyclerPoolLevel 4 cache, wheremAttachedScrapSave only the layout when displayed on the screenviewholder, generally does not participate in recycling and reuse (Drag sort participates);
  • mCachedViewsMainly save just removed screenviewholder, the initial size is 2;
  • mViewCacheExtensionThe reserved cache pool needs to be implemented by itself.
  • mRecyclerPoolIs the last level cache, whenmCachedViewsWhen it’s full,viewholderWill be storedmRecyclerPoolContinue reuse.

Where, mAttachedScrap and mCachedViews are exact matches, that is, viewholder corresponding to position will be reused. MRecyclerPool is a fuzzy match that only matches viewType. Therefore, onBindViewHolder needs to be called to set new data for mRecyclerPool.

Answer the previous question

When the sixth item is slid out, the first and second items are stored in mCachedViews, and the third, fourth, fifth and sixth items are displayed on the screen. When the seventh item is slid out, there is no viewholder that can be reused. So calling onCreateViewHolder creates a new Viewholder and puts the first viewholder into mRecyclerPool for reuse.

Complete source codePicRvDemo

Your recognition is the motivation for me to keep updating my blog. If it is useful, please give me a thumbs-up. Thank you