谈一谈 RecyclerView 的缓存机制

网上有很多关于RecyclerView缓存的文章,那么为什么还要写这篇文章?写本文之前我也浏览了一些网上点击量比较高的文章,总体写的还不错,美中不足的是有的知识点,他们未必理解明白,有的用错误的结论表述,有的则一笔带过。为了让读者更快速的决定要不要观看此文,提出如下几个问题,如果你能给出正确答案,那么此文的知识点基本都掌握了。

  • mAttachedScrap是干嘛的?这级缓存跟开发者的关系大吗?
  • mChangedScrap又是干嘛的?跟开发者的关系大吗?
  • 在一级缓存的维度上,为什么要同时设计mAttachedScrap和mChangedScrap两个不同的缓存?
  • mUnmodifiableAttachedScrap的设计小技巧是什么?
  • mCachedViews的缓存个数,以及该缓存中的ViewHolder有什么特性?
  • mViewCacheExtension虽说是给开发者定制缓存策略的,但并没什么软用。
  • RecyclerPool可以给多个RecyclerView共享缓存对象,但是如果设置不当,也会造成严重的性能问题?
  • hasStableIds返回true,到底有啥用?
  • onBindViewHolder(VH holder, int position, List payloads)这个方法到底该如何使用才好?

2. 关于RecyclerView相关的文章

我之前写过一些关于RecyclerView的文章,阅读它们,有利于将RecyclerView的知识点串联起来,触类旁通。

1. RecyclerView滑动时干什么了

2. 详解RecyclerView动画原理之一

3. 详解RecyclerView动画原理之二

在RecyclerView滑动时干什么了一文中,讲解了RecyclerView滑动过程中复用和回收的先后顺序问题,但是并没有详细讲解RecyclerView的缓存机制,本文可作为该文的后续补充。

在详解RecyclerView动画原理系列文章中,接触到了mAttachedScrap,在LayoutManager的onLayoutChildren方法中,会先把RecyclerView上的View剥离开,放入到mAttachedScrap中,该文涉及到了缓存机制的一级缓存,阅读该文可以很好的了解mAttachedScrap是干嘛用的。

3. RecyclerView缓存架构图

RecyclerView的缓存,一图以蔽之

由图可知,RecyclerView缓存是一个四级缓存的架构。当然,从RecyclerView的代码注释来看,官方认为只有三级缓存,即mCachedViews是一级缓存,mViewCacheExtension是二级缓存,mRecyclerPool是三级缓存。从开发者的角度来看,mAttachedScrap和mChangedScrap对开发者是不透明的,官方并未暴露出任何可以改变他们行为的方法。

3.1 mCacheViews可以通过如下方法,改变缓存的大小

```
public void setItemViewCacheSize(int size) {
    mRecycler.setViewCacheSize(size);
}
```

3.2 ViewCacheExtension则是一个抽象类,你完全可以自定义一个子类,修改获取缓存的策略。但是这个类只提供了获取缓存的接口,没有提供保存缓存的接口,对开发者要求甚高,而且使用RecyclerPool都能很好的实现一般的缓存需求。所以该接口,基本就是设计者的鸡肋,没啥软用。

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

3.3 RecyclerViewPool类提供了修改不同类型View的最大缓存数量,这对开发者很透明

```
public void setMaxRecycledViews(int viewType, int max) {
    ScrapData scrapData = getScrapDataForType(viewType);
    scrapData.mMaxScrap = max;
    final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
    while (scrapHeap.size() > max) {
        scrapHeap.remove(scrapHeap.size() - 1);
    }
}
```

4. RecyclerView$Recycler源码

我们知道RecyclerView的缓存功能是定义在RecyclerView$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;
} 

我们来依次讲解不同层级的缓存:

4.1 mAttachedScrap

mAttachedScrap的对应数据结构是ArrayList,在LayoutManager#onLayoutChildren方法中,对views进行布局时,会将RecyclerView上的Views全部暂存到该集合中,以备后续使用,该缓存中的ViewHolder的特性是,如果和RV上的position或者itemId匹配上了,那么认为是干净的ViewHolder,是可以直接拿出来使用的,无需调用onBindViewHolder方法。该ArrayList的大小是没有限制的,屏幕上有多少个View,就会创建多大的集合。触发该层级缓存的场景一般是调用notifyItemXXX方法。调用notifyDataSetChanged方法,只有当Adapter hasStableIds返回true,会触发该层级的缓存使用。

4.2 mChangedScrap

mChangedScrap和mAttachedScrap是同一级的缓存,他们是平等的。但是mChangedScrap的调用场景是notifyItemChanged和notifyItemRangeChanged,只有发生变化的ViewHolder才会放入到mChangedScrap中。mChangedScrap缓存中的ViewHolder是需要调用onBindViewHolder方法重新绑定数据的。那么此时就有个问题了,为什么同一级别的缓存需要设计两个不同的缓存?有何作用,阅读过动画原理系列文章详解RecyclerView动画原理之一和详解RecyclerView动画原理之二的同学会记得,在dispatchLayoutStep2阶段LayoutManager onLayoutChildren方法中最终会调用layoutForPredictiveAnimations方法,把mAttachedScrap中剩余的ViewHolder填充到屏幕上,所以他们的区别就是,mChangedScrap中的ViewHolder在RV填充满的情况下,是不会强行填充到RV上的。那么有办法可以让发生改变的ViewHolder进入mAttachedScrap缓存吗?当然可以。调用notifyItemChanged(int position, Object payload)方法可以,实现局部刷新功能,payload不为空,那么发生改变的ViewHolder是会被分离到mAttachedScrap中的。

4.3 mUnmodifiableAttachedScrap
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap)是对mAttachedScrap的封装,它将mAttachedScrap暴露给开发者调用,它的特性就是只可读不能写。

4.4 mCachedViews
mCachedViews对应的数据结构也是ArrayList但是该缓存对集合的大小是有限制的,默认是2。该缓存中ViewHolder的特性和mAttachedScrap中的特性是一样的,只要position或者itemId对应上了,那么它就是干净的,无需重新绑定数据。开发者可以调用setItemViewCacheSize(size)方法来改变缓存的大小。该层级缓存触发的一个常见的场景是滑动RV。当然notifyXXX也会触发该缓存。该缓存和mAttachedScrap一样特别高效。

4.5 ViewCacheExtension
ViewCacheExtension开发者自己实现的意义不大,基本上所有你想做的,都可以通过RecyclerViewPool来实现。

4.6 RecyclerViewPool
RecyclerViewPool缓存可以针对多ItemType,设置缓存大小。默认每个ItemType的缓存个数是5。而且该缓存可以给多个RecyclerView共享。由于默认缓存个数为5,假设某个新闻App,每屏幕可以展示10条新闻,那么必然会导致缓存命中失败,频繁导致创建ViewHolder影响性能。所以需要扩大缓存size。

5. 结束
本文没有涉及到代码的讲解。一来网上的资料太多了,而且讲解的比较全,二来本文侧重点在于讲解各级缓存的作用和区别。

PS:关于我


本人是一个拥有6年开发经验的帅气Android攻城狮,记得看完点赞,养成习惯,微信搜一搜「 程序猿养成中心 」关注这个喜欢写干货的程序员。

另外耗时两年整理收集的Android一线大厂面试完整考点PDF出炉,资料【完整版】已更新在我的【Github】,如有面试、进阶需要的朋友们可以去参考参考,如果对你有帮助,可以点个Star哦!

地址:【https://github.com/733gh/xiongfan】

推荐阅读更多精彩内容