关于android:深入理解Android-RecyclerView的缓存机制

2次阅读

共计 11294 个字符,预计需要花费 29 分钟才能阅读完成。

咱们晓得,RecyclerView 在大量数据时仍然能够丝滑般顺畅的滑动,那它到底是怎么实现的呢,而 RecyclerView 之所以好用得益于它优良的缓存机制。

咱们晓得,RecyclerView 自身是一个 ViewGroup,因而在滑动时就防止不了增加或移除子 View(子 View 通过 RecyclerView#Adapter 中的 onCreateViewHolder 创立),如果每次应用子 View 都要去从新创立,必定会影响滑动的流畅性,所以 RecyclerView 通过 Recycler 来缓存的是 ViewHolder(外部蕴含子 View),这样在滑动时能够复用子 View,某些条件下还能够复用子 View 绑定的数据。所以实质上来说,RecyclerView 之所以可能实现顺畅的滑动成果,是因为缓存机制,因为缓存缩小了反复绘制 View 和绑定数据的工夫,从而进步了滑动时的性能。

一、缓存

1.1、四级缓存

Recycler 缓存 ViewHolder 对象有 4 个等级,优先级从高到底顺次为:

  • mAttachedScrap:缓存屏幕中可见范畴的 ViewHolder;
  • mCachedViews:缓存滑动时行将与 RecyclerView 拆散的 ViewHolder,默认最大 2 个;
  • ViewCacheExtension:自定义实现的缓存;
  • RecycledViewPool:ViewHolder 缓存池,能够反对不同的 ViewType;

1.1.1 mAttachedScrap

mAttachedScrap 存储的是以后屏幕中的 ViewHolder,mAttachedScrap 的对应数据结构是 ArrayList,在调用 LayoutManager#onLayoutChildren 办法时对 views 进行布局,此时会将 RecyclerView 上的 Views 全副暂存到该汇合中,该缓存中的 ViewHolder 的个性是,如果和 RV 上的 position 或者 itemId 匹配上了那么能够间接拿来应用的,无需调用 onBindViewHolder 办法。

1.1.2 mChangedScrap

mChangedScrap 和 mAttachedScrap 属于同一级别的缓存,不过 mChangedScrap 的调用场景是 notifyItemChanged 和 notifyItemRangeChanged,只有发生变化的 ViewHolder 才会放入到 mChangedScrap 中。mChangedScrap 缓存中的 ViewHolder 是须要调用 onBindViewHolder 办法从新绑定数据的。

1.1.3 mCachedViews

mCachedViews 缓存滑动时行将与 RecyclerView 拆散的 ViewHolder,按子 View 的 position 或 id 缓存,默认最多寄存 2 个。mCachedViews 对应的数据结构是 ArrayList,然而该缓存对汇合的大小是有限度的。

该缓存中 ViewHolder 的个性和 mAttachedScrap 中的个性是一样的,只有 position 或者 itemId 对应就无需从新绑定数据。开发者能够调用 setItemViewCacheSize(size) 办法来扭转缓存的大小,该层级缓存触发的一个常见的场景是滑动 RecyclerView。当然调用 notify() 也会触发该缓存。

1.1.4 ViewCacheExtension

ViewCacheExtension 是须要开发者本人实现的缓存,基本上页面上的所有数据都能够通过它进行实现。

1.1.5 RecyclerViewPool

ViewHolder 缓存池,实质上是一个 SparseArray,其中 key 是 ViewType(int 类型),value 寄存的是 ArrayList< ViewHolder>,默认每个 ArrayList 中最多寄存 5 个 ViewHolder。

1.2 四级缓存比照

缓存级别 波及对象 阐明 是否从新创立视图 View 是否从新绑定数据
一级缓存 mAttachedScrap mChangedScrap 缓存屏幕中可见范畴的 ViewHolder false false
二级缓存 mCachedViews 缓存滑动时行将与 RecyclerView 拆散的 ViewHolder,按子 View 的 position 或 id 缓存 false false
三级缓存 mViewCacheExtension 开发者自行实现的缓存
四级缓存 mRecyclerPool ViewHolder 缓存池,实质上是一个 SparseArray,其中 key 是 ViewType(int 类型),value 寄存的是 ArrayList< ViewHolder>,默认每个 ArrayList 中最多寄存 5 个 ViewHolder false true

1.3 调用过程

通常,RecyclerView 滑动时会触发 onTouchEvent#onMove,回收及复用 ViewHolder 在这里就会开始。咱们晓得设置 RecyclerView 时须要设置 LayoutManager,LayoutManager 负责 RecyclerView 的布局,蕴含对 ItemView 的获取与复用。以 LinearLayoutManager 为例,当 RecyclerView 从新布局时会顺次执行上面几个办法:

  • onLayoutChildren():对 RecyclerView 进行布局的入口办法
  • fill(): 负责对残余空间一直地填充,调用的办法是 layoutChunk()
  • layoutChunk():负责填充 View, 该 View 最终是通过在缓存类 Recycler 中找到适合的 View 的

上述的整个调用链:onLayoutChildren()->fill()->layoutChunk()->next()->getViewForPosition(),getViewForPosition() 即是是从 RecyclerView 的回收机制实现类 Recycler 中获取适合的 View。

二、复用流程

RecyclerView 对 ViewHolder 的复用是从 LayoutState 的 next() 办法开始的。LayoutManager 在布局 itemView 时,须要获取一个 ViewHolder 对象,如下所示。

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

next 办法调用 RecyclerView 的 getViewForPosition 办法来获取一个 View,而 getViewForPosition 办法最终会调用到 RecyclerView 的 tryGetViewHolderForPositionByDeadline 办法,而 RecyclerView 真正复用的外围就在这里。

    @Nullable
    ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
        ViewHolder holder = null;
        // 0) 如果它是扭转的废除的 ViewHolder,在 scrap 的 mChangedScrap 找
        if (mState.isPreLayout()) {holder = getChangedScrapViewForPosition(position);
            fromScrapOrHiddenOrCache = holder != null;
        }
        // 1) 依据 position 别离在 scrap 的 mAttachedScrap、mChildHelper、mCachedViews 中查找
        if (holder == null) {holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        }

        if (holder == null) {final int type = mAdapter.getItemViewType(offsetPosition);
            // 2) 依据 id 在 scrap 的 mAttachedScrap、mCachedViews 中查找
            if (mAdapter.hasStableIds()) {holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
            }
            if (holder == null && mViewCacheExtension != null) {//3) 在 ViewCacheExtension 中查找,个别不必到,所以没有缓存
                final View view = mViewCacheExtension
                        .getViewForPositionAndType(this, position, type);
                if (view != null) {holder = getChildViewHolder(view);
                }
            }
            //4) 在 RecycledViewPool 中查找
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {holder.resetInternal();
                if (FORCE_INVALIDATE_DISPLAY_LIST) {invalidateDisplayListInt(holder);
                }
            }
        }
        //5) 到最初如果还没有找到复用的 ViewHolder,则新建一个
        holder = mAdapter.createViewHolder(RecyclerView.this, type);
    }

