一、都有哪些缓存,作用是什么,为什么这么设计
1.缓存还在屏幕内的ViewHolder——Scrap缓存
Scrap是RecyclerView中最轻量的缓存,它不参与滑动时的回收复用,只是作为重新布局时的一种临时缓存,缓存(保存)动作只发生在重新布局时,布局完成后就要清空缓存。它的目的是,缓存当界面重新布局(不包括初始化第一次)的前后都出现在屏幕上的ViewHolder,这样就省去了不必要的CreateView和bindView的工作
(1).mAttachedScrap
mAttachedScrap的数据结构:
final ArrayList mAttachedScrap &#61; new ArrayList<>();
它是用来保存将会原封不动的ViewHolder&#xff0c;例如调用notifyItemChanged方法时&#xff0c;在布局前先把那些屏幕内没有改变的ViewHolder保存在mAttachedScrap中&#xff0c;在布局时复用mAttachedScrap中的ViewHolder&#xff0c;布局结束后清空mAttachedScrap&#xff0c;缓存的动作只发生在布局前&#xff0c;复用的动作只发生在布局时&#xff0c;布局后清空
(2).mChangeScrap
mChangeScrap的数据结构&#xff1a;
ArrayList mChangedScrap
它是用来保存位置会发生移动的ViewHolder&#xff0c;注意只是位置发生移动&#xff0c;内容仍旧是原封不动&#xff0c;例如Remove掉一个Item&#xff0c;在布局前就能知道屏幕内哪些View是原封不动的&#xff0c;这些原封不动的保存在mAttachedScrap中&#xff0c;哪些View是只变换位置的&#xff0c;这些只变换位置的保存在mChangeScrap中&#xff0c;在布局时不变的复用mAttachedScrap中的&#xff0c;只有位置变化的复用mChangeScrap中的&#xff0c;缓存的动作只发生在布局前&#xff0c;复用的动作只发生在布局时&#xff0c;布局后清空
(3).用一个例子说明
上图描述的是我们在一个RecyclerView中删除B项&#xff0c;并且调用了notifyItemRemoved()时&#xff0c;mAttachedScrap与mChangedScrap分别会临时存储的View情况。此时&#xff0c;A是在删除前后完全没有变化的&#xff0c;它会被临时放入mAttachedScrap。B是我们要删除的&#xff0c;它也会被放进mAttachedScrap&#xff0c;但是会被额外标记REMOVED&#xff0c;并且在之后会被移除。C和D在删除B后会向上移动位置&#xff0c;因此他们会被临时放入mChangedScrap中。E在此次操作前并没有出现在屏幕中&#xff0c;它不属于Scrap需要管辖的&#xff0c;Scrap只会缓存屏幕上已经加载出来的ViewHolder。在删除时&#xff0c;A,B,C,D都会进入Scrap&#xff0c;而在删除后&#xff0c;A,C,D都会回来&#xff0c;其中C,D只进行了位置上的移动&#xff0c;其内容没有发生变化。
RecyclerView的局部刷新&#xff0c;依赖的就是Scrap的临时缓存&#xff0c;我们需要通过notifyItemRemoved()、notifyItemChanged()等系列方法通知RecyclerView哪些位置发生了变化&#xff0c;这样RecyclerView就能在处理这些变化的时候&#xff0c;使用Scrap来缓存其它内容没有发生变化的ViewHolder&#xff0c;于是就完成了局部刷新。需要注意的是&#xff0c;如果我们使用notifyDataSetChanged()方法来通知RecyclerView进行更新&#xff0c;其会标记所有屏幕上的View为FLAG_INVALID&#xff0c;从而不会尝试使用Scrap来缓存一会儿还会回来的ViewHolder&#xff0c;而是统统直接扔进RecycledViewPool池子里&#xff0c;回来的时候就要重新走一遍绑定的过程。
Scrap只是作为布局时的临时缓存&#xff0c;它和滑动时的缓存没有任何关系&#xff0c;它的detach和重新attach只临时存在于布局的过程中。布局结束时Scrap列表应该是空的&#xff0c;其成员要么被重新布局出来&#xff0c;要么将被移除&#xff0c;总之在布局过程结束的时候&#xff0c;两个Scrap列表中都不应该再存在任何东西。
2.缓存屏幕之外的ViewHolder——CacheView
CacheView是在RecyclerView列表位置产生变动的时候&#xff0c;对刚刚移出屏幕的View进行回收复用的缓存列表&#xff0c;它的数据结构是&#xff1a;
final ArrayList mCachedViews &#61; new ArrayList();
mCacheViews的缓存动作发生在滑动时&#xff0c;当有Item滑出屏幕外&#xff0c;就会原封不动的保存到mCacheViews中&#xff0c;复用动作发生在滑动回来的时候&#xff0c;场景是当上下小距离滑动时&#xff0c;刚划出去的Item又划回来&#xff0c;不用再重新创建和重新绑定数据
注意mCachedViews是有大小限制的&#xff0c;默认最大是2&#xff0c;当超过2时会怎样呢&#xff1f;
if (forceRecycle || holder.isRecyclable()) {if (mViewCacheMax > 0&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID| ViewHolder.FLAG_REMOVED| ViewHolder.FLAG_UPDATE| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {// Retire oldest cached viewint cachedViewSize &#61; mCachedViews.size();//&#x1f31f;mViewCacheMax的值是2if (cachedViewSize >&#61; mViewCacheMax && cachedViewSize > 0) {recycleCachedViewAt(0);cachedViewSize--;}int targetCacheIndex &#61; cachedViewSize;if (ALLOW_THREAD_GAP_WORK&& cachedViewSize > 0&& !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {// when adding the view, skip past most recently prefetched viewsint cacheIndex &#61; cachedViewSize - 1;while (cacheIndex >&#61; 0) {int cachedPos &#61; mCachedViews.get(cacheIndex).mPosition;if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {break;}cacheIndex--;}targetCacheIndex &#61; cacheIndex &#43; 1;}mCachedViews.add(targetCacheIndex, holder);cached &#61; true;}if (!cached) {addViewHolderToRecycledViewPool(holder, true);recycled &#61; true;}}
if (cachedViewSize >&#61; mViewCacheMax && cachedViewSize > 0) {recycleCachedViewAt(0);cachedViewSize--;
}
当mCachedViews的长度大于等于2时&#xff0c;就会移除索引为0的ViewHolder&#xff0c;这第0个是最早缓存进来的&#xff0c;这体现了LRU缓存的特性&#xff0c;淘汰最近不常用的&#xff0c;然后因为是ArrayList所以索引是1的会替补到索引是0的位置&#xff0c;然后下面把新加进来的VIewHolder放到索引是1的位置。
3.mViewCacheExtension
这是Google工程师预留给程序员的&#xff0c;可以做自己的缓存逻辑。
4.RecycledViewPool
RecycledViewPool是最后一层缓存:
RecycledViewPool getRecycledViewPool() {if (mRecyclerPool &#61;&#61; null) {mRecyclerPool &#61; new RecycledViewPool();}return mRecyclerPool;}public static class RecycledViewPool {static class ScrapData {final ArrayList mScrapHeap &#61; new ArrayList<>();int mMaxScrap &#61; DEFAULT_MAX_SCRAP;long mCreateRunningAverageNs &#61; 0;long mBindRunningAverageNs &#61; 0;}SparseArray mScrap &#61; new SparseArray<>();}
我们可以在RecyclerView中找到RecycledViewPool&#xff0c;可以看见它的保存形式是和上述的Srap、CacheView都不同的&#xff0c;它的数据结构是一个SparseArray&#xff0c;它的Value的数据类型是ScrapData&#xff0c;ScrapData中主要维护了一个ViewHolder的ArrayList。
原因是RecycledViewPool保存的是以ViewHolder的viewType为区分&#xff08;我们在重写RecyclerView的onCreateViewHolder()时可以发现这里有个viewType参数&#xff0c;可以借助它来实现展示不同类型的列表项&#xff09;的多个列表。
与前两者不同&#xff0c;RecycledViewPool在进行回收的时候&#xff0c;目标只是回收一个该viewType的ViewHolder对象&#xff0c;并没有保存下原来ViewHolder的内容&#xff0c;在保存之前会进行ViewHolder的格式化清空数据内容&#xff0c;因为清空后的ViewHolder都是一样的&#xff0c;所以它只保存前五个&#xff0c;后面的直接丢掉&#xff0c;并没有使用LRU缓存逻辑&#xff0c;在复用时&#xff0c;将会调用bindViewHolder() 按照我们在onBindViewHolder()描述的绑定步骤进行重新绑定&#xff0c;从而摇身一变变成了一个新的列表项展示出来。
同样&#xff0c;RecycledViewPool也有一个最大数量限制&#xff0c;默认情况下是5。在没有超过最大数量限制的情况下&#xff0c;Recycler会尽量把将被废弃的ViewHolder回收到RecycledViewPool中&#xff0c;以期能被复用。值得一提的是&#xff0c;RecycledViewPool只会按照ViewType进行区分&#xff0c;只要ViewType是相同的&#xff0c;甚至可以在多个RecyclerView中进行通用的复用&#xff0c;只要为它们设置同一个RecycledViewPool就可以了。
总的来看&#xff0c;RecyclerView着重在两个场景使用缓存与回收复用进行了性能上的优化。一是&#xff0c;在数据更新时&#xff0c;利用Scrap实现局部更新&#xff0c;尽可能地减少没有被更改的View进行无用地重新创建与绑定工作。二是&#xff0c;在快速滑动的时候&#xff0c;重复利用已经滑过的ViewHolder对象&#xff0c;以尽可能减少重新创建ViewHolder对象时带来的压力。总体的思路就是&#xff1a;只要没有改变&#xff0c;就直接重用&#xff1b;只要能不创建或重新绑定&#xff0c;就尽可能地偷懒。
二、到底是四级缓存还是三级缓存
Google工程师告诉我们有三层缓存&#xff0c;分别是&#xff1a;CacheView、mViewCacheExtension、RecycledViewPool
其实我们发现还有一个&#xff1a;Scrap
三、源码解析
待完成