• RecycleView缓存复用详细解析


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

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

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

    (1).mAttachedScrap

    mAttachedScrap的数据结构:

    final ArrayList mAttachedScrap = new ArrayList<>();

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

    (2).mChangeScrap

     mChangeScrap的数据结构:

    ArrayList mChangedScrap

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

     (3).用一个例子说明

    上图描述的是我们在一个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时会怎样呢?

    1. if (forceRecycle || holder.isRecyclable()) {
    2. if (mViewCacheMax > 0
    3. && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
    4. | ViewHolder.FLAG_REMOVED
    5. | ViewHolder.FLAG_UPDATE
    6. | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
    7. // Retire oldest cached view
    8. int cachedViewSize = mCachedViews.size();
    9. //🌟mViewCacheMax的值是2
    10. if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
    11. recycleCachedViewAt(0);
    12. cachedViewSize--;
    13. }
    14. int targetCacheIndex = cachedViewSize;
    15. if (ALLOW_THREAD_GAP_WORK
    16. && cachedViewSize > 0
    17. && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
    18. // when adding the view, skip past most recently prefetched views
    19. int cacheIndex = cachedViewSize - 1;
    20. while (cacheIndex >= 0) {
    21. int cachedPos = mCachedViews.get(cacheIndex).mPosition;
    22. if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
    23. break;
    24. }
    25. cacheIndex--;
    26. }
    27. targetCacheIndex = cacheIndex + 1;
    28. }
    29. mCachedViews.add(targetCacheIndex, holder);
    30. cached = true;
    31. }
    32. if (!cached) {
    33. addViewHolderToRecycledViewPool(holder, true);
    34. recycled = true;
    35. }
    36. }
    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是最后一层缓存:

    1. RecycledViewPool getRecycledViewPool() {
    2. if (mRecyclerPool == null) {
    3. mRecyclerPool = new RecycledViewPool();
    4. }
    5. return mRecyclerPool;
    6. }
    7. public static class RecycledViewPool {
    8. static class ScrapData {
    9. final ArrayList mScrapHeap = new ArrayList<>();
    10. int mMaxScrap = DEFAULT_MAX_SCRAP;
    11. long mCreateRunningAverageNs = 0;
    12. long mBindRunningAverageNs = 0;
    13. }
    14. SparseArray mScrap = new SparseArray<>();
    15. }

     我们可以在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

    三、源码解析

    待完成

  • 相关阅读:
    2023.10.8 基本 Thread 线程详解
    Apache Kafka - 高性能原因探究
    VMWare:使用命令更新或升级 VMWare ESXi 主机
    Kotlin中Any、Nothing、Unit 类型的概念和用法
    K8S之应用配置管理笔记
    阿里菜鸟面经 Java后端开发 社招三年
    19.Python函数(四)【Python常用内置函数】
    Linux usb Host 详解
    【C++】多态/虚表
    动力环境监控是什么?动力环境监控系统包括哪些设备
  • 原文地址:https://blog.csdn.net/m0_37707561/article/details/126608516