能够看到,tryGetViewHolderForPositionByDeadline() 办法别离去 scrap、CacheView、ViewCacheExtension、RecycledViewPool 中获取 ViewHolder,如果没有则创立一个新的 ViewHolder。

2.1 getChangedScrapViewForPosition

个别状况下,当咱们调用 adapter 的 notifyItemChanged() 办法,数据发生变化时,item 缓存在 mChangedScrap 中,后续拿到的 ViewHolder 须要从新绑定数据。此时查找 ViewHolder 就会通过 position 和 id 别离在 scrap 的 mChangedScrap 中查找。

   ViewHolder getChangedScrapViewForPosition(int position) {
        // 通过 position
        for (int i = 0; i < changedScrapSize; i++) {final ViewHolder holder = mChangedScrap.get(i);
            return holder;
        }
        // 通过 id
        if (mAdapter.hasStableIds()) {final long id = mAdapter.getItemId(offsetPosition);
            for (int i = 0; i < changedScrapSize; i++) {final ViewHolder holder = mChangedScrap.get(i);
                return holder;
            }
        }
        return null;
    }

2.2 getScrapOrHiddenOrCachedHolderForPosition

如果没有找到视图,依据 position 别离在 scrap 的 mAttachedScrap、mChildHelper、mCachedViews 中查找,波及的办法如下。

    ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {final int scrapCount = mAttachedScrap.size();

        // 首先从 mAttachedScrap 中查找,精准匹配无效的 ViewHolder
        for (int i = 0; i < scrapCount; i++) {final ViewHolder holder = mAttachedScrap.get(i);
            return holder;
        }
        // 接着在 mChildHelper 中 mHiddenViews 查找暗藏的 ViewHolder
        if (!dryRun) {View view = mChildHelper.findHiddenNonRemovedView(position);
            if (view != null) {final ViewHolder vh = getChildViewHolderInt(view);
                scrapView(view);
                return vh;
            }
        }
        // 最初从咱们的一级缓存中 mCachedViews 查找。final int cacheSize = mCachedViews.size();
        for (int i = 0; i < cacheSize; i++) {final ViewHolder holder = mCachedViews.get(i);
            return holder;
        }
    }

