RecyclerView的缓存机制——回收机制

如今在开发中RecyclerView已经基本替代了ListView来实现列表展示。而RecyclerView被说的最多的就是它的缓存机制,这也理所当然成了面试官的最爱啦。所以弄明白它的缓存机制就成了我们这些开发人员的必修课啦!

而要弄明白它的缓存机制,我们就要一点点的去抠它的源码,有人问这一步能不能省了。我想说我也想省,但是现实告诉我们,只看几篇博客(包括我这篇)而不去抠几遍源码时无法理解透彻RecyclerView的缓存机制的,到头来我们可能只知道有四级缓存:

 

缓存结构

说明

一级缓存

mChangedScrap 与 mAttachedScrap

onLayout过程中屏幕内或正在移出屏幕的Item

二级缓存

mCachedViews

移出屏幕的Item,默认大小2

三级缓存

mViewCacheExtension

ViewCacheExtension  Google留给开发者自定义的缓存

四级缓存

mRecyclerPool

当mCachedViews缓存满后会根据FIFO的规则将二级缓存中移出的ViewHolder缓存到RecycledViewPool中,默认大小5

但是却不明白ViewHolder在这四级缓存中是如何回收复用的。所以今天我们要做的就是通过源码弄明白这些不容易理解的知识点。

我们都知道只有在滑动的时候才会出现ViewHolder的回收复用现象。而滑动跟着的必然是Layout。而实际上RecyclerView的回收机制就是分onLayout和onTouchEvent这两条线的。但是由于篇幅和可读性关系,这里我只会对关键部分的源码进行分析。具体的流程可跟着流程图抠一遍源码。

一、第一条线:onTouchEvent

RecyclerView的缓存机制——回收机制_第1张图片

 1、RecyclerView.onTouchEvent

 public boolean onTouchEvent(MotionEvent e) {
       ....
		1、一:首先将事件下发到子Item,这里是针对item有设置setOnTouchListener的事件下发情况。
        if (dispatchOnItemTouch(e)) {
            cancelTouch();
            return true;
        }
		....
		2、二:判断LayoutManager是否允许RecyclerView布局上下或者左右滑动。
		如果我们有需要禁用RecyclerView某个方向滑动的需求就可以通过重写LayoutManager这两个方法的方式来实现
        final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
        final boolean canScrollVertically = mLayout.canScrollVertically();
		.......
        switch (action) {
            .......

		3、三:这里才是我们需要关注的重点,RecyclerView的回收复用就出现在它滑动的时候
            case MotionEvent.ACTION_MOVE: {
                final int index = e.findPointerIndex(mScrollPointerId);
                if (index < 0) {
                    Log.e(TAG, "Error processing scroll; pointer index for id "
                            + mScrollPointerId + " not found. Did any MotionEvents get skipped?");
                    return false;
                }
                ........

                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    mLastTouchX = x - mScrollOffset[0];
                    mLastTouchY = y - mScrollOffset[1];
			//但是看了半天发现具体的逻辑被分装在了scrollByInternal方法中
                    if (scrollByInternal(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            vtev)) {
			//这里我们前面在讲滑动冲突的内部解决方案的时候有说过,这里就不再说了。
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    if (mGapWorker != null && (dx != 0 || dy != 0)) {
                        mGapWorker.postFromTraversal(this, dx, dy);
                    }
                }
            } break;

          ......
        }

        return true;
    }

 2、RecyclerView.scrollStep

void scrollStep(int dx, int dy, @Nullable int[] consumed) {
        .......
	/**我们都知道,RecyclerView是可以通过LayoutManager来设置横向还是竖向滑动的,
	*只是方向处理上有差距,和我们这里要分析的并没有多大关联,所以我们只需要分析一个方向就好              
       *了。
       */
        if (dx != 0) {
            consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
        }
        if (dy != 0) {
            consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
        }

     ........
    }

RecyclerView的缓存机制——回收机制_第2张图片

3、LinearLayoutManager. scrollBy 

int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {

....

//很多人可能会和我一样,看到这里就懵了,感觉路断了,因为跟到现在还是没有看到回收或者复用ViewHolder的逻辑,

难道是updateLayoutState方法?但是一看还是不对。

于是只能硬着头皮继续往下看了,找了半天找到了fill方法。这才是真正的入口啊!

所以看源码真的好难呀,一不小心就迷路了,到最后都不知道自己在干嘛,要干嘛,有同感的小伙伴请点赞。

updateLayoutState(layoutDirection, absDy, true, state);

final int consumed = mLayoutState.mScrollingOffset

+ fill(recycler, mLayoutState, state, false);

....

return scrolled;

}

4、LinearLayoutManager.fill

 int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        // max offset we should set is mFastScroll + available
        final int start = layoutState.mAvailable;
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            // TODO ugly bug fix. should not happen  
            //这是Google工程师的注释,看人家多谦虚,直接吐槽自己写的bug太ugly了。
            //所以我们要做的就是连人家ugly的代码都要研究透彻
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
			//重点一:回收机制
            recycleByLayoutState(recycler, layoutState);
        }
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
            if (RecyclerView.VERBOSE_TRACING) {
                TraceCompat.beginSection("LLM LayoutChunk");
            }
			//重点二:复用机制
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            .....

            if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if (layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
                recycleByLayoutState(recycler, layoutState);
            }
           ......
        }
        .......
        return start - layoutState.mAvailable;
    }

 讲到这里就出现了两个关键点了,一个回收,一个复用,这里我们就先分析回收机制

