RecyclerView的回收与复用

94 阅读7分钟

RecyclerView的回收与复用

1.RecyclerView的优势

  • RecyclerView提供了局部刷新的接口notifyItemChanged(),通过局部刷新,就能避免调用许多无用的bindView。

  • RecyclerView提供了setItemAnimator()方法,让开发者能够方便地定义添加、删除、移动的动画

  • RecyclerView的扩展性更强大

    • 把布局的工作抽象出来,放到了LayoutManager当中,并预制了瀑布流布局
    • 提供ItemDecoration

2.Recycler

对与RecyclerView而言,回收复用的具体逻辑都放在了 Recycler 中,Recycler 和 LayoutManager 一样是 RecyclerView 的内部类,其英文解释如下:

A Recycler is responsible for managing scrapped or detached item views for reuse.

2.1 关键变量

下面简单介绍下一些关于缓存的变量

    public final class Recycler {
		    // 一级缓存
        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
        ArrayList<ViewHolder> mChangedScrap = null;
        // 二级缓存
        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

        private final List<ViewHolder>
                mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);

        private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
        int mViewCacheMax = DEFAULT_CACHE_SIZE;
        
        // 三级缓存
        private ViewCacheExtension mViewCacheExtension;
        
        // 四级缓存
        RecycledViewPool mRecyclerPool;

        static final int DEFAULT_CACHE_SIZE = 2;

       
    }

在这里先对这些定义有个大致的了解:

  1. mAttachedScrap、mChangedScrap 屏幕上的 ViewHolder 缓存;

    • mAttachedScrap、mCachedViews 就是简单的 ArrayList,存储 ViewHolder

    • mAttachedScrap 存放可见范围内的 ViewHolder , 从这里复用的 ViewHolder 如果 position 或者 id 对应的上,则不需要重新绑定数据

    • (但是在 onLayoutChildren 的时候,会将所有 View 都会缓存到这)

    • mChangedScrap存放可见范围内并且数据发生了变化的 ViewHolder,从这里复用的 ViewHolder 需要重新绑定数据。

  2. mCachedViews 刚刚移除屏幕的 ViewHolder 会加入进来,默认容量为 2,超出容量后最先放入的会移出,并且加入 mRecyclerPool 缓存池中;

    • 存放 remove 掉的 ViewHolder,从这里复用的 ViewHolder 如果 position 或者 id 对应的上,则不需要重新绑定数据
  3. mViewCacheExtension 供开发者自定义的缓存,一般不实现

  4. mRecyclerPool 缓存池;

    • 存放 remove 掉,并且重置了数据的 ViewHolder,从这里复用的 ViewHolder 需要重新绑定数据

对于缓存池而言,其内部结构如下

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;
            long mCreateRunningAverageNs = 0;
            long mBindRunningAverageNs = 0;
        }
        // 缓存池容器
        SparseArray<ScrapData> mScrap = new SparseArray<>();
        
        ...
   }     

RecycledViewPool 内部的容器为 SparseArray,存储类型是其内部类 ScrapData,而 ScrapData 内部使用 ArrayList 来存储 ViewHolder。

当 RecyclerView 多类型 item 进行缓存时,直接以 itemViewType 作为 key,ScrapData 作为 value 对 ViewHolder 进行缓存,默认情况下每种不同的 ViewHolder 可以分别缓存 5 个

2.2 Recycler的回收

RecyclerView回收复用的具体逻辑都放在了 Recycler ,首先来看下回收流程,其入口在

