【Android Framework系列】第12章 RecycleView相关原理及四级缓存策略分析
1 RecyclerView简介
RecyclerView是一款非常强大的widget,它可以帮助您灵活地显示列表数据。当我开始学习 RecyclerView的时候,我发现对于复杂的列表界面有很多资源可以参考,但是对于简单的列表展现就鲜有可参考的资源了。虽然RecyclerView的组成结构乍一看有些复杂,但是深入理解以后您会发现它其实非常简单明了。
RecyclerView一般作为Android显示列表的控件,有诸多优异的性能。回收池策略能加载上亿级数据并不发生卡顿,适配器模式能展示任意显示需求。
RecyclerView就像传送带,充分利用传送带原理,永远只有用户看到的数据才会加载到内存,而看不到的在等待被加载。传送带能够源源不断的传送亿级货物,RecyclerView也能够显示加载亿级Item。
1.3 RecyclerView架构中核心组件
回收池:能回收任意Item控件,并返回符合类型的Item控件;
比如onBinderViewHodler方法中的第一个参数是从回收池中返回的适配器:Adapter接口,经常辅助RecyclerView实现列表展示;适配器模式,将用户界面展示与交互分离RecyclerView:是做触摸事件的交互,主要实现边界值判断;根据用户的触摸反馈,协调回收池对象与适配器对象之间的工作
我们带着这几个问题来学习RecyclerView:
Listview和Recycerview的缓存差别RecyclerView滑出去的View到哪里去了RecyclerView如何复用回收池的ViewRecyclerView的四级缓存机制
1.4 RecyclerView滑动相关
众所周知,RecyclerView在android中实现列表是性能非常好的,那么性能好的原因在哪里呢?关键还是在它在处理view时的回收和复用。列表在滑动的时候,会进行itemView的回收和复用,那么我们就从滑动回调即onTouchEvent来入手分析
1.4.1 基本概念
- ViewHolder: View的容器,一项View就对应一个ViewHolder
- Recyler:RecyclerView的内部类,主要负责View的回收和复用
- LinearLayoutManager: RecyclerView的线性布局管理器
1.4.2 滑动时函数调用链

这里我们大概了解下滑动时的函数调用链,帮助理解后面分析四级缓存相关的思路。
1.4.3 onMeasrue初始化
RecyclerView的宽度和高度开发者们都喜欢设置层wrap_content或者match_parent。所以需要通过实际内容确定RecyclerView高度
情况1 : 当item数不足的时候,比如RecyclerView只加载了2个Item 以子控件总高度测算的高度为准
情况2 : 当item数量超过实际屏幕高度,以match_parent为准,也就是最大高度
1.4.4 onLayout
RecyclerView作为一个容器类控件 继承自ViewGroup。必须实现onLayout方法来子控件进行正确摆放,由于我们手写的RecyclerVIew是垂直的,摆放是由上至下进行。同时为了不将所有Item全部加载到内存 也需要进行准确的控制
1.4.5 事件拦截
RecyclerView作为一个容器类控件 需要拦截滑动事件,用户手指滑动则让所有子Item滑动,子Item在滑动中是接收不到任何事件的。当RecyclerVIew静止时,子Item需要接收到点击事件
2 RecyclerView适配器与回收池的工作机制
这里我们先以图片的形式了解一下RecyclerView相关的加载逻辑。
2.1 RecyclerView中的第一屏加载

2.2 RecyclerView中的第二屏

2.3 回收池回收策略

2.4 回收池填充策略

2.5 回收池设计

3 RecyclerView回收与复用

