热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

深入解析RecyclerView的缓存与视图复用机制

本文深入探讨了RecyclerView的缓存与视图复用机制,详细解析了不同类型的缓存及其功能。首先,介绍了屏幕内ViewHolder的Scrap缓存,这是一种最轻量级的缓存方式,旨在提高滚动性能并减少不必要的视图创建。通过分析其设计原理,揭示了Scrap缓存为何能有效提升用户体验。此外,还讨论了其他类型的缓存机制,如RecycledViewPool和ViewCacheExtension,进一步优化了视图复用效率。
一、都有哪些缓存,作用是什么,为什么这么设计

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

三、源码解析

待完成


推荐阅读
  • 本文详细介绍了优化DB2数据库性能的多种方法,涵盖统计信息更新、缓冲池调整、日志缓冲区配置、应用程序堆大小设置、排序堆参数调整、代理程序管理、锁机制优化、活动应用程序限制、页清除程序配置、I/O服务器数量设定以及编入组提交数调整等方面。通过这些技术手段,可以显著提升数据库的运行效率和响应速度。 ... [详细]
  • ElasticSearch 集群监控与优化
    本文详细介绍了如何有效地监控 ElasticSearch 集群,涵盖了关键性能指标、集群健康状况、统计信息以及内存和垃圾回收的监控方法。 ... [详细]
  • 在尝试使用C# Windows Forms客户端通过SignalR连接到ASP.NET服务器时,遇到了内部服务器错误(500)。本文将详细探讨问题的原因及解决方案。 ... [详细]
  • 并发编程 12—— 任务取消与关闭 之 shutdownNow 的局限性
    Java并发编程实践目录并发编程01——ThreadLocal并发编程02——ConcurrentHashMap并发编程03——阻塞队列和生产者-消费者模式并发编程04——闭锁Co ... [详细]
  • iOS 开发技巧:TabBarController 自定义与本地通知设置
    本文介绍了如何在 iOS 中自定义 TabBarController 的背景颜色和选中项的颜色,以及如何使用本地通知设置应用程序图标上的提醒个数。通过这些技巧,可以提升应用的用户体验。 ... [详细]
  • 丽江客栈选择问题
    本文介绍了一道经典的算法题,题目涉及在丽江河边的n家特色客栈中选择住宿方案。两位游客希望住在色调相同的两家客栈,并在晚上选择一家最低消费不超过p元的咖啡店小聚。我们将详细探讨如何计算满足条件的住宿方案总数。 ... [详细]
  • 深入解析Java虚拟机(JVM)架构与原理
    本文旨在为读者提供对Java虚拟机(JVM)的全面理解,涵盖其主要组成部分、工作原理及其在不同平台上的实现。通过详细探讨JVM的结构和内部机制,帮助开发者更好地掌握Java编程的核心技术。 ... [详细]
  • 优化Flask应用的并发处理:解决Mysql连接过多问题
    本文探讨了在Flask应用中通过优化后端架构来应对高并发请求,特别是针对Mysql 'too many connections' 错误的解决方案。我们将介绍如何利用Redis缓存、Gunicorn多进程和Celery异步任务队列来提升系统的性能和稳定性。 ... [详细]
  • ListView简单使用
    先上效果:主要实现了Listview的绑定和点击事件。项目资源结构如下:先创建一个动物类,用来装载数据:Animal类如下:packagecom.example.simplelis ... [详细]
  • 深入剖析JVM垃圾回收机制
    本文详细探讨了Java虚拟机(JVM)中的垃圾回收机制,包括其意义、对象判定方法、引用类型、常见垃圾收集算法以及各种垃圾收集器的特点和工作原理。通过理解这些内容,开发人员可以更好地优化内存管理和程序性能。 ... [详细]
  • Django 使用slug field时遇到的问题 ... [详细]
  • CentOS 7.6环境下Prometheus与Grafana的集成部署指南
    本文旨在提供一套详细的步骤,指导读者如何在CentOS 7.6操作系统上成功安装和配置Prometheus 2.17.1及Grafana 6.7.2-1,实现高效的数据监控与可视化。 ... [详细]
  • 本文探讨了如何通过一系列技术手段提升Spring Boot项目的并发处理能力,解决生产环境中因慢请求导致的系统性能下降问题。 ... [详细]
  • 本文档汇总了Python编程的基础与高级面试题目,涵盖语言特性、数据结构、算法以及Web开发等多个方面,旨在帮助开发者全面掌握Python核心知识。 ... [详细]
  • Google排名优化-面向Google(Search Engine Friendly)的URL设计 ... [详细]
author-avatar
don't
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有