Recycler.recycleView(view)

        public void recycleView(@NonNull View view) {
            //  从内部可以直接获取到holder
            ViewHolder holder = getChildViewHolderInt(view);
            if (holder.isTmpDetached()) {
                // 清除动画资源等
                removeDetachedView(view, false);
            }
            if (holder.isScrap()) {
                holder.unScrap();
            } else if (holder.wasReturnedFromScrap()) {
                holder.clearReturnedFromScrapFlag();
            }
            // !! 回收的核心方法 !!
            recycleViewHolderInternal(holder);
            // 停止动画
            if (mItemAnimator != null && !holder.isRecyclable()) {
                mItemAnimator.endAnimation(holder);
            }
        }
        
        
        
        void recycleViewHolderInternal(ViewHolder holder) {
            ...
            final boolean transientStatePreventsRecycling = holder
                    .doesTransientStatePreventRecycling();
            // 这里 可以重写 adapter 的 onFailedToRecycleView 方法返回 ture 来达到强制回收 ViewHolder
            final boolean forceRecycle = mAdapter != null
                    && transientStatePreventsRecycling
                    && mAdapter.onFailedToRecycleView(holder);
            boolean cached = false;
            boolean recycled = false;
            if (DEBUG && mCachedViews.contains(holder)) {
                throw new IllegalArgumentException("cached view received recycle internal? "
                        + holder + exceptionLabel());
            }
            // 真正的回收逻辑
            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
                     // 先获取 mCachedViews 的 size
                    int cachedViewSize = mCachedViews.size();
                    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                      // 容量超出处理
                        recycleCachedViewAt(0);
                        cachedViewSize--;
                    }

                    int targetCacheIndex = cachedViewSize;
                    // 预加载处理
                    if (ALLOW_THREAD_GAP_WORK
                            && cachedViewSize > 0
                            && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                        ...
                    }
                    // 添加到 mCachedViews 缓存中
                    mCachedViews.add(targetCacheIndex, holder);
                    cached = true;
                }
                if (!cached) {
                 // 如果没有添加到 mCachedView 中,则直接加入 RecycledViewPool 中
                    addViewHolderToRecycledViewPool(holder, true);
                    recycled = true;
                }
            } else {
                ...
            }
            // 清除保存的holder 动画相关信息
            mViewInfoStore.removeViewHolder(holder);
            if (!cached && !recycled && transientStatePreventsRecycling) {
                holder.mOwnerRecyclerView = null;
            }
        }

        void recycleCachedViewAt(int cachedViewIndex) {
            ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
            // 添加holder到RecycledViewPool
            **addViewHolderToRecycledViewPool**(viewHolder, true);
            // 从mCachedViews中移除
            mCachedViews.remove(cachedViewIndex);
        }
        
        
        void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
            clearNestedRecyclerViewIfNotNested(holder);
            View itemView = holder.itemView;
            ...
            if (dispatchRecycled) {
            // 触发 adapter 的 onViewRecycled 方法
                dispatchViewRecycled(holder);
            }
            holder.mOwnerRecyclerView = null;
            // 添加到缓存池
            getRecycledViewPool().putRecycledView(holder);
        }

总结

  • 若需要回收的holder存在于一级缓存(mAttachedScrap、mChangedScrap),则从中移除,并将 ViewHolder 加入二级缓存 mCacheViews 中;
  • 当二级缓存 mCacheViews容量超出时,则移除 index 为 0 的 ViewHolder,并且将其添加到 RecyclerViewPool 中;
  • 若holder没有成功添加到 mCacheViews 中,则直接添加进 RecyclerViewPool 缓存;
  • RecyclerViewPool 中会根据 ItemViewType 获取对应 ViewHolder 的 ArrayList 容器(如果还没初始化则直接 new 一个)判断容量后添加进去,添加前会清除 ViewHolder 的一些状态信息;

2.2 Recycler的复用

        @NonNull
        public View getViewForPosition(int position) {
            // 调用重载方法
            return getViewForPosition(position, false);
        }

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

        @Nullable
        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
            ...
            boolean fromScrapOrHiddenOrCache = false;
            ViewHolder holder = null;
            // 0) If there is a changed scrap, try to find from there
            // 0)  如果是预布局状态通过一级缓存 mChangedScrap 中查找
            if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }
            // 1) Find by position from scrap/hidden list/cache
            if (holder == null) {
            // 1) 内部会先从 mAttachedScrap 中尝试获取,获取不到则尝试从 mCacheViews 中寻找[二级缓存]
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                ...
                }
            }
            
            if (holder == null) {
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                ...

                final int type = mAdapter.getItemViewType(offsetPosition);
                // 2) Find from scrap/cache via stable ids, if exists
             // 2) 从scrap/cache中获取,跟上面的区别是这次根据 mHasStableIds 寻找
                if (mAdapter.hasStableIds()) {
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    if (holder != null) {
                        // update position
                        holder.mPosition = offsetPosition;
                        fromScrapOrHiddenOrCache = true;
                    }
                }
                // 3)从自定义缓存中获取[三级缓存]
                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 (view != null) {
                     // 通过 RecyclerView.LayoutParams 获取其 ViewHolder
                        holder = getChildViewHolder(view);
                        ...
                    }
                }
                
                if (holder == null) { // fallback to pool
                    ...
                    // 4)通过RecycledViewPool 获取【四级缓存】
                    holder = getRecycledViewPool().getRecycledView(type);
                    if (holder != null) {
                        holder.resetInternal();
                        if (FORCE_INVALIDATE_DISPLAY_LIST) {
                            invalidateDisplayListInt(holder);
                        }
                    }
                }
                //  四级缓存都没有获取到
                if (holder == null) {
                    long start = getNanoTime();
                    ...
                    // 通过 adapter **onCreateViewHolder** 方法创建 ViewHolder
                    holder = mAdapter.createViewHolder(RecyclerView.this, type);
                    ...
                    long end = getNanoTime();
                    mRecyclerPool.factorInCreateTime(type, end - start);
                    ...
                }
            }

            ...

            boolean bound = false;
            if (mState.isPreLayout() && holder.isBound()) {
                // do not update unless we absolutely have to.
                holder.mPreLayoutPosition = position;
            } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
                **// 如果没有绑定过数据 则调用 adapter onBindViewHolder 进行绑定**
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            }
            // 给 ViewHolder 的 itemView 设置 LayoutParams
            final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
            final LayoutParams rvLayoutParams;
            if (lp == null) {
                rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
                holder.itemView.setLayoutParams(rvLayoutParams);
            } else if (!checkLayoutParams(lp)) {
                rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
                holder.itemView.setLayoutParams(rvLayoutParams);
            } else {
                rvLayoutParams = (LayoutParams) lp;
            }
            // // 将 ViewHolder 赋值给 LayoutParams 
            rvLayoutParams.mViewHolder = holder;
            rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
            return holder;
        }

