Android RecyclerView缓存复用原理解析

一、牵出缓存

都有哪些缓存,作用是什么,为什么这么设计

1.缓存还在屏幕内的ViewHolder——Scrap缓存

Scrap是RecyclerView中最轻量的缓存,它不参与滑动时的回收复用,只是作为重新布局时的一种临时缓存,缓存(保存)动作只发生在重新布局时,布局完成后就要清空缓存。它的目的是,缓存当界面重新布局(不包括初始化第一次)的前后都出现在屏幕上的ViewHolder,这样就省去了不必要的CreateView和bindView的工作

mAttachedScrap

mAttachedScrap的数据结构:

final ArrayList mAttachedScrap = new ArrayList<>();

它是用来保存将会原封不动的ViewHolder,例如调用notifyItemChanged方法时,在布局前先把那些屏幕内没有改变的ViewHolder保存在mAttachedScrap中,在布局时复用mAttachedScrap中的ViewHolder,布局结束后清空mAttachedScrap,缓存的动作只发生在布局前,复用的动作只发生在布局时,布局后清空

mChangeScrap

mChangeScrap的数据结构:

ArrayList mChangedScrap

它是用来保存位置会发生移动的ViewHolder,注意只是位置发生移动,内容仍旧是原封不动,例如Remove掉一个Item,在布局前就能知道屏幕内哪些View是原封不动的,这些原封不动的保存在mAttachedScrap中,哪些View是只变换位置的,这些只变换位置的保存在mChangeScrap中,在布局时不变的复用mAttachedScrap中的,只有位置变化的复用mChangeScrap中的,缓存的动作只发生在布局前,复用的动作只发生在布局时,布局后清空

用一个例子说明

Android RecyclerView缓存复用原理解析_第1张图片

上图描述的是我们在一个RecyclerView中删除B项,并且调用了notifyItemRemoved()时,mAttachedScrap与mChangedScrap分别会临时存储的View情况。此时,A是在删除前后完全没有变化的,它会被临时放入mAttachedScrap。B是我们要删除的,它也会被放进mAttachedScrap,但是会被额外标记REMOVED,并且在之后会被移除。C和D在删除B后会向上移动位置,因此他们会被临时放入mChangedScrap中。E在此次操作前并没有出现在屏幕中,它不属于Scrap需要管辖的,Scrap只会缓存屏幕上已经加载出来的ViewHolder。在删除时,A,B,C,D都会进入Scrap,而在删除后,A,C,D都会回来,其中C,D只进行了位置上的移动,其内容没有发生变化。

RecyclerView的局部刷新,依赖的就是Scrap的临时缓存,我们需要通过notifyItemRemoved()、notifyItemChanged()等系列方法通知RecyclerView哪些位置发生了变化,这样RecyclerView就能在处理这些变化的时候,使用Scrap来缓存其它内容没有发生变化的ViewHolder,于是就完成了局部刷新。需要注意的是,如果我们使用notifyDataSetChanged()方法来通知RecyclerView进行更新,其会标记所有屏幕上的View为FLAG_INVALID,从而不会尝试使用Scrap来缓存一会儿还会回来的ViewHolder,而是统统直接扔进RecycledViewPool池子里,回来的时候就要重新走一遍绑定的过程。

Scrap只是作为布局时的临时缓存,它和滑动时的缓存没有任何关系,它的detach和重新attach只临时存在于布局的过程中。布局结束时Scrap列表应该是空的,其成员要么被重新布局出来,要么将被移除,总之在布局过程结束的时候,两个Scrap列表中都不应该再存在任何东西。

2.缓存屏幕之外的ViewHolder——CacheView

CacheView是在RecyclerView列表位置产生变动的时候,对刚刚移出屏幕的View进行回收复用的缓存列表,它的数据结构是:

final ArrayList mCachedViews = new ArrayList();

mCacheViews的缓存动作发生在滑动时,当有Item滑出屏幕外,就会原封不动的保存到mCacheViews中,复用动作发生在滑动回来的时候,场景是当上下小距离滑动时,刚划出去的Item又划回来,不用再重新创建和重新绑定数据