能够看到,getScrapOrHiddenOrCachedHolderForPosition 查找 ViewHolder 的程序如下:

  • 首先,从 mAttachedScrap 中查找,精准匹配无效的 ViewHolder;
  • 接着,在 mChildHelper 中 mHiddenViews 查找暗藏的 ViewHolder;
  • 最初,从一级缓存中 mCachedViews 查找。

2.3 getScrapOrCachedViewForId

如果在 getScrapOrHiddenOrCachedHolderForPosition 没有找到视图,泽通过 id 在 scrap 的 mAttachedScrap、mCachedViews 中查找,代码如下。

  ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {
        // 在 Scrap 的 mAttachedScrap 中查找
        final int count = mAttachedScrap.size();
        for (int i = count - 1; i >= 0; i--) {final ViewHolder holder = mAttachedScrap.get(i);
            return holder;
        }

        // 在一级缓存 mCachedViews 中查找
        final int cacheSize = mCachedViews.size();
        for (int i = cacheSize - 1; i >= 0; i--) {final ViewHolder holder = mCachedViews.get(i);
            return holder;
        }
    }        

getScrapOrCachedViewForId() 办法查找的程序如下:

  • 首先,从 mAttachedScrap 中查找,精准匹配无效的 ViewHolder;
  • 接着,从一级缓存中 mCachedViews 查找;

2.4 mViewCacheExtension

mViewCacheExtension 是由开发者定义的一层缓存策略,Recycler 并没有将任何 view 缓存到这里。

if (holder == null && mViewCacheExtension != null) {final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
        if (view != null) {holder = getChildViewHolder(view);
        }
    }

这里没有自定义缓存策略,那么就找不到对应的 view。

2.5 RecycledViewPool

在 ViewHolder 的四级缓存中,咱们有提到过 RecycledViewPool,它是通过 itemType 把 ViewHolder 的 List 缓存到 SparseArray 中的,在 getRecycledViewPool().getRecycledView(type) 依据 itemType 从 SparseArray 获取 ScrapData,而后再从外面获取 ArrayList<ViewHolder>,从而获取到 ViewHolder。

    @Nullable
    public ViewHolder getRecycledView(int viewType) {final ScrapData scrapData = mScrap.get(viewType);// 依据 viewType 获取对应的 ScrapData 
        if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
            final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
            for (int i = scrapHeap.size() - 1; i >= 0; i--) {if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {return scrapHeap.remove(i);
                }
            }
        }
        return null;
    }

2.6 创立新的 ViewHolder

如果还没有获取到 ViewHolder,则通过 mAdapter.createViewHolder() 创立一个新的 ViewHolder 返回。

  // 如果还没有找到复用的 ViewHolder,则新建一个
  holder = mAdapter.createViewHolder(RecyclerView.this, type);

上面是寻找 ViewHolder 的一个残缺的流程图:

三、回收流程

RecyclerView 回收的入口有很多,然而不论怎么样操作,RecyclerView 的回收或者复用必然波及到 add View 和 remove View 操作,所以咱们从 onLayout 的流程动手剖析回收和复用的机制。

首先,在 LinearLayoutManager 中,咱们来到 itemView 布局入口的办法 onLayoutChildren(),如下所示。

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {if (state.getItemCount() == 0) {removeAndRecycleAllViews(recycler);// 移除所有子 View
                return;
            }
        }
        ensureLayoutState();
        mLayoutState.mRecycle = false;// 禁止回收
        // 颠倒绘制布局
        resolveShouldLayoutReverse();
        onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);

        // 临时拆散曾经附加的 view,行将所有 child detach 并通过 Scrap 回收
        detachAndScrapAttachedViews(recycler);
    }

