深入了解RecyclerView

RecyclerView缓存机制

说到Android的布局,ListView肯定是最常用的控件之一了,而提到ListView就不得不说起RecyclerView了,强大的RecyclerView不仅可以实现和ListView同样的效果,还优化了ListView中存在的各种不足之处。Android官方更加推荐使用RecyclerView。官方给RecyclerView的解释:

A flexible view for providing a limited window into a large data set.

翻译过来的意思就是一个为大数据集提供有限窗口的灵活视图。如果我们想用好这个RecyclerView,那么对于他的缓存机制我们就有了解的必要了,下面我们就来看下RecyclerView 的缓存机制

RecyclerView的缓存类

RecyclerView 有几个比较重要的缓存类:Recycler,RecycledViewPool,ViewCacheExtension

Recycler

我们先来看下源码( 部分)

	public final class Recycler {

        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
        ArrayList<ViewHolder> mChangedScrap = null;
      
        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

        int mViewCacheMax = DEFAULT_CACHE_SIZE;

        RecycledViewPool mRecyclerPool;

        private ViewCacheExtension mViewCacheExtension;

        static final int DEFAULT_CACHE_SIZE = 2;
	 	...省略...
        }

这个Recycler 类里就基本包含了所有的缓存类,首先说下他的成员变量

  1. mAttachedScrap :这个变量用一个ArrayList存放了ViewHolder,这里的ViewHolder的数据是不做修改的,当使用到这里的缓存的时候是不用走adapter的绑定方法的。
  2. mChangedScrap :这个变量和上面的mAttachedScrap 一样,不过有区别的是这里存放的是已经变了的ViewHolder,使用到了这个缓存的话是需要重新走adapter的绑定方法的。
  3. mCachedViews :这个存放的是已经与RecyclerView分离了的ViewHolder,但是他依然保存了ViewHolder的信息,比如position,数据等。这里的默认的大小为2,就是mViewCacheMax这个变量,不过我们可以人为的改变他的大小,用到了RecyclerView.setItemViewCacheSize()方法。
  4. mRecyclerPool:这个就是一个缓存类,从名字中我们也可以看到,是一个缓存池。进入这个类我们可以看到:
	public static class RecycledViewPool {
       private static final int DEFAULT_MAX_SCRAP = 5;
       ...省略...
       static class ScrapData {
           final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
           int mMaxScrap = DEFAULT_MAX_SCRAP;
       }
       SparseArray<ScrapData> mScrap = new SparseArray<>();

       private int mAttachCount = 0;
        ...省略...
        }
  • 首先看到的就是这个常量DEFAULT_MAX_SCRAP ,这个指的就是这个缓存池默认的缓存数,这个数量也是可以修改的,而且这个数量指的并不是这个池子可以缓存这么多,而指的是每个itemType可以缓存的ViewHolder数量。
  • 接下来就是静态内部类ScrapData ,这里面也创建了一个存储ViewHoler的ArrayList,和一个mMaxScrap ,表示了这个默认的缓存数为5。
  • 最后就是SparseArray ,它存储了我们上面的ScrapData ,这样的话就可以通过区分itemType来缓存ViewHolder了。
  1. mViewCacheExtension:是一个抽象类,开发者可以自己去定义这个缓存类,我们可以根据实际场景来制定。

RecyclerView 的加载过程

上面介绍了RecyclerView 的几个比较重要的缓存类,那他们之间是怎么样工作的呢?下面我就就来看一下RecyclerView 的加载过程,一个view从创建到显示出来大概要经过measure,layout,draw这么几个方法,我们来看measure这个过程,定位到RecyclerView 的onMeasure()方法:

	@Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        if (mLayout == null) {
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }
        if (mLayout.isAutoMeasureEnabled()) {
            if (mState.mLayoutStep == State.STEP_START) {
                dispatchLayoutStep1();
            }
            dispatchLayoutStep2();
        }else{
        ...
        }
    }

省略了一些无关的代码,其中比较重要的方法dispatchLayoutStep1()和dispatchLayoutStep2(),我们可以看到影响dispatchLayoutStep1()方法运行的条件是mState.mLayoutStep == State.STEP_START,我们进入源码可以看到:State.STEP_START是一个常量,值为1,mState.mLayoutStep指向了一个变量

	int mLayoutStep = STEP_START;

条件为true所以首先进入到dispatchLayoutStep1()

	private void dispatchLayoutStep1() {
        if (mState.mRunSimpleAnimations) {
        ......
        }
        if (mState.mRunPredictiveAnimations) {
        ......
        } else {
            clearOldPositions();
        }
        mState.mLayoutStep = State.STEP_LAYOUT;
    }

有两个比较重要的if语句,条件为mState.mRunSimpleAnimations和mState.mRunPredictiveAnimations,点击进去我们可以发现

	boolean mRunSimpleAnimations = false;
	boolean mRunPredictiveAnimations = false;