3.1 回收的关键方法分析
RecyclerView.java
void recycleViewHolderInternal(ViewHolder holder) {...if (forceRecycle || holder.isRecyclable()) {if (mViewCacheMax > 0&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID| ViewHolder.FLAG_REMOVED| ViewHolder.FLAG_UPDATE| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) // 1) 先尝试放到cacheView中int cachedViewSize = mCachedViews.size();if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {// 如果 mCachedViews 已经满了,把第0个位置的移除并放到 缓存池中recycleCachedViewAt(0);cachedViewSize--;}if (!cached) {// 2) 如果CacheView中没放进去,就放到 缓存池中addViewHolderToRecycledViewPool(holder, true);recycled = true;}...
}
3.2 复用的关键方法分析
tryGetViewHolderForPositionByDeadline
从一级缓存 mChangeScrap 中取
从二级缓存 mCachedViews中取
从三级缓存 mViewCacheExtension 中取
从四级缓存 缓存池中取
缓存中都没有拿到值,就直接创建
未绑定过时,进行绑定
ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {ViewHolder holder = null;// 1) 从一级缓存 changed scrap 中取if (mState.isPreLayout()) {holder = getChangedScrapViewForPosition(position);fromScrapOrHiddenOrCache = holder != null;}// 2)从二级缓存 cache中取if (holder == null) {final int type = mAdapter.getItemViewType(offsetPosition);if (mAdapter.hasStableIds()) {holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun);if (holder != null) {// update positionholder.mPosition = offsetPosition;fromScrapOrHiddenOrCache = true;}}// 3. 从三级缓存 CacheExtension 中取 if (holder == null && mViewCacheExtension != null) {final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);if (view != null) {holder = getChildViewHolder(view);}}// 4) 从四级缓存 缓存池中取if (holder == null) { // fallback to poolholder = getRecycledViewPool().getRecycledView(type);if (holder != null) {holder.resetInternal();if (FORCE_INVALIDATE_DISPLAY_LIST) {invalidateDisplayListInt(holder);}}}// 5)缓存中都没有拿到值,就直接创建if (holder == null) {holder = mAdapter.createViewHolder(RecyclerView.this, type);if (ALLOW_THREAD_GAP_WORK) {// only bother finding nested RV if prefetchingRecyclerView innerView = findNestedRecyclerView(holder.itemView);if (innerView != null) {holder.mNestedRecyclerView = new WeakReference<>(innerView);}}long end = getNanoTime();mRecyclerPool.factorInCreateTime(type, end - start);if (DEBUG) {Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");}}}// 6)已经 bind过了,不会再去绑定,未绑定过时,进行绑定if (mState.isPreLayout() && holder.isBound()) {holder.mPreLayoutPosition = position;} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {final int offsetPosition = mAdapterHelper.findPositionOffset(position);// 尝试 bindViewbound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);}return holder;
}
4 四级缓存机制
4.1 一级缓存-缓存碎片
ViewHolder getChangedScrapViewForPosition(int position) {// If pre-layout, check the changed scrap for an exact match.final int changedScrapSize;if (mChangedScrap == null || (changedScrapSize = mChangedScrap.size()) == 0) {return null;}// find by positionfor (int i = 0; i < changedScrapSize; i++) {final ViewHolder holder = mChangedScrap.get(i);if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);return holder;}}// find by idif (mAdapter.hasStableIds()) {final int offsetPosition = mAdapterHelper.findPositionOffset(position);if (offsetPosition > 0 && offsetPosition < mAdapter.getItemCount()) {final long id = mAdapter.getItemId(offsetPosition);for (int i = 0; i < changedScrapSize; i++) {final ViewHolder holder = mChangedScrap.get(i);if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) {holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);return holder;}}}}return null;
}
4.2 二级缓存-缓存列表
ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {// 1) 先从mAttachedScrap中取,取到便返回final int count = mAttachedScrap.size();for (int i = count - 1; i >= 0; i--) {final ViewHolder holder = mAttachedScrap.get(i);if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {if (type == holder.getItemViewType()) {holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);if (holder.isRemoved()) {if (!mState.isPreLayout()) {holder.setFlags(ViewHolder.FLAG_UPDATE, ViewHolder.FLAG_UPDATE| ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED);}}return holder;} else if (!dryRun) {mAttachedScrap.remove(i);removeDetachedView(holder.itemView, false);quickRecycleScrapView(holder.itemView);}}}// 2)二级缓存,从mCachedViews中取final int cacheSize = mCachedViews.size();for (int i = cacheSize - 1; i >= 0; i--) {final ViewHolder holder = mCachedViews.get(i);//从mCachedViews中取到后便返回if (holder.getItemId() == id) {if (type == holder.getItemViewType()) {if (!dryRun) {mCachedViews.remove(i);}return holder;} else if (!dryRun) {recycleCachedViewAt(i);return null;}}}return null;}
4.3 三级缓存-自定义缓存
这一级缓存为用户自定义,这里不做详解。
4.4 四级缓存-缓存池
public ViewHolder getRecycledView(int viewType) {//从mScrap中根据view的类型来取出一个final ScrapData scrapData = mScrap.get(viewType);if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;//从 scrapData 中拿最后一个数据,先进后出return scrapHeap.remove(scrapHeap.size() - 1);}return null;
}static class ScrapData {//ViewHolder是作为一个List被存进来的ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();// 缓存池中 list的大小是5,也就是每个类型的view缓存池中存储5个int mMaxScrap = 5;long mCreateRunningAverageNs = 0;long mBindRunningAverageNs = 0;}
相关文章:
【Android Framework系列】第12章 RecycleView相关原理及四级缓存策略分析
1 RecyclerView简介 RecyclerView是一款非常强大的widget,它可以帮助您灵活地显示列表数据。当我开始学习 RecyclerView的时候,我发现对于复杂的列表界面有很多资源可以参考,但是对于简单的列表展现就鲜有可参考的资源了。虽然RecyclerView的…...
P1886 滑动窗口 /【模板】(双端队列)+双端队列用法
例题 有一个长为 n 的序列 a,以及一个大小为 k 的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。 例如: The array is [1,3,−1,−3,5,3,6,7],and k3。 输入格式 输入一共有两行…...
网络渗透day6-面试01
😉 和渗透测试相关的面试问题。 介绍 如果您想自学网络渗透,有许多在线平台和资源可以帮助您获得相关的知识和技能。以下是一些受欢迎的自学网络渗透的平台和资源: Hack The Box: Hack The Box(HTB)是一个受欢迎的平…...
Docker 及 Docker Compose 安装指南
Docker 是一个开源的容器化平台,可以帮助我们快速构建、打包和运行应用程序。而 Docker Compose 则是用于管理多个容器应用的工具,可以轻松定义和管理多个容器之间的关系。现在,让我们开始安装过程吧! docker 安装 apt安装 sudo…...
Gitlab创建一个空项目
1. 创建项目 Project slug是访问地址的后缀,跟前边的ProjectUrl拼在一起,就是此项目的首页地址; Visibility Level选择默认私有即可,选择内部或者公开,就会暴露代码。 勾选Readme选项,这样项目内默认会带…...
C语言-内存分布(STM32内存分析)
C/C内存分布 一、内存组成二、静态区域文本段 (Text / 只读区域 RO)已初始化读写数据段(RW data -- Initialized Data Segment)未初始化数据段(BSS -- Block Started by Symbol) 三、动态区域堆(…...
Linux上配置NAT
Linux系统上实现NAT上网是一个挑战性的任务,需要对操作系统进行合理的配置。本文将概述在Linux上实现NAT上网,并给出相应的工作步骤。 NAT,即Network Address Translation,是一种网络部署技术,可以在peivate network&…...
springboot实现简单的消息对话
目录 一、前言 二、实战步骤 步骤 1: 步骤 2: 步骤 3: 步骤 4: 一、前言 要在Spring Boot项目中实现消息对话,你可以使用WebSocket技术。WebSocket是一种在客户端和服务器之间提供实时双向通信的协议。 二、实…...
「Tech初见」Linux驱动之blkdev
目录 一、Motivation二、SolutionS1 - 块设备驱动框架(1)注册块设备(2)注销块设备(3)申请 gendisk(4)删除 gendisk(5)将 gendisk 加入 kernel(6&a…...
ssh配置(二、登录服务器)
一. 登录 linux 服务器的两种方式 使用 ssh用户名密码 的方式登录,但这种方式不安全,密码太简单容易被暴力破解,密码太复杂又不容易记。使用 ssh公私钥 的方式登录。 以上两种方式都可以在图形化软件工具中配置,例如 finalshell…...
pytorch异常——RuntimeError:Given groups=1, weight of size..., expected of...
文章目录 省流异常报错异常截图异常代码原因解释修正代码执行结果 省流 nn.Conv2d 需要的输入张量格式为 (batch_size, channels, height, width),但您的示例输入张量 x 是 (batch_size, height, width, channels)。因此,需要对输入张量进行转置。 注意…...
【FPGA项目】沙盘演练——基础版报文收发
第1个虚拟项目 前言 点灯开启了我们的FPGA之路,那么我们来继续沙盘演练。 用一个虚拟项目,来入门练习,以此步入数字逻辑的…...
【C++技能树】继承概念与解析
Halo,这里是Ppeua。平时主要更新C,数据结构算法,Linux与ROS…感兴趣就关注我bua! 继承 0. 继承概念0.1 继承访问限定符 1. 基类和派生类对象赋值兼容转换2. 继承中的作用域3. 派生类中的默认成员函数4.友元5.继承中的静态成员6.菱…...
计算机网络 第二节
目录 一,计算机网络的分类 1.按照覆盖范围分 2.按照所属用途分 二,计算机网络逻辑组成部分 1.核心部分 (通信子网) 1.1电路交换 1.2 分组交换 两种方式的特点 重点 2.边缘部分 (资源子网) 进程通信的方…...
无涯教程-机器学习 - 矩阵图函数
相关性是有关两个变量之间变化的指示,在前面的章节中,无涯教程讨论了Pearson的相关系数以及相关的重要性,可以绘制相关矩阵以显示哪个变量相对于另一个变量具有较高或较低的相关性。 在以下示例中,Python脚本将为Pima印度糖尿病数…...
Redis 高可用与集群
Redis 高可用与集群 虽然 Redis 可以实现单机的数据持久化,但无论是 RDB 也好或者 AOF 也好,都解决 不了单点宕机问题,即一旦单台 redis 服务器本身出现系统故障、硬件故障等问题后, 就会直接造成数据的丢失,因此需要…...
修改文件名后Git仓上面并没有修改
场景: 我在本地将文件夹名称由Group → group ,执行git push 后,远程分支上的文件名称并没有修改。 原因: 是我绕过了git 直接使用了系统的重命名操作。 在 Git 中,对于已经存在的文件或文件夹进行大小写重命名是一个敏感的操作…...
Linux 信号
目录 基本概念信号的分类可靠信号与不可靠信号实时信号与非实时信号 常见信号与默认行为进程对信号的处理signal()函数sigaction()函数 向进程发送信号kill()函数raise() alarm()和pause()函数alarm()函数pause()函数 信号集初始化信号集测试信号是否在信号集中 获取信号的描述…...
深入探讨梯度下降:优化机器学习的关键步骤(二)
文章目录 🍀引言🍀eta参数的调节🍀sklearn中的梯度下降 🍀引言 承接上篇,这篇主要有两个重点,一个是eta参数的调解;一个是在sklearn中实现梯度下降 在梯度下降算法中,学习率…...
高频算法面试题
合并两个有序数组 const merge (nums1, nums2) > {let p1 0;let p2 0;const result [];let cur;while (p1 < nums1.length || p2 < nums2.length) {if (p1 nums1.length) {cur nums2[p2];} else if (p2 nums2.length) {cur nums1[p1];} else if (nums1[p1] &…...
铭豹扩展坞 USB转网口 突然无法识别解决方法
当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…...
在软件开发中正确使用MySQL日期时间类型的深度解析
在日常软件开发场景中,时间信息的存储是底层且核心的需求。从金融交易的精确记账时间、用户操作的行为日志,到供应链系统的物流节点时间戳,时间数据的准确性直接决定业务逻辑的可靠性。MySQL作为主流关系型数据库,其日期时间类型的…...
【HTTP三个基础问题】
面试官您好!HTTP是超文本传输协议,是互联网上客户端和服务器之间传输超文本数据(比如文字、图片、音频、视频等)的核心协议,当前互联网应用最广泛的版本是HTTP1.1,它基于经典的C/S模型,也就是客…...
学校时钟系统,标准考场时钟系统,AI亮相2025高考,赛思时钟系统为教育公平筑起“精准防线”
2025年#高考 将在近日拉开帷幕,#AI 监考一度冲上热搜。当AI深度融入高考,#时间同步 不再是辅助功能,而是决定AI监考系统成败的“生命线”。 AI亮相2025高考,40种异常行为0.5秒精准识别 2025年高考即将拉开帷幕,江西、…...
Java 二维码
Java 二维码 **技术:**谷歌 ZXing 实现 首先添加依赖 <!-- 二维码依赖 --><dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.5.1</version></dependency><de…...
高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数
高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数 在软件开发中,单例模式(Singleton Pattern)是一种常见的设计模式,确保一个类仅有一个实例,并提供一个全局访问点。在多线程环境下,实现单例模式时需要注意线程安全问题,以防止多个线程同时创建实例,导致…...
腾讯云V3签名
想要接入腾讯云的Api,必然先按其文档计算出所要求的签名。 之前也调用过腾讯云的接口,但总是卡在签名这一步,最后放弃选择SDK,这次终于自己代码实现。 可能腾讯云翻新了接口文档,现在阅读起来,清晰了很多&…...
mac 安装homebrew (nvm 及git)
mac 安装nvm 及git 万恶之源 mac 安装这些东西离不开Xcode。及homebrew 一、先说安装git步骤 通用: 方法一:使用 Homebrew 安装 Git(推荐) 步骤如下:打开终端(Terminal.app) 1.安装 Homebrew…...
解析奥地利 XARION激光超声检测系统:无膜光学麦克风 + 无耦合剂的技术协同优势及多元应用
在工业制造领域,无损检测(NDT)的精度与效率直接影响产品质量与生产安全。奥地利 XARION开发的激光超声精密检测系统,以非接触式光学麦克风技术为核心,打破传统检测瓶颈,为半导体、航空航天、汽车制造等行业提供了高灵敏…...
Vue 模板语句的数据来源
🧩 Vue 模板语句的数据来源:全方位解析 Vue 模板(<template> 部分)中的表达式、指令绑定(如 v-bind, v-on)和插值({{ }})都在一个特定的作用域内求值。这个作用域由当前 组件…...
