1. 引言
网上有很多关于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. 结束
本文没有涉及到代码的讲解。一来网上的资料太多了,而且讲解的比较全,二来本文侧重点在于讲解各级缓存的作用和区别。关于开头的灵魂几问,如果您还不确定,赶紧翻开源码好好阅读一番吧。