RecyclerView的缓存获取机制

RecyclerView的缓存获取机制

RecyclerView是我们开发中接触比较多的控件。官方给他的定义是"A flexible view for providing a limited window into a large data set."定义中有个large data 很是醒目,那RecyclerView怎么处理大量的数据,而不oom和卡顿呢?这 就是RecyclerView里面的缓存机制,首先我们看一下几个基本概念:

  • Binding: 子视图显示adapter中与其对应位置的数据的过程。
  • Recycle (view): 之前用于展示adapter中某个位置数据的itemview,被缓存起来了,之后会被重新使用,来展示相同类型 的数据,这样可以避免布局的inflation和construction。
  • Scrap (view):在layout期间进入临时detached状态的ItemView。scrap视图可以在不完全detach父视图RecyclerView的 情况下被重用,如果不需要重新绑定,则可以不被修改;如果视图被认为是Dirty的,则可以由适配器Binding。
  • Dirty (view): Dirty的ItemView指的是需要经过adapter重新Binding的View.

Recycler类

在RecyclerView中有一个内部类Recycler来负责管理废弃或者分离的item view以供重用。我们先看Recycler的源码,如下:

    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;

        RecycledViewPool mRecyclerPool;

        private ViewCacheExtension mViewCacheExtension;

        static final int DEFAULT_CACHE_SIZE = 2;
    }

从源码中可以看出来该类有5个成员变量

  1. mAttachedScrap:存放的是attached父视图RecycleView的Scrap视图,不需要重新绑定就可以重用。没有容量限制。
  2. mChangedScrap:存放的是发生改变的Scrap视图,如果重用,需要重新经过adapter的绑定。

mChangedScrap 和 mAttachedScrap 只在布局阶段使用。其他时候它们是空的。布局完成之后,这两个缓存中的 viewHolder,会移到 mCacheView 或者 RecyclerViewPool 中。

  1. mCachedViews:存放的是已经remove的视图,已经和RecyclerView分离的视图,但是依然存放着position和Binding的数 据信息,容量默认为2.
  2. mRecyclerPool:存放的是被恢复出厂设置的view,没有任何的Binding痕迹。我们看一下RecycledViewPool的源码,可知 默认容量为5。里面有一个ScrapData内部类,内部类里面存储着ViewHolder,说明它是按照不同类型数据来分开存储的,这就是 RecyclerView的多种子布局的实现方式。
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<>();
}
  1. mViewCacheExtension:这一级缓存开发者可以扩展的缓存,ViewCacheExtension是一个抽象类,如果有需要可以自己定 义实现。
    public abstract static class ViewCacheExtension {

        @Nullable
        public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position, int type);
    }

读缓存的规则

讲完了,缓存的存放类型和形式,我们下面看一下缓存的存放规则。RecylerView的包下面一共有38个类文件,只RecyclerView 本身就有13501行代码,要全部读完不太现实,所有我们就借助于查找引用的方式来看具体的缓存策略。

首先我们查看了mAttachedScrap的使用,如下图,可以看出主要的添加和删除方法为:void scrapView(View view) 和void unscrapView(ViewHolder holder)两个方法。我们继续按照查找使用引用的方式来探索。

 最终发现了方法tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs),发 现该方法的上级调用 是getViewForPosition(int position, boolean dryRun),我们看一下它的源码,

ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {

            // 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);
			}
           // 2) Find from scrap/cache via stable ids, if exists
			if (mAdapter.hasStableIds()) {
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    if (holder != null) {
                        // update position
                        holder.mPosition = offsetPosition;
                        fromScrapOrHiddenOrCache = true;
                    }
           }
			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);
           }
			//3 creating it directly
           if (holder == null) {
          
               holder = mAdapter.createViewHolder(RecyclerView.this, type);
           }

           return holder;
        }

从该方法中我们可以看到,google的工程师的注释写的真好,直接用了序号0123来标记, 第1步:如果是pre-layout状态,会从changed scrap 也就是mChangedScrap中获取。具体是首先通过position来获取,如果 为空,则在通过stableid来获取,如果设置了stableid的话。

        ViewHolder getChangedScrapViewForPosition(int position) {
            // If pre-layout, check the changed scrap for an exact match.
            final int changedScrapSize;
            if (mChangedScrap == null || (changedScrapSize = mChangedScrap.size()) == 0) {
                return null;
            }
            // find by position
            for (int i = 0; i < changedScrapSize; i++) {
                final ViewHolder holder = mChangedScrap.get(i);
                if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
                    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                    return holder;
                }
            }
            // find by id
            if (mAdapter.hasStableIds()) {
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                if (offsetPosition > 0 && offsetPosition < mAdapter.getItemCount()) {
                    final long id = mAdapter.getItemId(offsetPosition);
                    for (int i = 0; i < changedScrapSize; i++) {
                        final ViewHolder holder = mChangedScrap.get(i);
                        if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) {
                            holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                            return holder;
                        }
                    }
                }
            }
            return null;
        }

第2步:是从scrap/hidden list/cache,调用getScrapOrHiddenOrCachedHolderForPosition方法来获取,这个方法是通 过position按照顺序从attach scrap、hidden children、cache中来获取view。我们知道attach scrap就是 mAttachedScrap缓存中获取,cache就是mCachedViews缓存中获取,那hidden children是什么东东?通过深入代码,我们在 类ChildHelper中变量final List mHiddenViews; hidden view指的是那些正在从 RecyclerView 边界中脱离的 view。为了让这些 view 正确地执行对应的分离动画,它 们仍然作为 RecyclerView 的子 view 被保留下来。

第3步:如果上一步没有获取到缓存,则如果adapter设置了stableId的话,则会通过stableId在mAttachedScrap中获取。

第4步, mViewCacheExtension中查找,我们说过这个对象默认是null的,是由我们开发者自定义缓存策略的一层,所以如果你 没有定义过,这里是找不到View的。

第5步,从RecycledViewPool里面获取。

第6步:如果上述5步骤都没有获取到的话,则通过adapter的createViewHolder方法来直接创建。

补充知识 StableID

如果调用 notifyDataSetChanged 的时候,Adapter 并没有设置 hasStableId,RecyclerView 不知道 发生了什么,哪一 些东西变化了,所以,它假设所有的东西都变了,每一个 ViewHolder 都是无效的,因此应该把它们放到 RecyclerViewPool 而不是 scrap 中。如果有stableId ViewHolder 会进入 scrap 而不是 pool 中。然后会通过特定的 Id(Adapter 中的 getItemId 获取到的 id)而不是 postion 到 scrap 中查找 ViewHolder。

优化建议

  •  尽量使用 notifyItem***()相关的方法进行局部的通知更新,而不是 notifyDataSetChanged()
  •  数据集的变化推荐使用DiffUtil,如果数据集比较大,还可以结合AsyncListDiffer在子线程做diff运算。
  •  如果特定 viewType 的 item 只有一个,可以通过 RecyclerView.getRecycledViewPool() .setMaxRecycledViews(viewType,1); 来调整缓存区的大小,减少内存占用
  •  RecyclerView嵌套RecyclerView的话,可以RecyclerView.setRecycledViewPool(@Nullable RecycledViewPool pool); 复用RecyclerView的缓存池。

猜你喜欢

转载自blog.csdn.net/challenge51all/article/details/120137666