5、LinearLayoutManager.recycleByLayoutState

  private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
        if (!layoutState.mRecycle || layoutState.mInfinite) {
            return;
        }
		//这里就出现了两个方向了,这里我们只分析一个方向,因为原理都是一样的。
        if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
		//向下滑动,底部回收
            recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);
        } else {
		//向上滑动,顶部回收
            recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
        }
    }

 6、LinearLayoutManager.recycleChildren

    //不管是哪个方向上的回收,最终调用的都是recycleChildren
   private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
        if (startIndex == endIndex) {
            return;
        }
	
	//这里循环执行判断哪些item需要回收
        if (endIndex > startIndex) {
            for (int i = endIndex - 1; i >= startIndex; i--) {
                removeAndRecycleViewAt(i, recycler);
            }
        } else {
            for (int i = startIndex; i > endIndex; i--) {
                removeAndRecycleViewAt(i, recycler);
            }
        }
    }

7、Recycler .recycleView

public void recycleView(@NonNull View view) {
            // This public recycle method tries to make view recycle-able since layout manager
            // intended to recycle this view (e.g. even if it is in scrap or change cache)
            ViewHolder holder = getChildViewHolderInt(view);//找到与View绑定的ViewHolder对象
            if (holder.isTmpDetached()) {//如果已经是detach状态,则先从视图上移出。
                removeDetachedView(view, false);
            }
            if (holder.isScrap()) {//判断是否已经在mChangedScrap或者mAttachedScrap中
                holder.unScrap();//从mAttachedScrap或者mChangedScrap中移除
            } else if (holder.wasReturnedFromScrap()) {
                holder.clearReturnedFromScrapFlag();
            }
            recycleViewHolderInternal(holder);//执行真正的回收逻辑,将ViewHolder缓存到mCachedViews和Pool中
        }

 这里要说一点就是,RecyclerView的回收复用机制的逻辑都是由Recycler 这个内部类来完成的,这也体现了Google工程师在开发时遵循的单一原则等设计原则,让逻辑更加清晰。我们先去看一眼Recycler 这个类。

public final class Recycler {
        1、一级缓存,回收布局过程中,屏幕可视范围内的VH
        final ArrayList mAttachedScrap = new ArrayList<>();
        ArrayList mChangedScrap = null;
        2、二级缓存,回收已移出屏幕的VH
        final ArrayList mCachedViews = new ArrayList();
        private final List
                mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
        private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
        int mViewCacheMax = DEFAULT_CACHE_SIZE;
        4、四级缓存,回收池
        RecycledViewPool mRecyclerPool;
        3、三级缓存,虽然通常我们不会用到这一级缓存
        private ViewCacheExtension mViewCacheExtension;
        static final int DEFAULT_CACHE_SIZE = 2;//mCachedViews 的默认大小
}

再看一眼RecycledViewPool的源码

public static class RecycledViewPool {
        private static final int DEFAULT_MAX_SCRAP = 5;//每种viewType对应的默认大小
        static class ScrapData {
            final ArrayList mScrapHeap = new ArrayList<>();
            int mMaxScrap = DEFAULT_MAX_SCRAP;
            long mCreateRunningAverageNs = 0;
            long mBindRunningAverageNs = 0;
        }
    //可以看到RecycledViewPool内部是个SparseArray的结构,
    //即Key、Value的结构。而这里的Key就对应着viewType,value对应着ViewHolder
        SparseArray mScrap = new SparseArray<>();
}

再来看一下RecycledViewPool的结构图:

RecyclerView的缓存机制——回收机制_第3张图片

 8、Recycler .recycleViewHolderInternal

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) {
					//mViewCacheMax为mCachedViews缓存区的上限大小,默认为2。
					当缓存区满,则移出最老的缓存视图,被移出的视图将被缓存至RecycledViewPool中
                        recycleCachedViewAt(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;
                    }
		//缓存最新的ViewHolder
                    mCachedViews.add(targetCacheIndex, holder);
                    cached = true;
                }
                if (!cached) {//如果未被缓存到mCachedViews中,则缓存至RecycledViewPool中
                    addViewHolderToRecycledViewPool(holder, true);
                    recycled = true;
                }
            }
            // even if the holder is not removed, we still call this method so that it is removed
            // from view holder lists.
            mViewInfoStore.removeViewHolder(holder);
            if (!cached && !recycled && transientStatePreventsRecycling) {//如果ViewHolder未被缓存,则将被永久丢弃。
                holder.mOwnerRecyclerView = null;
            }
        }

 

二、第二条线:onLayout

RecyclerView的缓存机制——回收机制_第4张图片