总结

  • 一级缓存 getChangedScrapViewForPosition,根据 position 从 mChangedScrap 中获取;
  • 二级缓存 getScrapOrHiddenOrCachedHolderForPosition、getScrapOrCachedViewForId,优先根据 position 从 mAttachScrap、mCacheViews 中获取,其次根据 itemId 再次从 mAttachScrap、mCacheViews 中获取;
  • 三级缓存 mViewCacheExtension.getViewForPositionAndType,自定义缓存,一定注意这里返回的是 View;
  • 四级缓存 getRecycledViewPool().getRecycledView,根据 itemViewType 获取对应 ViewHolder 的缓存 ArrayList,再从其中尝试获取。

获取的holder的顺序:一级缓存→二级缓存→三级缓存→四级缓存


2.3 RecyclerView回收与复用的触发时机

当滑动RecyclerView时,其入口就是 onTouchEvent()方法,从该方法开始,以此调用的方法有:

RecyclerView.onTouchEvent() // 处理touch事件

  • RecyclerView.scrollByInternal() // 调用scrollStep()计算滚动距离

    • RecyclerView.scrollStep() // 通过LayoutManager计算横/纵向滚动的距离

      • LayoutManager.scrollHorizontallyBy() 、scrollVerticallyBy() // 调用 scrollBy()

        • LayoutManager.scrollBy() //通过调用 fill() 添加滑进来的View、回收滑出去的 View

          • LayoutManager.fill() //触发回收、复用操作

这里以LinearLayoutManager为例

   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) {
             ...
            **// 回收**
            **recycleByLayoutState(recycler, layoutState);**
        }
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
             ...
            **// 复用**
            **layoutChunk(recycler, state, layoutState, layoutChunkResult);**
            ...
        }
        if (DEBUG) {
            validateChildOrder();
        }
        return start - layoutState.mAvailable;
    }

核心方法就是如下两个:

  • 回收:recycleByLayoutState()
  • 复用:layoutChunk()
    private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
        if (!layoutState.mRecycle || layoutState.mInfinite) {
            return;
        }
        int scrollingOffset = layoutState.mScrollingOffset;
        int noRecycleSpace = layoutState.mNoRecycleSpace;
        if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
            recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);
        } else {
            recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
        }
    }

2.3.1 回收时机

回收方法从LayoutManager的recycleByLayoutState()开始,其调用流程为:

  • recycleByLayoutState() // 根据布局状态 回收view 进一步调用recycleViewsFromXXX

    • recycleViewsFromEnd() 、recycleViewsFromStart() 回收头部、尾部view

      • recycleChildren() //回收child

        • removeAndRecycleViewAt()
        public void removeAndRecycleViewAt(int index, Recycler recycler) {
                    final View view = getChildAt(index);
                    removeViewAt(index);
                    // 最终就会调用Recycler的recycleView()进行回收 见3.1
                    recycler.recycleView(view);
                }
        

2.3.2 复用时机

回收方法从LayoutManager的layoutChunk()开始,其调用流程为:

  • layoutChunk() // 调用添加view的方法 addView()

    • addView() // 调用重载方法 & addViewInt()

      • next() // 获取应该布局的下一个视图

         View next(RecyclerView.Recycler recycler) {
                    if (mScrapList != null) {
                        return nextViewFromScrapList();
                    }
                    // 最终就会调用Recycler的**getViewForPosition**()进行复用 见3.2
                    final View view = **recycler.getViewForPosition**(mCurrentPosition);
                    mCurrentPosition += mItemDirection;
                    return view;
                }
        

综上,可以看出复用和回收ViewHolder的操作是在LayoutManager中进行管理的;但最终的实现是交给Recycler去完成的。


Question

  1. 滑进来的 View 是怎么来的?
  • 如果是 isPreLayout 则先从 mChangedScrap 中尝试获取。
  • 未获取到,再从 mAttachedScrap / mHiddenViews / mCachedViews (通过 position ) 中尝试获取
  • 未获取到,再从 mAttachedScrap / mCachedViews (通过 id)中尝试获取
  • 未获取到,再从(三级缓存) 自定义缓存中尝试获取
  • 未获取到,再从 (四级缓存)RecycledViewPool 中尝试获取
  • 未获取到,创建一个新的 ViewHolder ( onCreateViewHolder() )
  1. 滑出去的 View 最后去哪里?
  • 需要回收的holder存在于一级缓存(mAttachedScrap、mChangedScrap),则从中移除,回收到 二级缓存 mCacheViews 中
  • 二级缓存mCacheViews超出容量 或未成功时,将其回收到 RecyclerViewPool