这两个变量默认都为false,第二个判断走了一个clearOldPositions()方法,从字面意思上来看就是清除之前的position操作,由于我们是第一次加载,这个方法暂且与我们无关,接下来就是mState.mLayoutStep = State.STEP_LAYOUT,我们可以看到这里的值改变了,方法结束,返回到onMeasure我们来往下看dispatchLayoutStep2()方法:

	private void dispatchLayoutStep2() {
        startInterceptRequestLayout();
        onEnterLayoutOrScroll();
        mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
        mAdapterHelper.consumeUpdatesInOnePass();
        mState.mItemCount = mAdapter.getItemCount();
        mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

        // Step 2: Run layout
        mState.mInPreLayout = false;
        mLayout.onLayoutChildren(mRecycler, mState);

        mState.mStructureChanged = false;
        mPendingSavedState = null;

        // onLayoutChildren may have caused client code to disable item animations; re-check
        mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
        mState.mLayoutStep = State.STEP_ANIMATIONS;
        onExitLayoutOrScroll();
        stopInterceptRequestLayout(false);
    }

在这个方法会调用到mLayout.onLayoutChildren(mRecycler, mState)方法,mLayout也就是LayoutManager,我们去LinearLayoutManager里看看onLayoutChildren到底都做了什么

	@Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
     	...
        detachAndScrapAttachedViews(recycler);
        ...
        if (mAnchorInfo.mLayoutFromEnd) {
        	...
            // fill towards start
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
            ...
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;
			...
        } else {
        	...
            // fill towards end
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;
            ...
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
            ...
        }
        ...
    }

这个方法代码比较多,我只拿出了对我们比较重要的代码,首先来看下detachAndScrapAttachedViews(recycler)方法

		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);
            }
        }
        
        private void scrapOrRecycleView(Recycler recycler, int index, View view) {
            final ViewHolder viewHolder = getChildViewHolderInt(view);
            ...
            if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                    && !mRecyclerView.mAdapter.hasStableIds()) {
                removeViewAt(index);
                recycler.recycleViewHolderInternal(viewHolder);
            } else {
                detachViewAt(index);
                recycler.scrapView(view);
                mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
            }
        }

detachAndScrapAttachedViews方法里调用了scrapOrRecycleView(recycler, i, v)方法,在这个方法里正常第一次布局的时候是会走else分支,在else分支里我们看到了recycler这个成员变量,也就是Recycler ,熟悉的东西来了,我们走进recycler.scrapView(view);

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

进到这个方法里,我们看到了熟悉的mAttachedScrap和mChangedScrap,到这里我们基本可以知道,在首次布局onLayoutChildren()调用了detachAndScrapAttachedViews(recycler)方法中把我们的view添加到了mAttachedScrap缓存中。接下来我们返回到onLayoutChildren方法,下面有个if语句,不过最终还是会走fill方法,fill方法里又调用了layoutChunk()方法,我们来看一看这个layoutChunk()里做了什么

	void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);
        ...
        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addView(view);
            } else {
                addView(view, 0);
            }
        } else {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addDisappearingView(view);
            } else {
                addDisappearingView(view, 0);
            }
        }
        ...
    }

首先通过next方法把view拿到,然后经过一系列的判断将view添加到布局里去,我们着重看下next方法,看一看是怎么获取到view的

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

如果mScrapList 不为空就走nextViewFromScrapList()方法,我们暂时忽略这个方法,然后走进recycler.getViewForPosition(mCurrentPosition)这个方法,

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

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

经过层层调用,最终走到了tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS)方法

        @Nullable
        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
           
            ViewHolder holder = null;
            // 0) If there is a changed scrap, try to find from there
            if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }
            // 1) Find by position from scrap/hidden list/cache
            if (holder == null) {
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
            }
            if (holder == null) {
                // 2) Find from scrap/cache via stable ids, if exists
                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 (holder == null) { // fallback to pool
                    holder = getRecycledViewPool().getRecycledView(type);
                }
                if (holder == null) {
                    holder = mAdapter.createViewHolder(RecyclerView.this, type);
                }
            }
            return holder;
        }

这里面做了很多的事情,view 的获取和绑定都是在这里实现的,我们从顺序往下看

  1. 首先经过第一个if语句,当mState.isPreLayout()为true时就会调用getChangedScrapViewForPosition(position)来获取view;我们在调用adapter的notifyItemChanged等方法时isPreLayout()就为true,意思就是列表的item数据改变了,就会把这个viewHoler放入到mChangedScrap里
  2. 接下来如果没有找到holder 就会走getScrapOrHiddenOrCachedHolderForPosition()方法
        ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
            final int scrapCount = mAttachedScrap.size();
			...
            // Try first for an exact, non-invalid match from scrap.
            for (int i = 0; i < scrapCount; i++) {
                final ViewHolder holder = mAttachedScrap.get(i);
            }
            if (!dryRun) {
                View view = mChildHelper.findHiddenNonRemovedView(position);
            }
            // Search in our first-level recycled view cache.
            final int cacheSize = mCachedViews.size();
            for (int i = 0; i < cacheSize; i++) {
                final ViewHolder holder = mCachedViews.get(i);
            }
            ...
        }

