RecyclerView(三)—— RecyclerView的缓存机制

RecyclerView内存优越性,得益于它独特的缓存机制。

1 如何复用表项

如果列表中的每个表项在移出屏幕时被销毁,移入时又被重新创建,是很消耗资源,所以RecyclerView引入了缓存机制。缓存是为了复用,复用的好处是有可能免去两个耗费资源的操作:创建表项视图和为每个表项视图绑定数据。

2 ViewHolder

2.1 作用

ViewHolder是对RecyclerView上的itemView的封装,它是RecyclerView缓存的载体。它封装了以下属性:

  • View itemView:对应RecyclerView的子View
  • int mPositionView当前对应数据在数据源中的位置;
  • int mOldPositionView上次绑定的数据在数据源中的位置;
  • long mItemId:可以判断ViewHolder是否需要重新绑定数据;
  • int mItemViewTypeitemView对应的类型;
  • int mPreLayoutPosition:在预布局阶段ViewHolder对应数据在数据源中的位置;
  • int mFlagsViewHolder对应的标记位;
  • List<Ojbect> mPayloads:实现局部刷新;
  • Recycler mScrapContainer:如果不为空,表示ViewHolder是存放在scrap缓存中;
2.2 flag
  • FLAG_BOUNDViewHolder对应的View已经绑定好了数据,无需重新绑定
  • FLAG_UPDATE:数据发生了变化,View需要重新绑定
  • FLAG_INVALID:数据失效了,View需要重新绑定
  • FLAG_REMOVED:数据从数据源中删除,View在消失动画中仍然有用
  • FLAG_NOT_RECYCLABLEViewHolder不能被回收,ViewHolder对应ItemView做动画时需要保证ViewHolder不能被回收掉
  • FLAG_RETURNED_FROM_SCRAP:从scrap缓存中获取到的ViewHolder
  • FLAG_IGNORE:如果回收该类型的ViewHolder会报错
  • FLAG_TMP_DETACHED:表示ItemViewRecyclerView上DETACHED了,detachremove的区别是,remove会将ViewViewGroupchildren数组中删除并且刷新ViewGroupdetach只会删除不会触发刷新
  • FLAG_ADAPTER_FULLUPDATE:表示ViewHolder需要全量更新,如果没有设置该标志位,则是局部更新
  • FLAG_MOVED:当ViewHolder的位置发生变化,做动画时需要使用
  • FLAG_APPEARED_IN_PRE_LAYOUTViewHolder出现在预布局中,需要做APPEARED动画

3 RecyclerView局部刷新

RecyclerView的数据更新,主要有以下几个方法:

  • notifyDataSetChanged():刷新全部可见的ViewHolder
  • notifyItemChanged(int position):刷新指定位置的ViewHolder;
  • notifyItemChanged(int position, @Nullable Object payload):刷新指定位置的ViewHolder,其中playload参数可以认为是要刷新的一个标示;
  • notifyItemRangeChanged(int positionStart, int itemCount):从指定的位置开始刷新指定个数的ViewHolder
  • notifyItemRangeChanged(int positionStart, int itemCount, Object payload):从指定的位置开始刷新指定个数的ViewHolder,其中playload参数可以认为是要刷新的一个标示;
  • notifyItemInserted(int position)notifyItemMoved(int fromPosition, int toPosition)notifyItemRangeInserted(int positionStart, int itemCount)notifyItemRemoved(int position)notifyItemRangeRemoved(int positionStart, int itemCount):插入、移动、移除并自动刷新;
@Override
public void onBindViewHolder(ViewHolderholder, int position, List<Object> payloads) {
  if (payloads.isEmpty()) {
    // payloads为空,说明是更新整个ViewHolder
    onBindViewHolder(holder, position);
  } else {
    // payloads不为空,这只更新需要更新的View即可。
    String payload = payloads.get(0).toString();
    if ("changeColor".equals(payload)) {
      holder.textView.setTextColor("");
    }
  }
}

4 四级缓存

以下是RecylcerView缓存机制的时序图:

RecyclerView的缓存机制

其中,Recycler用于表项的复用,RecyclerView通过Recycler获得下一个待绘制的表项。RecyclerView缓存基本上是通过三个内部类管理的,RecyclerRecycledViewPoolViewCacheExtension,以下是RecyclerView.Recycler的部分源码:

