热门标签 | 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

三、源码解析

待完成


推荐阅读
  • 深入解析零拷贝技术(Zerocopy)及其应用优势
    零拷贝技术(Zero-copy)是Netty框架中的一个关键特性,其核心在于减少数据在操作系统内核与用户空间之间的传输次数。通过避免不必要的内存复制操作,零拷贝显著提高了数据传输的效率和性能。本文将深入探讨零拷贝的工作原理及其在实际应用中的优势,包括降低CPU负载、减少内存带宽消耗以及提高系统吞吐量等方面。 ... [详细]
  • 如何使用 net.sf.extjwnl.data.Word 类及其代码示例详解 ... [详细]
  • 计算 n 叉树中各节点子树的叶节点数量分析 ... [详细]
  • 在进行网络编程时,准确获取本地主机的IP地址是一项基本但重要的任务。Winsock作为20世纪90年代初由Microsoft与多家公司共同制定的Windows平台网络编程接口,为开发者提供了一套高效且易用的工具。通过Winsock,开发者可以轻松实现网络通信功能,并准确获取本地主机的IP地址,从而确保应用程序在网络环境中的稳定运行。此外,了解Winsock的工作原理及其API函数的使用方法,有助于提高开发效率和代码质量。 ... [详细]
  • BZOJ4240 Gym 102082G:贪心算法与树状数组的综合应用
    BZOJ4240 Gym 102082G 题目 "有趣的家庭菜园" 结合了贪心算法和树状数组的应用,旨在解决在有限时间和内存限制下高效处理复杂数据结构的问题。通过巧妙地运用贪心策略和树状数组,该题目能够在 10 秒的时间限制和 256MB 的内存限制内,有效处理大量输入数据,实现高性能的解决方案。提交次数为 756 次,成功解决次数为 349 次,体现了该题目的挑战性和实际应用价值。 ... [详细]
  • Spring Boot 实战(一):基础的CRUD操作详解
    在《Spring Boot 实战(一)》中,详细介绍了基础的CRUD操作,涵盖创建、读取、更新和删除等核心功能,适合初学者快速掌握Spring Boot框架的应用开发技巧。 ... [详细]
  • 在《PHP应用性能优化实战指南:从理论到实践的全面解析》一文中,作者分享了一次实际的PHP应用优化经验。文章回顾了先前进行的一次优化项目,指出即使系统运行时间较长后出现的各种问题和性能瓶颈,通过采用一些通用的优化策略仍然能够有效解决。文中不仅详细阐述了优化的具体步骤和方法,还结合实例分析了优化前后的性能对比,为读者提供了宝贵的参考和借鉴。 ... [详细]
  • 本文作为“实现简易版Spring系列”的第五篇,继前文深入探讨了Spring框架的核心技术之一——控制反转(IoC)之后,将重点转向另一个关键技术——面向切面编程(AOP)。对于使用Spring框架进行开发的开发者来说,AOP是一个不可或缺的概念。了解AOP的背景及其基本原理,对于掌握这一技术至关重要。本文将通过具体示例,详细解析AOP的实现机制,帮助读者更好地理解和应用这一技术。 ... [详细]
  • 在Ubuntu系统中,由于预装了MySQL,因此无需额外安装。通过命令行登录MySQL时,可使用 `mysql -u root -p` 命令,并按提示输入密码。常见问题包括:1. 错误 1045 (28000):访问被拒绝,这通常是由于用户名或密码错误导致。为确保顺利连接,建议检查MySQL服务是否已启动,并确认用户名和密码的正确性。此外,还可以通过配置文件调整权限设置,以增强安全性。 ... [详细]
  • 本文详细介绍了如何在Linux系统中搭建51单片机的开发与编程环境,重点讲解了使用Makefile进行项目管理的方法。首先,文章指导读者安装SDCC(Small Device C Compiler),这是一个专为小型设备设计的C语言编译器,适合用于51单片机的开发。随后,通过具体的实例演示了如何配置Makefile文件,以实现代码的自动化编译与链接过程,从而提高开发效率。此外,还提供了常见问题的解决方案及优化建议,帮助开发者快速上手并解决实际开发中可能遇到的技术难题。 ... [详细]
  • 本文详细解析了 MySQL 5.7.20 版本中二进制日志(binlog)崩溃恢复机制的工作流程。假设使用 InnoDB 存储引擎,并且启用了 `sync_binlog=1` 配置,文章深入探讨了在系统崩溃后如何通过 binlog 进行数据恢复,确保数据的一致性和完整性。 ... [详细]
  • MySQL性能优化与调参指南【数据库管理】
    本文详细探讨了MySQL数据库的性能优化与参数调整技巧,旨在帮助数据库管理员和开发人员提升系统的运行效率。内容涵盖索引优化、查询优化、配置参数调整等方面,结合实际案例进行深入分析,提供实用的操作建议。此外,还介绍了常见的性能监控工具和方法,助力读者全面掌握MySQL性能优化的核心技能。 ... [详细]
  • JVM参数设置与命令行工具详解
    JVM参数配置与命令行工具的深入解析旨在优化系统性能,通过合理设置JVM参数,确保在高吞吐量的前提下,有效减少垃圾回收(GC)的频率,进而降低系统停顿时间,提升服务的稳定性和响应速度。此外,本文还将详细介绍常用的JVM命令行工具,帮助开发者更好地监控和调优JVM运行状态。 ... [详细]
  • 在IntelliJ IDEA中初始化Git并将项目推送到远程仓库的具体步骤包括:首先,登录Gitee(码云)账号并创建新的仓库;接着,在IDEA中通过VCS菜单选择Git进行本地项目的初始化;最后,配置远程仓库地址并执行推送操作,确保项目代码安全上传至云端。 ... [详细]
  • 根据 Laravel 官方文档,视图文件在首次加载时会被编译成普通的 PHP 代码并存储在缓存中。当视图文件发生更改时,系统会自动检测到这些变化并重新编译和缓存新的版本,以确保用户始终看到最新的内容。这一机制显著提高了应用的性能和响应速度。 ... [详细]
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社区 版权所有