手摸手第二弹,可视化 RecyclerView 缓存机制

8,782 阅读5分钟

前言

开题前,笔者还是要说几句先,依旧和前文一样,文章内不涉及源码讲解,默认各位读者对源码有一定的了解,撰文的原因也如同前文,因为笔者认为当下在 ListView/RecyclerView 的源码讲解的文章中,大都是对着源码噼里啪啦,实在有些晦涩难懂,于是笔者想将部分数据可视化,手摸手带领读者去了解一下缓存机制的实现,另推荐阅读腾讯 Bugly 的《Android ListView与RecyclerView对比浅析--缓存机制》一文。

1.前文地址:《可视化 ListView 缓存机制,手摸手带你打通任督二脉》

2.本文项目地址:RecyclerViewVisualization 或直接下载 apk

希望阅读本文前请先阅读前文,本文所涉及的一些关键字在上文有所提及。

一缓

手摸手打开 app:

这里写图片描述

RecyclerView 中的一缓 mAttachedScrap 与 ListView 中的一缓 mActiveViews 功能是基本相似的,为了屏幕内 item 快速复用而存在(RecyclerView/ListView 具有两次 onLayout() 过程,第二次 onLayout() 中直接使用第一次 onLayout() 缓存的 View,而不必再创建)。

二缓

实际上,二缓 mCachedViews 加上四缓 RecyclerViewPool 合在一起与 ListView 的二缓 mScrapedViews 意义相同,为了即将给即将入屏的 item 复用而存在。下面来细谈下二缓:

  • ArrayList 类型
  • 默认 size 为 2
  • size 可变
  • 复用算法是从尾部倒序匹配 ViewHolder position 与传入的 position 是否相等,匹配成功则返回
  • 为了优化上一步,下一个可能出现的 item 将会被置于尾部

二缓是通过 position 来匹配相应的 ViewHolder 的,这里的 position 指的是 RecyclerView 预测的、可能进入屏幕的 item 的 position,它是由当前屏幕滑动方向和可见的 item 位置来共同决定的。例如:屏幕向下滑动,那么可能进入屏幕的 item 的 position 就是当前可见第一个 item 的 position - 1;屏幕向上滑动,那么可能进入屏幕的 item 的 position 就是当前可见的最后一个 item 的 position + 1。这样说起来可能有些模糊,举个例子:

这里写图片描述

以上述状态来说,如果屏幕下滑,那么预测下一个可能出现在屏幕上的 item 的 position 可能是 4(也就是 Item1『E(layoutPosition:4)』);而如果屏幕上滑,预测的下一个出现在屏幕上的 item 的 position 是 0(也就是 Item『A(layoutPosition:0)』)。然后通过将 position 用于与 mCacheViews 中的 ViewHolder 的 layoutPosition 做比较,如果相同则返回该 ViewHolder。

来看动图:

1.屏幕上滑:

这里写图片描述

可以看到 target mCacheView position 由 0 变成了 4。与此同时,mCachedViews 将可能出现在屏幕上的 item 的位置从原有的位置调整为 ArrayList 的最后一位。

2.屏幕下滑:

这里写图片描述

屏幕下滑的话,target mCachedView position 由 4 变成了 0。与此同时,mCachedViews 内部也会做相应的调整。

四缓

四缓(RecycledViewPool)性质:

  • 内部维护了一个 SparseArray

  • SparseArray key 为 ViewHolder 的 ViewType,这说明每一套 ViewHolder 都具有自己的缓存数据

  • SparseArray value 为 ScrapData 类型,ScrapData 就是关键的缓存数据了,其数据结构简略如下:

      static class ScrapData {
          ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
          int mMaxScrap = 5;
          // ...
      }
    

由此可见,针对每一种 ViewHolder,RecycledViewPool 都会维护一个默认大小为 5 的 ArrayList 来用做缓存它们,当然,这里还需要提及的一点是,ArrayList 的默认大小被限制为 5,但是这个值是可以通过 RecycledViewPool#setMaxRecycledViews(viewType, max) 来替换的,比如想换成大一点的 10、20,都是可以的(这也是该数据类型为 ArrayList 而不是数组的原因之一)。

前面说到——“实际上,二缓 mCachedViews 加上四缓 RecyclerViewPool 合在一起与 ListView 的二缓 mScrapedViews 意义相同,为了即将给即将入屏的 item 复用而存在。”,可能有小伙伴疑惑了,既然意义相同,为何不是只有二缓就足够了,还要多一个四缓来更复杂?缘由在于:可以由开发者主动向内填充数据(RecycledViewPool#putRecycledView(ViewHolder),技术上可以实现多个 RecyclerView 共用同一个 RecyclerViewPool(RecyclerView#setRecycledViewPool(RecycledViewPool))。这两点在笔者看来,是在某种业务场景下选择 RecyclerView 还是 ListView 的一个重要缘由所在。至于这两点的实践,第一点笔者已经添加在 Demo 中了(含彩蛋),第二点笔者就不在此处扩展了,各位读者可以自行添加入 RecyclerViewVisualization Demo 中,相应的数据也都会被展示到屏幕上~

其他

谈谈 BindView(与 ListView 对比)

一个 View 被完整的展示到屏幕上,应该经过创建 View 和给 View 添加数据(BindView)两个过程,所以实际上缓存机制不仅仅针对于 View 要做缓存,最好还能对添加数据的这个过程再优化下,毕竟 setText()、setImage() 也可能是一个耗时操作。RecyclerView 就针对此做了优化,我们知道,ListView 实际上缓存的是 View,而 RecyclerView 实际上缓存的是 ViewHolder,这就意味着 ListView 虽然可以复用 View,但是给 View 添加数据这个过程就不能复用了,而如果是复用 ViewHolder 的话,不仅复用了 View,同时将给 View 添加数据的这个过程也被“缓存”起来了,而 RecyclerView 就是这么干的 ——

这里写图片描述

我们可以看到,但凡是被二缓缓存起来的 ViewHolder 再被展示到屏幕上,是不会触发 BindViewHolder 这个过程的。

ps:当然,这得基于数据源不变,如果数据源改变,肯定得重新给 View 添加数据。

谈谈 BindView(局部刷新)

RecyclerView 相比于 ListView 还提供了局部刷新的接口,这让 RecyclerView 在性能上又有了一个亮点:

1.局部刷新:

这里写图片描述

2.全局刷新:

这里写图片描述

可以看到,局部刷新能够只针对改变的 View 进行 bind view,而全局刷新会针对被影响到的所有的 View 都进行 bind view。所以,在日常使用中,是不是应该多考虑使用局部刷新代替全局刷新呢?