public class RecyclerView extends ViewGroup implements ScrollingView {

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

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

    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;
      }
      if (DEBUG && scrapHeap.contains(scrap)) {
        throw new IllegalArgumentException("this scrap item already exists");
      }
      scrap.resetInternal();
      scrapHeap.add(scrap);
    }
  }

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

scrap [skræp] 碎片,小块; attach [əˈtætʃ] 系上,贴上 detached [dɪˈtætʃt] 单独的,分离的 extension [ɪkˈstenʃn] 延伸,扩展

由上可知,Recycler4个变量用来缓存ViewHolder对象,优先级由高到低依次为ArrayList<ViewHolder> mAttachedScrapArrayList<ViewHolder> mCachedViewsViewCacheExtension mViewCacheExtensionRecycledViewPool mRecyclerPoolRecycledViewPool ViewHolderviewType分类存储(通过SparseArray),同类ViewHolder存储在默认大小为5ArrayList中。

以下是它们的含义:

Recycler的成员变量

RecyclerView的四级缓存:

  • mChangedScrapmAttachedScrap是用来做屏幕内ViewHolder复用,不需要重新onCreateViewHolderonBindViewHolder
  • mCachedView是用来缓存最近移出屏幕的ViewHolder,包括数据和position信息。复用时必须是是相同位置的ViewHolder,应用场景是在那些来回滑动的列表中,往回滑动时,能直接复用ViewHolder的数据,不用onBindeViewHolder
  • mViewCacheExtension,自定义缓存,这个的创建和缓存完全由开发者自己控制,系统未在这里添加数据;
  • RecycledViewPoolViewHolder缓存池,当mCacheView满后或者adapter被更换,将mCacheView中移出的ViewHolder放到缓存池中,同时把ViewHolder的数据清除掉,所以复用的时候需要onBindeViewHolder

四级缓存

