RecyclerView 四级缓存
再次总结一下 RecyclerView 的四级缓存
- Scrap
- Cache
- ViewCacheExtension
- RecycledViewPool Scrap: 对应 ListView 的 Active View,就是屏幕内的缓存数据,就是相当于换了个名字,可以直接拿来复用。
Cache: 刚刚移出屏幕的缓存数据,默认大小是 2 个,当其容量被充满同时又有新的数据添加的时候,会根据 FIFO 原则,把优先进入的缓存数据移出并放到下一级缓存中,然后再把新的数据添加进来。Cache 里面的数据是干净的,也就是携带了原来的 ViewHolder 的所有数据信息,数据可以直接来拿来复用。需要注意的是,cache 是根据 position 来寻找数据的,这个 postion 是根据第一个或者最后一个可见的 item 的 position 以及用户操作行为(上拉还是下拉)。
举个栗子:当前屏幕内第一个可见的 item 的 position 是 1,用户进行了一个下拉操作,那么当前预测的 position 就相当于(1-1=0),也就是 position=0 的那个 item 要被拉回到屏幕,此时 RecyclerView 就从 Cache 里面找 position=0 的数据,如果找到了就直接拿来复用。
ViewCacheExtension: 是 google 留给开发者自己来自定义缓存的,这个 ViewCacheExtension 我个人建议还是要慎用,因为我扒拉扒拉网上其他的博客,没有找到对应的使用场景,而且这个类的 api 设计的也有些奇怪,只有一个public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position, int type);
让开发者重写通过 position 和 type 拿到 ViewHolder 的方法,却没有提供如何产生 ViewHolder 或者管理 ViewHolder 的方法,给人一种只出不进的赶脚,还是那句话慎用。
RecycledViewPool: 刚才说了 Cache 默认的缓存数量是 2 个,当 Cache 缓存满了以后会根据 FIFO(先进先出)的规则把 Cache 先缓存进去的 ViewHolder 移出并缓存到 RecycledViewPool 中,RecycledViewPool 默认的缓存数量是 5 个。RecycledViewPool 与 Cache 相比不同的是,从 Cache 里面移出的 ViewHolder 再存入 RecycledViewPool 之前 ViewHolder 的数据会被全部重置,相当于一个新的 ViewHolder,而且 Cache 是根据 position 来获取 ViewHolder,而 RecycledViewPool 是根据 itemType 获取的,如果没有重写 getItemType() 方法,itemType 就是默认的。因为 RecycledViewPool 缓存的 ViewHolder 是全新的,所以取出来的时候需要走 onBindViewHolder() 方法。
ref
Prefetch 功能的使用
google 官方在 Support Library v25 版本中,为 RecyclerView 增加了 Prefetch。 并且在 v25.1.0 以及 25.3.0 版本中进行了完善。在最新的稳定版本 25.3.1 中已经基本稳定。
Prefetch 默认就是处理开启的状态,通过 LinearLayoutManager 的 setItemPrefetchEnabled() 我们可以手动控制该功能的开启关闭。
我们都知道 android 是通过每 16ms 刷新一次页面来保证 ui 的流畅程度,现在 android 系统中刷新 ui 会通过 cpu 产生数据,然后交给 gpu 渲染的形式来完成,从上图可以看出当 cpu 完成数据处理交给 gpu 后就一直处于空闲状态,需要等待下一帧才会进行数据处理,而这空闲时间就被白白浪费了,如何才能压榨 cpu 的性能,让它一直处于忙碌状态,这就是 rv 的预取功能 (Prefetch) 要做的事情,rv 会预取接下来可能要显示的 item,在下一帧到来之前提前处理完数据,然后将得到的 itemholder 缓存起来,等到真正要使用的时候直接从缓存取出来即可。
预取代码理解
虽说预取是默认开启不需要我们开发者操心的事情,但是明白原理还是能加深该功能的理解。下面就说下自己在看预取源码时的一点理解。实现预取功能的一个关键类就是 gapworker,可以直接在 rv 源码中找到该类
GapWorker mGapWorker; |
rv 通过在 ontouchevent 中触发预取的判断逻辑,在手指执行 move 操作的代码末尾有这么段代码
case MotionEvent.ACTION_MOVE: { |
通过每次 move 操作来判断是否预取下一个可能要显示的 item 数据,判断的依据就是通过传入的 dx 和 dy 得到手指接下来可能要移动的方向,如果 dx 或者 dy 的偏移量会导致下一个 item 要被显示出来则预取出来,但是并不是说预取下一个可能要显示的 item 一定都是成功的,其实每次 rv 取出要显示的一个 item 本质上就是取出一个 viewholder,根据 viewholder 上关联的 itemview 来展示这个 item。而取出 viewholder 最核心的方法就是
tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) |
名字是不是有点长,在 rv 源码中你会时不时见到这种巨长的方法名,看方法的参数也能找到和预取有关的信息, deadlineNs 的一般取值有两种,一种是为了兼容版本 25 之前没有预取机制的情况,兼容 25 之前的参数为
static final long FOREVER_NS = Long.MAX_VALUE; |
,另一种就是实际的 deadline 数值,超过这个 deadline 则表示预取失败,这个其实也好理解,预取机制的主要目的就是提高 rv 整体滑动的流畅性,如果要预取的 viewholder 会造成下一帧显示卡顿强行预取的话那就有点本末倒置了。
关于预取成功的条件通过调用
boolean willCreateInTime(int viewType, long approxCurrentNs, long deadlineNs) { |
来进行判断,approxCurrentNs 的值为
long start = getNanoTime(); |
而 mCreateRunningAverageNs 就是创建同 type 的 holder 的平均时间,感兴趣的可以去看下这个值如何得到,不难理解就不贴代码了。关于预取就说到这里,感兴趣的可以自己去看下其余代码的实现方式,可以说 google 对于 rv 还是相当重视的,煞费苦心提高 rv 的各种性能,据说最近推出的 viewpager2 控件就是通过 rv 来实现的,大有 rv 控件一统天下的感觉。
预取功能的应用
预取功能是默认开启的,在对应的 RecyclerView.LayoutManager 中提供了一个setInitialPrefetchItemCount(int itemCount)
来设置预取个数。有一种场景,比如你在垂直列表里面有一个水平滚动列表的时候,竖屏每一行都是展示三个半 item,可以调用内部(横向)Recyclerview 的 innerLLM.setInitialItemsPrefetchCount(4)
,这样当水平列表将要展示在屏幕上的时候,如果 UI 线程有空闲时间,RecyclerView 会尝试在内部预先把这几个 item 取出来。
Ref
RecyclerView 预加载机制源码分析 :https://blog.csdn.net/weishenhong/article/details/81150172
https://www.jianshu.com/p/1d2213f303fc
延伸阅读 Recyclerview 缓存
RecyclerView 源码分析 (三) - RecyclerView 的缓存机制
【进阶】RecyclerView 源码解析 (三)——深度解析缓存机制
本文链接:http://agehua.github.io/2019/07/17/RecyclerView-pre-initiate/