这里面有三个步骤,首先从mAttachedScrap里找,然后再从mChildHelper里的mHiddenViews里找,最后再从mCachedViews里找

  1. 假如上面的方法依然没有找到holder,就从mViewCacheExtension里找,我们之前说过这个类是给开发者自定义的一个类,如果我们自己没有重写,那么这里是找不到的
  2. 接下来就是在pool中找了,这里调用了getRecycledViewPool().getRecycledView(type),这个参数type就是我们之前说的viewHoler的类型了
        public ViewHolder getRecycledView(int viewType) {
            final ScrapData scrapData = mScrap.get(viewType);
            if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
                final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
                return scrapHeap.remove(scrapHeap.size() - 1);
            }
            return null;
        }
  1. 最后如果上面的方法中都没有找到holder,那么就会调用mAdapter.createViewHolder(RecyclerView.this, type)了,是不是很熟悉?没错,就是我们在Adapter里重写的createViewHolder()方法了。

到此为止一个RecyclerView从无到有的加载过程我们就分析完了。

RecyclerView 的滑动缓存过程

我们知道RecyclerView 具有强大的缓存机制,内部也实现了可滑动的机制,那么当滑动时被滑出屏幕外的View和滑动进屏幕内的View他们都是从哪里来的呢,我们一起来看源码

    @Override
    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
            RecyclerView.State state) {
        if (mOrientation == VERTICAL) {
            return 0;
        }
        return scrollBy(dx, recycler, state);
    }
    
    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
            RecyclerView.State state) {
        if (mOrientation == HORIZONTAL) {
            return 0;
        }
        return scrollBy(dy, recycler, state);
    }

我们知道RecyclerView 是可以横向滑动的,那么横向和纵向滑动分别调用了上面的两个方法,他们也最终调用了同一个方法scrollBy(dx, recycler, state);而这个方法里也调用了fill()方法

	int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            // TODO ugly bug fix. should not happen
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            recycleByLayoutState(recycler, layoutState);
        }
       ...
    }

进入fill方法,这次我们不看layoutChunk()方法,我们来看一下recycleByLayoutState(recycler, layoutState)方法,方法中判断条件分别会走recycleViewsFromEnd()和recycleViewsFromStart()方法,但是这两个方法最终都会调用recycleChildren(recycler, childCount - 1, i)方法,而该方法最后调用了removeAndRecycleViewAt(i, recycler)方法

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

这里把index位的view给remove掉了,然后调用了recycler.recycleView(view),紧接着这个方法又调用了recycleViewHolderInternal(holder)方法。

       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)) {
                    // Retire oldest cached view
                    int cachedViewSize = mCachedViews.size();
                    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                        recycleCachedViewAt(0);
                        cachedViewSize--;
                    }
                    
                    mCachedViews.add(targetCacheIndex, holder);
                    cached = true;
                }
                if (!cached) {
                    addViewHolderToRecycledViewPool(holder, true);
                    recycled = true;
                }
           }

这里面的逻辑就是先判断mCachedViews的容量是否超过默认值,如果超过了就要走recycleCachedViewAt(0)方法,这个方法里的逻辑就是把最老的缓存放到RecyclePool里,然后把mCachedViews里的remove掉,执行了该操作之后再把新的缓存放到mCachedViews里,如果这个视图不符合条件会直接被放进RecyclerPool中,注意这里有个cached的boolean类型的变量,就是来判断是否符合条件的,RecyclePool里的操作addViewHolderToRecycledViewPool(holder, true)方法最终会调用getRecycledViewPool().putRecycledView(holder);

        public void putRecycledView(ViewHolder scrap) {
            final int viewType = scrap.getItemViewType();
            final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
            if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
                return;
            }
            scrap.resetInternal();
            scrapHeap.add(scrap);
        }

这个方法里的逻辑为:首先判断池子里的容量是否满了,如果满了的话就直接返回,不再缓存了,否则的话会调用resetInternal()方法,从reset我们也可以看出是一个重置方法,缓存之前全部清空,这里也就可以说明为什么满了之后就不再继续缓存了,到此为止,滑出屏幕外的缓存过程也就讲完了,那么滑入屏幕内的过程呢,其实是和RecyclerView加载过程是一样的。

总结

我们从RecyclerView的几个缓存类整体入手,分析成员变量,到RecyclerView的加载过程,再到他的缓存过程,这几个方面了解了RecyclerView缓存层级与缓存机制。

参考:
https://blog.csdn.net/singwhatiwanna/article/details/100166495