Android RecyclerView缓存复用原理解析

作者:AllenC6 时间:2022-04-29 10:51:03 

一、牵出缓存

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

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

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

mAttachedScrap

mAttachedScrap的数据结构:

final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();

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

mChangeScrap

mChangeScrap的数据结构:

ArrayList<ViewHolder> mChangedScrap

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

用一个例子说明

Android RecyclerView缓存复用原理解析

上图描述的是我们在一个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&mdash;&mdash;CacheView

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

final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

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<ViewHolder> mScrapHeap = new ArrayList<>();
           int mMaxScrap = DEFAULT_MAX_SCRAP;
           long mCreateRunningAverageNs = 0;
           long mBindRunningAverageNs = 0;
       }
       SparseArray<ScrapData> 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

来源:https://blog.csdn.net/m0_37707561/article/details/126608516

标签:Android,RecyclerView,缓存复用
0
投稿

猜你喜欢

  • JavaFX Metro UI 和 开发库使用简介

    2021-12-11 19:01:39
  • 关于线程池你不得不知道的一些设置

    2021-06-08 11:34:45
  • 小米Java程序员第二轮面试10个问题 你是否会被刷掉?

    2023-11-13 09:00:29
  • Java经典面试题汇总:Mybatis

    2021-09-20 07:42:44
  • Java基于Socket实现HTTP下载客户端

    2022-07-06 01:02:30
  • 使用 Spring Boot 2.0 + WebFlux 实现 RESTful API功能

    2023-12-22 19:51:03
  • Java 8 中 Map 骚操作之 merge() 的使用方法

    2022-03-31 01:19:27
  • Android实现屏幕旋转方法总结

    2023-04-01 19:37:31
  • Java线程同步Lock同步锁代码示例

    2023-11-17 13:23:54
  • Android开发之TabHost选项卡及相关疑难解决方法

    2022-01-10 06:30:57
  • Android 7.0行为变更 FileUriExposedException解决方法

    2023-07-28 01:38:29
  • 快速解决commons-fileupload组件无法处理自定义head信息的bug

    2023-03-05 15:20:14
  • Unity Shader实现描边OutLine效果

    2022-01-13 03:11:13
  • Java实现单向链表反转

    2023-11-18 01:03:11
  • 详解Spring框架下向异步线程传递HttpServletRequest参数的坑

    2021-05-31 04:45:21
  • SpringBoot JS-SDK自定义微信分享的实现

    2023-11-10 05:08:28
  • C#实现无损压缩图片代码示例

    2022-01-23 19:59:25
  • C# 格式化JSON的两种实现方式

    2023-03-14 05:31:16
  • GSON实现Java对象的JSON序列化与反序列化的实例教程

    2023-11-23 12:39:02
  • C#强制转换和尝试转换的方法

    2021-10-10 10:48:23
  • asp之家 软件编程 m.aspxhome.com