注意mCachedViews是有大小限制的,默认最大是2,当超过2时会怎样呢?

            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 view
                    int cachedViewSize = mCachedViews.size();
                    //mViewCacheMax的值是2
                    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                        recycleCachedViewAt(0);
                        cachedViewSize--;
                    }
                    int targetCacheIndex = cachedViewSize;
                    if (ALLOW_THREAD_GAP_WORK
                            && cachedViewSize > 0
                            && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                        // when adding the view, skip past most recently prefetched views
                        int cacheIndex = cachedViewSize - 1;
                        while (cacheIndex >= 0) {
                            int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                            if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                                break;
                            }
                            cacheIndex--;
                        }
                        targetCacheIndex = cacheIndex + 1;
                    }
                    mCachedViews.add(targetCacheIndex, holder);
                    cached = true;
                }
                if (!cached) {
                    addViewHolderToRecycledViewPool(holder, true);
                    recycled = true;
                }
            } 
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
    recycleCachedViewAt(0);
    cachedViewSize--;
}

当mCachedViews的长度大于等于2时,就会移除索引为0的ViewHolder,这第0个是最早缓存进来的,这体现了LRU缓存的特性,淘汰最近不常用的,然后因为是ArrayList所以索引是1的会替补到索引是0的位置,然后下面把新加进来的VIewHolder放到索引是1的位置。

3.mViewCacheExtension

这是Google工程师预留给程序员的,可以做自己的缓存逻辑。

4.RecycledViewPool

RecycledViewPool是最后一层缓存:

        RecycledViewPool getRecycledViewPool() {
            if (mRecyclerPool == null) {
                mRecyclerPool = new RecycledViewPool();
            }
            return mRecyclerPool;
        }
    public static class RecycledViewPool {
        static class ScrapData {
            final ArrayList mScrapHeap = new ArrayList<>();
            int mMaxScrap = DEFAULT_MAX_SCRAP;
            long mCreateRunningAverageNs = 0;
            long mBindRunningAverageNs = 0;
        }
        SparseArray mScrap = new SparseArray<>();
    }

我们可以在RecyclerView中找到RecycledViewPool,可以看见它的保存形式是和上述的Srap、CacheView都不同的,它的数据结构是一个SparseArray,它的Value的数据类型是ScrapData,ScrapData中主要维护了一个ViewHolder的ArrayList。

原因是RecycledViewPool保存的是以ViewHolder的viewType为区分(我们在重写RecyclerView的onCreateViewHolder()时可以发现这里有个viewType参数,可以借助它来实现展示不同类型的列表项)的多个列表。

与前两者不同,RecycledViewPool在进行回收的时候,目标只是回收一个该viewType的ViewHolder对象,并没有保存下原来ViewHolder的内容,在保存之前会进行ViewHolder的格式化清空数据内容,因为清空后的ViewHolder都是一样的,所以它只保存前五个,后面的直接丢掉,并没有使用LRU缓存逻辑,在复用时,将会调用bindViewHolder()按照我们在onBindViewHolder()描述的绑定步骤进行重新绑定,从而摇身一变变成了一个新的列表项展示出来。

同样,RecycledViewPool也有一个最大数量限制,默认情况下是5。在没有超过最大数量限制的情况下,Recycler会尽量把将被废弃的ViewHolder回收到RecycledViewPool中,以期能被复用。值得一提的是,RecycledViewPool只会按照ViewType进行区分,只要ViewType是相同的,甚至可以在多个RecyclerView中进行通用的复用,只要为它们设置同一个RecycledViewPool就可以了。

总的来看,RecyclerView着重在两个场景使用缓存与回收复用进行了性能上的优化。一是,在数据更新时,利用Scrap实现局部更新,尽可能地减少没有被更改的View进行无用地重新创建与绑定工作。二是,在快速滑动的时候,重复利用已经滑过的ViewHolder对象,以尽可能减少重新创建ViewHolder对象时带来的压力。总体的思路就是:只要没有改变,就直接重用;只要能不创建或重新绑定,就尽可能地偷懒。

二、到底是四级缓存还是三级缓存

Google工程师告诉我们有三层缓存,分别是:CacheView、mViewCacheExtension、RecycledViewPool

其实我们发现还有一个:Scrap

到此这篇关于Android RecyclerView缓存复用原理解析的文章就介绍到这了,更多相关Android RecyclerView 内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

你可能感兴趣的:(Android RecyclerView缓存复用原理解析)