在 onLayoutChildren() 布局的时候,先依据理论状况是否须要 removeAndRecycleAllViews() 移除所有的子 View,哪些 ViewHolder 不可用;而后通过 detachAndScrapAttachedViews() 临时拆散曾经附加的 ItemView,并缓存到 List 中。

detachAndScrapAttachedViews() 的作用就是把以后屏幕所有的 item 与屏幕拆散,将他们从 RecyclerView 的布局中拿下来,保留到 list 中,在从新布局时,再将 ViewHolder 从新一个个放到新的地位下来。

将屏幕上的 ViewHolder 从 RecyclerView 的布局中拿下来后,寄存在 Scrap 中,Scrap 包含 mAttachedScrap 和 mChangedScrap,它们是一个 list,用来保留从 RecyclerView 布局中拿下来 ViewHolder 列表,detachAndScrapAttachedViews() 只会在 onLayoutChildren() 中调用,只有在布局的时候,才会把 ViewHolder detach 掉,而后再 add 进来从新布局,然而大家须要留神,Scrap 只是保留从 RecyclerView 布局中以后屏幕显示的 item 的 ViewHolder,不参加回收复用,单纯是为了现从 RecyclerView 中拿下来再从新布局下来。对于没有保留到的 item,会放到 mCachedViews 或者 RecycledViewPool 缓存中参加回收复用。

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

下面代码的作用是,遍历所有 view,拆散所有曾经增加到 RecyclerView 的 itemView。

   private void scrapOrRecycleView(Recycler recycler, int index, View view) {final ViewHolder viewHolder = getChildViewHolderInt(view);
        if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                && !mRecyclerView.mAdapter.hasStableIds()) {removeViewAt(index);// 移除 VIew
            recycler.recycleViewHolderInternal(viewHolder);// 缓存到 CacheView 或者 RecycledViewPool 中
        } else {detachViewAt(index);// 拆散 View
            recycler.scrapView(view);//scrap 缓存
            mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
        }
    }

而后,咱们看 detachViewAt() 办法拆散视图,再通过 scrapView() 缓存到 scrap 中。

    void scrapView(View view) {final ViewHolder holder = getChildViewHolderInt(view);
        if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {holder.setScrapContainer(this, false);
            mAttachedScrap.add(holder);// 保留到 mAttachedScrap 中
        } else {if (mChangedScrap == null) {mChangedScrap = new ArrayList<ViewHolder>();
            }
            holder.setScrapContainer(this, true);
            mChangedScrap.add(holder);// 保留到 mChangedScrap 中
        }
    }

而后,咱们回到 scrapOrRecycleView() 办法中,进入 if() 分支。如果 viewHolder 是有效、未被移除、未被标记的则放到 recycleViewHolderInternal() 缓存起来,同时 removeViewAt() 移除了 viewHolder。

   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)) {int cachedViewSize = mCachedViews.size();
                if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {// 如果超出容量限度,把第一个移除
                    recycleCachedViewAt(0);
                    cachedViewSize--;
                }
                     ·····
                mCachedViews.add(targetCacheIndex, holder);//mCachedViews 回收
                cached = true;
            }
            if (!cached) {addViewHolderToRecycledViewPool(holder, true);// 放到 RecycledViewPool 回收
                recycled = true;
            }
        }
    }

如果符合条件,会优先缓存到 mCachedViews 中时,如果超出了 mCachedViews 的最大限度,通过 recycleCachedViewAt() 将 CacheView 缓存的第一个数据增加到终极回收池 RecycledViewPool 后再移除掉,最初才会 add() 新的 ViewHolder 增加到 mCachedViews 中。

剩下不符合条件的则通过 addViewHolderToRecycledViewPool() 缓存到 RecycledViewPool 中。

    void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {clearNestedRecyclerViewIfNotNested(holder);
        View itemView = holder.itemView;
        ······
        holder.mOwnerRecyclerView = null;
        getRecycledViewPool().putRecycledView(holder);// 将 holder 增加到 RecycledViewPool 中
    }

最初,就是在填充布局调用 fill() 办法的时候,它会回收移出屏幕的 view 到 mCachedViews 或者 RecycledViewPool 中。

 int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {recycleByLayoutState(recycler, layoutState);// 回收移出屏幕的 view
        }
    }

而 recycleByLayoutState() 办法就是用来回收移出屏幕的 view,残缺的流程如下图。

正文完
 0