【Android进阶】RecyclerView之缓存(二)

4,165 阅读6分钟

前言

2021-01-22 修订版,源码基于 implementation 'androidx.recyclerview:recyclerview:1.1.0'

上一篇,说了ItemDecoration,这一篇,我们来说说RecyclerView的回收复用逻辑。

问题

假如有100个item ,首屏最多展示2个半(一屏同时最多展示4个),RecyclerView 滑动时,会创建多少个viewholder

先别急着回答,我们写个 demo 看看

首先,是item的布局

<?xml version="1.0" encoding="utf-8"?>
<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>

然后是RepeatAdapter,这里使用的是原生的Adapter

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() {
        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);
        }
    }
}

Activity中使用

        List<String> list = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            list.add("第" + i + "个item");
        }
        RepeatAdapter repeatAdapter = new RepeatAdapter(list, this);
        rvRepeat.setLayoutManager(new LinearLayoutManager(this));
        rvRepeat.setAdapter(repeatAdapter);

当我们滑动时,log如下: image.png 可以看到,总共执行了7次onCreateViewHolder,也就是说,总共100个item,只创建了7个viewholder(篇幅问题,没有截到100,有兴趣的同学可以自己试试)

WHY?

通过阅读源码,我们发现,RecyclerView的缓存单位是viewholder,而获取viewholder最终调用的方法是Recycler#tryGetViewHolderForPositionByDeadline 源码如下:

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

从上到下,依次是mChangedScrapmAttachedScrapmCachedViewsmViewCacheExtensionmRecyclerPool最后才是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;
  • mChangedScrap

完整源码如下:

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

由于isPreLayout方法取决于mInPreLayoutmInPreLayout默认为false,而mInPreLayout什么时候会设置为true呢? 答案是在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);
            }

其中,mAdapterUpdateDuringMeasure是在Adapter的增删改插方法中才会设置为true,详情可以查看【Android进阶】RecyclerView之绘制流程(三)

他是什么时候添加的呢?咱们接着往下看

  • mAttachedScrap

什么时候将viewholder添加到mAttachedScrap的?

我们在源码中全局搜索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);
            }
        }

什么时候调用scrapView()方法呢? 继续全局搜索,发现最终是Recycler#detachAndScrapAttachedViews()方法,这个方法又是什么时候会被调用的呢? 答案是LayoutManager#onLayoutChildren()

我们知道onLayoutChildren负责item的布局工作(这部分后面再说),所以,mAttachedScrap应该存放是当前屏幕上显示的viewhoder,我们来看下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);
            }

        }

其中,childCount 即为屏幕上显示的item数量,当他大于0时,才会执行上述一系列的判断,他是什么时候添加的呢? 是在RecyclerView#addViewInt方法中,其调用链为

RecyclerView#addView

->LinearLayoutManager#layoutChunk

->LinearLayoutManager#fill

->LinearLayoutManager#onLayoutChildren

最终还是回到了onLayoutChildren方法,但尴尬是detachAndScrapAttachedViews是在fill方法之前调用!!!

也就是说,正常情况下上述2者都是不参与RecycleView的回收与复用。

  • mCachedViews

完整代码如下:

            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);
                    }

                    return holder;
                }
            }

我们先来找找viewholder是在什么时候添加进mCachedViews?是在Recycler#recycleViewHolderInternal()方法

        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());
            }
        }

最上层是RecyclerView#removeAndRecycleViewAt方法

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

这个方法是在哪里调用的呢?答案是LayoutManager,我们写个demo效果看着比较直观 定义MyLayoutManager,并重写removeAndRecycleViewAt,然后添加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);
        }
    }

将其设置给RecyclerView,然后滑动,查看日志输出情况 image.png

image.png 可以看到,每次有item滑出屏幕时,都会调用removeAndRecycleViewAt()方法,需要注意的是,此index表示的是该itemchlid中的下标,也就是在当前屏幕中的下标,而不是在RecyclerView的。

事实是不是这样的呢?让我们来看看源码,以LinearLayoutManager为例,默认是垂直滑动的,此时控制其滑动距离的方法是scrollVerticallyBy(),其调用的是scrollBy()方法

    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;
                return scrolled;
            }
        } else {
            return 0;
        }
    }

关键代码是fill()方法中的recycleByLayoutState(),判断滑动方向,从第一个还是最后一个开始回收。

    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);
            }

        }
    }

扯的有些远了,让我们回顾下recycleViewHolderInternal()方法,当cachedViewSize >= this.mViewCacheMax时,会移除第1个,也就是最先加入的viewholdermViewCacheMax是多少呢?

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

mViewCacheMax 为2,也就是mCachedViews的初始化大小为2,超过这个大小时,viewholer将会被移除,放到哪里去了呢?带着这个疑问我们继续往下看

  • mViewCacheExtension

这个类需要使用者通过 setViewCacheExtension() 方法传入,RecyclerView 自身并不会实现它,一般正常的使用也用不到。

  • mRecyclerPool

我们带着之前的疑问,继续看源码,之前提到mCachedViews初始大小为2,超过这个大小,最先放入的会被移除,移除的viewholder到哪里去了呢?我们来看recycleCachedViewAt()方法的源码

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

addViewHolderToRecycledViewPool()方法

        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);
        }

可以看到,该viewholder被添加到mRecyclerPool

我们继续看看RecycledViewPool的源码

    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() {
        }
         ...省略代码...
}
        static class ScrapData {
            final ArrayList<RecyclerView.ViewHolder> mScrapHeap = new ArrayList();
            int mMaxScrap = 5;
            long mCreateRunningAverageNs = 0L;
            long mBindRunningAverageNs = 0L;

            ScrapData() {
            }
        }

可以看到,其内部有一个SparseArray用来存放viewholder

总结

  • 总共有mAttachedScrapmCachedViewsmViewCacheExtensionmRecyclerPool4级缓存,其中mAttachedScrap只保存布局时,屏幕上显示的viewholder,一般不参与回收、复用(拖动排序时会参与);
  • mCachedViews主要保存刚移除屏幕的viewholder,初始大小为2;
  • mViewCacheExtension为预留的缓存池,需要自己去实现;
  • mRecyclerPool则是最后一级缓存,当mCachedViews满了之后,viewholder会被存放在mRecyclerPool,继续复用。

其中,mAttachedScrapmCachedViews为精确匹配,即为对应positionviewholder才会被复用; mRecyclerPool为模糊匹配,只匹配viewType,所以复用时,需要调用onBindViewHolder为其设置新的数据。

回答之前的疑问

当滑出第6个item时,这时mCachedViews中存放着第1、2个item,屏幕上显示的是第3、4、5、6个item,再滑出第7个item时,不存在能复用的viewholder,所以调用onCreateViewHolder创建了一个新的viewholder,并且把第1个viewholder放入mRecyclerPool,以备复用。

完整源码 PicRvDemo

你的认可,是我坚持更新博客的动力,如果觉得有用,就请点个赞,谢谢