1、LinearLayoutManager.onLayoutChildren

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        ......
        detachAndScrapAttachedViews(recycler);//填充之前先回收所有Item
        mLayoutState.mInfinite = resolveIsInfinite();
        mLayoutState.mIsPreLayout = state.isPreLayout();
	//这里还是分两个方向
        if (mAnchorInfo.mLayoutFromEnd) {//向上滑动布局
            // fill towards start
            updateLayoutStateToFillStart(mAnchorInfo);
            mLayoutState.mExtra = extraForStart;
            fill(recycler, mLayoutState, state, false);//多次执行回收复用逻辑
            startOffset = mLayoutState.mOffset;
            final int firstElement = mLayoutState.mCurrentPosition;
            if (mLayoutState.mAvailable > 0) {
                extraForEnd += mLayoutState.mAvailable;
            }
            // fill towards end
            updateLayoutStateToFillEnd(mAnchorInfo);
            mLayoutState.mExtra = extraForEnd;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
            fill(recycler, mLayoutState, state, false);
         
        } else {//向下滑动布局
            // fill towards end
            updateLayoutStateToFillEnd(mAnchorInfo);
            mLayoutState.mExtra = extraForEnd;
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;
            final int lastElement = mLayoutState.mCurrentPosition;
            if (mLayoutState.mAvailable > 0) {
                extraForStart += mLayoutState.mAvailable;
            }
            // fill towards start
            updateLayoutStateToFillStart(mAnchorInfo);
            mLayoutState.mExtra = extraForStart;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;

            if (mLayoutState.mAvailable > 0) {
                extraForEnd = mLayoutState.mAvailable;
                // start could not consume all it should. add more items towards end
                updateLayoutStateToFillEnd(lastElement, endOffset);
                mLayoutState.mExtra = extraForEnd;
                fill(recycler, mLayoutState, state, false);
                endOffset = mLayoutState.mOffset;
            }
        }
		....
    }

 2、RecyclerView.detachAndScrapAttachedViews

public void detachAndScrapAttachedViews(Recycler recycler) {
        final int childCount = getChildCount();
        for (int i = childCount - 1; i >= 0; i--) {
            final View v = getChildAt(i);
            scrapOrRecycleView(recycler, i, v);
        }
    }
   
    /**
     * 1.Recycle操作对应的是removeView, View被remove后调用Recycler的recycleViewHolderInternal回收其ViewHolder
     2.Scrap操作对应的是detachView,View被detach后调用Reccyler的scrapView暂存其ViewHolder
     * @param recycler
     * @param index
     * @param view
     */
    private void scrapOrRecycleView(Recycler recycler, int index, View view) {
        final ViewHolder viewHolder = getChildViewHolderInt(view);
        if (viewHolder.shouldIgnore()) {
            if (DEBUG) {
                Log.d(TAG, "ignoring view " + viewHolder);
            }
            return;
        }
        if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                && !mRecyclerView.mAdapter.hasStableIds()) {
            //remove : VH无效且没有移除,且没有StableIds
            removeViewAt(index);
            //往cacheview和pool中添加ViewHolder
            recycler.recycleViewHolderInternal(viewHolder);
        } else {
            //detach : 也就是还在屏幕可视范围内
            detachViewAt(index);
            //存到scrap中:mChangedScrap 与 mAttachedScrap
            recycler.scrapView(view);
            mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
        }
    }

这里就可以看到分为Remove和Detach两种,所以我们需要区分两个概念,DetachRemove

Detach: 首先我们应该了解每个ViewGroup都会经历两次onLayout过程,而RecyclerView在这个过程中会将屏幕范围内的childView临时从ChildView数组中remove, 但并不是真的移除,childView此时和View树还是藕断丝连的。View被detach一般是临时的,在后面会被重新attach.而这些被临时remove的View将被缓存到mAttachedScrap 与 mChangedScrap中。缓存时机是在onLayout的过程中,并且用完即清空。

remove: 真正的移除(真正移出屏幕可视范围内),不光被从ChildView数组中除名,其他和View树各项联系也会被彻底斩断, 比如焦点被清除,从TouchTarget中被移除等。此时会执行recycleViewHolderInternal(viewHolder)方法,而这个方法最终会将ViewHolder加入CacheView和Pool中。注意这里缓存的都是ViewHolder。

3、Recycler.scrapView

Detach :将屏幕范围内的VH回收到mAttachedScrap 与 mChangedScrap中

void scrapView(View view) {
            final ViewHolder holder = getChildViewHolderInt(view);
            if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                    || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
					//非移除和无效VH、非Update VH:即在屏幕范围内的VH
                holder.setScrapContainer(this, false);
                mAttachedScrap.add(holder);
            } else {
			//移除、无效VH:即真正移出屏幕范围内的VH
                if (mChangedScrap == null) {
                    mChangedScrap = new ArrayList();
                }
                holder.setScrapContainer(this, true);
                mChangedScrap.add(holder);
            }
        }

4、Recycler.recycleViewHolderInternal

Remove:回收移除屏幕的VH,逻辑与上面一致,就不再重写一遍了。

 

你可能感兴趣的:(Android开发——UI基础)