4.1 屏幕内缓存/一级缓存/scrap缓存(mAttachedScrapmChangedScrap

屏幕内缓存指在屏幕中显示的ViewHolder,这些ViewHolder会缓存在mAttachedScrapmChangedScrap中:

  • mAttachedScrap中存放的是没有与RecyclerView分离的ViewHolder列表,从其中复用的ViewHolder既不需要重新创建也不需要重新绑定数据;
  • mChangedScrap表示数据已经改变的ViewHolder列表,只有满足以下的条件ViewHolder才会被添加到mChangedScrap中:当它关联的itemView发生了变化(notifyItemChanged或者notifyItemRangeChanged被调用),并且ItemAnimator调用ViewHolder#canReuseUpdatedViewHolder方法时,返回了false(返回false表示要执行用一个view替换另一个 view的动画,例如淡入淡出动画。true表示动画在view内部发生)。否则,ViewHolder会被添加到mAttachedScrap中;

mChangedScrapmAttachedScrap可以看做是一个层级,都是屏幕上可见ViewHolder,只不过区分了状态(改变和未改变)。

4.2 屏幕外缓存/二级缓存(mCachedViews

当列表滑动出了屏幕时,ViewHolder会被缓存在mCachedViews ,其大小由mViewCacheMax决定,默认DEFAULT_CACHE_SIZE2,可通过Recyclerview.setItemViewCacheSize()动态设置。

mCachedViews中复用的ViewHolder ,只能复用于指定位置的表项。也可以说,mCachedViews是离屏缓存,用于缓存指定位置的ViewHolder ,只有“列表回滚”这一种场景(刚滚出屏幕的表项再次进入屏幕),才有可能命中该缓存。该缓存存放在默认大小为2ArrayList中;

4.3 自定义缓存/三级缓存(ViewCacheExtension

可以自己实现ViewCacheExtension类实现自定义缓存,可通过Recyclerview.setViewCacheExtension()设置。

4.4 缓存池/四级缓存(RecycledViewPool

ViewHolder在首先会缓存在mCachedViews中,当超过了个数(比如默认为2),就会添加到RecycledViewPool中。从mRecyclerPool中复用的ViewHolder,只能复用于viewType相同的表项,并且需要重新绑定数据。

RecycledViewPool会根据每个ViewTypeViewHolder分别存储在不同的列表中,每个ViewType最多缓存DEFAULT_MAX_SCRAP = 5 x ViewHolder,如果RecycledViewPool没有被多个RecycledView共享,对于线性布局,每个ViewType最多只有一个缓存,如果是网格有多少行就缓存多少个。它们之间的关系如下 :

缓存池

5 缓存策略

缓存流程:

  • 在插入或删除ViewHolder的时候,首先会把屏幕内的ViewHolder保存至mAttachedScrap中;
  • 滑动屏幕时,首先消失的ViewHolder会保存到mCachedViews中,mCachedView的大小默认是2,超过的话,就按照先入先出的原则,将移出头部的ViewHolder保存到RecyclerPool缓存池(如果有自定义的缓存就保存到自定义缓存中)。RecyclerPool缓存池会按照ViewHolderviewType进行保存,每个viewType缓存个数为5个,超过了就会被回收;

获取缓存流程:首先从mAttachedScrap中获取,通过position匹配ViewHolder,如果匹配失败,就会从mCachedViews中获取,也是通过position获取ViewHolder缓存;如果获取失败,就会继续从自定义缓存中获取;如果获取失败,会继续从mRecyclerPool中获取;如果获取失败,会重新创建ViewHolder 流程如下 :

缓存策略

Recyclerview在获取ViewHolder时按四级缓存的顺序查找,如果没找到就创建(createViewHolder)。其中只有RecycledViewPool找到时才会调用 onBindViewHolder,其它缓存不会重新onBindViewHolder

问:在RecyclerView中,滑动10ViewHolder,再滑动回去,会有几个ViewHolder执行onBindViewHolder?

假设页面可以容纳7条数据

  • 首先,页面内的7条数据会依次调用onCreateViewHolderonBindViewHolder
  • 之后,向下滑动1条(position = 7),会把position = 0的数据存放到mCachedViews中,此时mCachedViews缓存数量为1RecyclerPool中缓存数量为0position = 7的数据通过positionmCachedViews找不到对应的ViewHolder,通过viewTypemRecyclerPool中找不到对应的ViewHolder,所以会调用onCreateViewHolderonBindViewHolder方法;
  • 再往下滑动1条(position = 8),同position = 7position = 1的数据会存放到mCachedViews
  • 再往下滑动1条(position = 9),由于mCahcedViews的缓存区默认容量为2,所以,position = 0的数据会被清空,存放到mRecyclerPool缓存池中,而position = 2的数据会存放到mCachedViews,而position = 9的数据无法通过positionmCacheViews中获取,也无法通过viewTypemRecyclerPool中获取,所以还会调用onCreateViewHolderonBindViewHolder。此时,mCachedViews缓存区数量是2mRecyclerPool的数量为1;
  • 再往下滑动1条(position = 10),这个时候就可以通过viewTypemRecyclerPool中查找到ViewHolder,所以可以直接复用了,并通过onBindeViewHolder来绑定数据;
  • 依次类推

RecyclerView中,并不是每次绘制表项都会重新创建ViewHolder对象,也不是每次都会重新绑定ViewHolder数据。

  • 最坏的情况:重新创建ViewHolder并绑定数据;
  • 次好的情况:复用ViewHolder但重新绑定数据;
  • 最好的情况:复用ViewHolder且不重新绑定数据;

通过了解RecyclerView的四级缓存,可以知道,RecyclerView最多可以缓存N(屏幕最多可显示的item数) + 2(屏幕外的缓存) + 5 x M (M代表M个ViewType,缓存池的缓存),只有RecycledViewPool找到时才会重新调用bindViewHolder
还需要注意的是,RecycledViewPool可以被多个RecyclerView共享,其缓存个数与ViewType个数、布局相关,如果RecycledViewPool没有被多个RecycledView共享,对于线性布局,每个ViewType最多只有一个缓存,如果是网格布局有多少行就缓存多少个。

参考

https://juejin.cn/post/6844903778303344647
https://zhooker.github.io/2017/08/14/关于Recyclerview的缓存机制的理解/
http://www.lxiaoyu.com/p/286763
https://www.jianshu.com/p/b11c62871169
https://jishuin.proginn.com/p/763bfbd55a86
https://www.jianshu.com/p/443d741c7e3e

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值