阅读 3874

聊一聊RecyclerView的缓存机制

1. 引言

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

  1. mAttachedScrap是干嘛的?这级缓存跟开发者的关系大吗?
  2. mChangedScrap又是干嘛的?跟开发者的关系大吗?
  3. 在一级缓存的维度上,为什么要同时设计mAttachedScrap和mChangedScrap两个不同的缓存?
  4. mUnmodifiableAttachedScrap的设计小技巧是什么?
  5. mCachedViews的缓存个数,以及该缓存中的ViewHolder有什么特性?
  6. mViewCacheExtension虽说是给开发者定制缓存策略的,但并没什么软用。
  7. RecyclerPool可以给多个RecyclerView共享缓存对象,但是如果设置不当,也会造成严重的性能问题?
  8. hasStableIds返回true,到底有啥用?
  9. 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. 结束

    本文没有涉及到代码的讲解。一来网上的资料太多了,而且讲解的比较全,二来本文侧重点在于讲解各级缓存的作用和区别。关于开头的灵魂几问,如果您还不确定,赶紧翻开源码好好阅读一番吧,或者关注我,回复"答案"二字,立马获取答案。

文章分类
Android
文章标签