RecyclerView详解

article/2025/11/10 5:14:21

RecyclerView 简称 RV, 是作为 ListView 和 GridView 的加强版出现的,目的是在有限的屏幕之上展示大量的内容,因此 RecyclerView 的复用机制的实现是它的一个核心部分。

RV 常规使用方式如下:

解释说明。

setLayoutManager:必选项,设置 RV 的布局管理器,决定 RV 的显示风格。常用的有线性布局管理器(LinearLayoutManager)、网格布局管理器(GridLayoutManager)、瀑布流布局管理器(StaggeredGridLayoutManager)。
setAdapter:必选项,设置 RV 的数据适配器。当数据发生改变时,以通知者的身份,通知 RV 数据改变进行列表刷新操作。
addItemDecoration:非必选项,设置 RV 中 Item 的装饰器,经常用来设置 Item 的分割线。
setItemAnimator:非必选项,设置 RV 中 Item 的动画。
本课时主要来看下 RV 是如何一步步将每一个 ItemView 显示到屏幕上,然后再分析在显示和滑动过程中,是如何通过缓存复用来提升整体性能的。RV 本质上也是一个自定义控件,所以也符合上节课所讲的自定义控件的规则。因此我们也可以沿着分析其 onMeasure -> onLayout -> onDraw 这 3 个方法的路线来深入研究。

绘制流程分析
onMeasure
RV 的 onMeasure 方法如下:

图中 1 处,表示在 XML 布局文件中,RV 的宽高被设置为 match_parent 或者具体值,那么直接将 skipMeasure 置为 true,并调用 mLayout(传入的 LayoutManager)的 onMeasure 方法测量自身的宽高即可。
图中 2 处,表示在 XML 布局文件中,RV 的宽高设置为 wrap_content,则会执行下面的 dispatchLayoutStep2(),其实就是测量 RecyclerView 的子 View 的大小,最终确定 RecyclerView 的实际宽高。
注意:
在上图代码中还有一个 dispatchLayoutStep1() 方法,这个方法并不是本节课重点介绍内容,但是它跟RV的动画息息相关,详细可以参考: RecyclerView.ItemAnimator实现动画效果

onLayout
RV 的 onLayout 方法如下:

很简单,只是调用了一层 dispatchLayout() 方法,此方法具体如下:

如果在 onMeasure 阶段没有执行 dispatchLayoutStep2() 方法去测量子 View,则会在 onLayout 阶段重新执行。

dispatchLayoutStep2() 源码如下:

可以看出,核心逻辑是调用了 mLayout 的 onLayoutChildren 方法。这个方法是 LayoutManager 中的一个空方法,主要作用是测量 RV 内的子 View 大小,并确定它们所在的位置。LinearLayoutManager、GridLayoutManager,以及 StaggeredLayoutManager 都分别复写了这个方法,并实现了不同方式的布局。

以 LinearLayoutManager 的实现为例,展开分析,实现如下 :

解释说明:

在 onLayoutChildren 中调用 fill 方法,完成子 View 的测量布局工作;
在 fill 方法中通过 while 循环判断是否还有剩余足够空间来绘制一个完整的子 View;
layoutChunk 方法中是子 View 测量布局的真正实现,每次执行完之后需要重新计算 remainingSpace。
layoutChunk 是一个非常核心的方法,这个方法执行一次就填充一个 ItemView 到 RV,部分代码如下:

说明:

图中 1 处从缓存(Recycler)中取出子 ItemView,然后调用 addView 或者 addDisappearingView 将子 ItemView 添加到 RV 中。
图中 2 处测量被添加的 RV 中的子 ItemView 的宽高。
图中 3 处根据所设置的 Decoration、Margins 等所有选项确定子 ItemView 的显示位置。
 

onDraw
测量和布局都完成之后,就剩下最后的绘制操作了。代码如下:

这个方法很简单,如果有添加 ItemDecoration,则循环调用所有的 Decoration 的 onDraw 方法,将其显示。至于所有的子 ItemView 则是通过 Android 渲染机制递归的调用子 ItemView 的 draw 方法显示到屏幕上。

小结:RV 会将测量 onMeasure 和布局 onLayout 的工作委托给 LayoutManager 来执行,不同的 LayoutManager 会有不同风格的布局显示,这是一种策略模式。用一张图来描述这段过程如下:

缓存复用原理 Recycler
缓存复用是 RV 中另一个非常重要的机制,这套机制主要实现了 ViewHolder 的缓存以及复用

核心代码是在 Recycler 中完成的,它是 RV 中的一个内部类,主要用来缓存屏幕内 ViewHolder 以及部分屏幕外 ViewHolder,部分代码如下:

Recycler 的缓存机制就是通过上图中的这些数据容器来实现的,实际上 Recycler 的缓存也是分级处理的,根据访问优先级从上到下可以分为 4 级,如下:

各级缓存功能
RV 之所以要将缓存分成这么多块,是为了在功能上进行一些区分,并分别对应不同的使用场景。

a 第一级缓存 mAttachedScrap&mChangedScrap

是两个名为 Scrap 的 ArrayList,这两者主要用来缓存屏幕内的 ViewHolder。为什么屏幕内的 ViewHolder 需要缓存呢?做过 App 开发的应该都熟悉下面的布局场景:

通过下拉刷新列表中的内容,当刷新被触发时,只需要在原有的 ViewHolder 基础上进行重新绑定新的数据 data 即可,而这些旧的 ViewHolder 就是被保存在 mAttachedScrap 和 mChangedScrap 中。实际上当我们调用 RV 的 notifyXXX 方法时,就会向这两个列表进行填充,将旧 ViewHolder 缓存起来。

b 第二级缓存 mCachedViews

它用来缓存移除屏幕之外的 ViewHolder,默认情况下缓存个数是 2,不过可以通过 setViewCacheSize 方法来改变缓存的容量大小。如果 mCachedViews 的容量已满,则会根据 FIFO 的规则将旧 ViewHolder 抛弃,然后添加新的 ViewHolder,如下所示:

通常情况下刚被移出屏幕的 ViewHolder 有可能接下来马上就会使用到,所以 RV 不会立即将其设置为无效 ViewHolder,而是会将它们保存到 cache 中,但又不能将所有移除屏幕的 ViewHolder 都视为有效 ViewHolder,所以它的默认容量只有 2 个。

c 第三级缓存 ViewCacheExtension
这是 RV 预留给开发人员的一个抽象类,在这个类中只有一个抽象方法,如下:

开发人员可以通过继承 ViewCacheExtension,并复写抽象方法 getViewForPositionAndType 来实现自己的缓存机制。只是一般情况下我们不会自己实现也不建议自己去添加缓存逻辑,因为这个类的使用门槛较高,需要开发人员对 RV 的源码非常熟悉。

d 第四级缓存 RecycledViewPool

RecycledViewPool 同样是用来缓存屏幕外的 ViewHolder,当 mCachedViews 中的个数已满(默认为 2),则从 mCachedViews 中淘汰出来的 ViewHolder 会先缓存到 RecycledViewPool 中。ViewHolder 在被缓存到 RecycledViewPool 时,会将内部的数据清理,因此从 RecycledViewPool 中取出来的 ViewHolder 需要重新调用 onBindViewHolder 绑定数据。这就同最早的 ListView 中的使用 ViewHolder 复用 convertView 的道理是一致的,因此 RV 也算是将 ListView 的优点完美的继承过来。

RecycledViewPool 还有一个重要功能,官方对其有如下解释:

RecycledViewPool lets you share Views between multiple RecyclerViews.

可以看出,多个 RV 之间可以共享一个 RecycledViewPool,这对于多 tab 界面的优化效果会很显著。需要注意的是,RecycledViewPool 是根据 type 来获取 ViewHolder,每个 type 默认最大缓存 5 个。因此多个 RecyclerView 共享 RecycledViewPool 时,必须确保共享的 RecyclerView 使用的 Adapter 是同一个,或 view type 是不会冲突的。

RV 是如何从缓存中获取 ViewHolder 的
在上文介绍 onLayout 阶段时,有介绍在 layoutChunk 方法中通过调用 layoutState.next 方法拿到某个子 ItemView,然后添加到 RV 中。

看一下 layoutState.next 的详细代码:

代码继续往下跟:

可以看出最终调用 tryGetViewHolderForPositionByDeadline 方法来查找相应位置上的ViewHolder,在这个方法中会从上面介绍的 4 级缓存中依次查找:

如图中红框处所示,如果在各级缓存中都没有找到相应的 ViewHolder,则会使用 Adapter 中的 createViewHolder 方法创建一个新的 ViewHolder。

何时将 ViewHolder 存入缓存
接下来看下 ViewHolder 被存入各级缓存的场景。

第一次 layout
当调用 setLayoutManager 和 setAdapter 之后,RV 会经历第一次 layout 并被显示到屏幕上,如下所示:

此时并不会有任何 ViewHolder 的缓存,所有的 ViewHolder 都是通过 createViewHolder 创建的。

刷新列表
如果通过手势下拉刷新,获取到新的数据 data 之后,我们会调用 notifyXXX 方法通知 RV 数据发生改变,这回 RV 会先将屏幕内的所有 ViewHolder 保存在 Scrap 中,如下所示:

当缓存执行完之后,后续通过 Recycler 就可以从缓存中获取相应 position 的 ViewHolder(姑且称为旧 ViewHolder),然后将刷新后的数据设置到这些 ViewHolder 上,如下所示:

最后再将新的 ViewHolder 绘制到 RV 上:

总结
这节课我带着你深入分析了 Android RecyclerView 源码中的 2 块核心实现:

RecyclerView 是如何经过测量、布局,最终绘制到屏幕上,其中大部分工作是通过委托给 LayoutManager 来实现的。
RecyclerView 的缓存复用机制,主要是通过内部类 Recycler 来实现。
谷歌 Android 团队对 RecyclerView 做了很多优化,导致 RecyclerView 最终的代码极其庞大。这也是为什么当 RecyclerView 出现问题的时候,排查问题的复杂度相对较高。理解 RecyclerView 的源码实现,有助于我们快速定位问题原因、拓展 RecyclerView 功能、提高分析 RecyclerView 性能问题的能力。 
 

关于面试题:

1.1 RecyclerView第一次layout时,会发生预布局pre-layout吗?

​ 第一次布局时,并不会触发pre-layout。pre-layout只会在每次notify change时才会被触发,目的是通过saveOldPosition方法将屏幕中各位置上的ViewHolder的坐标记录下来,并在重新布局之后,通过对比实现Item的动画效果。比如以下效果:

1.2 如果自定义LayoutManager需要注意什么?

在RecyclerView的dispatchLayoutStep1阶段,会调用自定义LayoutManager的 supportsPredictiveItemAnimations 方法判断在某些状态下是否展示predictive animation。以下LinearLayoutManager的实现:

@Override
public boolean supportsPredictiveItemAnimations() {
return mPendingSavedState == null && mLastStackFromEnd == mStackFromEnd;
}

​ 如果 supportsPredictiveItemAnimations 返回true,则LayoutManager中复写onLayoutChildren方法会被调用2次:一次是在pre-layout,另一次是real-layout。

因为会有pre-layout和real-layout,所有在自定义LayoutManager中,需要根据RecyclerView.State中的isPreLayout方法的返回值,在这两次布局中做区分。比如LinearLayoutManager中的onLayoutChildren中有如下判断:

上面代码中有一段注释:

if the child is visible and we are going to move it around, we should layout extra items in the opposite direction to make sure new items animate nicely instead of just fading in

​ 代表的意思就是如果当前正在update的item是可见状态,则需要在pre-layout阶段额外填充一个item,目的是为了保证处于不可见状态的item可以平滑的滑动到屏幕内。

1.3 举例说明

​ 比如下图中点击item2将其删除,调用notifyItemRemoved后,在pre-layout之前item5并没有被添加到RecyclerView中,而经过pre-layout之后,item5经过布局会被填充到RecyclerView中

当item移出屏幕之后,item5会随同item3和item4一起向上移动,如下图所示:

​ 如果自定义LayoutManager并没有实现pre-layout,或者实现不合理,则当item2移出屏幕时,只会将item3和item4进行平滑移动,而item5只是单纯的appear到屏幕中,如下所示:

可以看出item5并没有同item3和item4一起平滑滚动到屏幕内,这样界面上显示会给用户卡顿的感觉。

1.4 ViewHolder何时被缓存到RecycledViewPool中?

主要有以下2种情况:

  1. 当ItemView被滑动出屏幕时,并且CachedView已满,则ViewHolder会被缓存到RecycledViewPool中
  2. 当数据发生变动时,执行完disappearrance的ViewHolder会被缓存到RecycledViewPool中

1.5 CachedView和RecycledViewPool的关系

​ 当一个ItemView被滑动滚出屏幕之后,默认会先被保存在CachedView中。CachedView的默认大小为2,可以通过 setItemViewCacheSize 方法修改它的值。当CachedView已满后,后续有新的ItemView从屏幕内滑出时,会迫使CachedView根据FIFO规则,将之前的缓存的ViewHolder转移到RecycledViewPool中,效果可以参考下图:

RecycledViewPool默认大小为5,可以通过以下方式修改RecycledViewPool的缓存大小:

RecyclerView.getRecycledViewPool().setMaxRecycledViews(int viewType, int max);

1.6 CachedView和RecycledViewPool两者区别

缓存到CachedView中的ViewHolder并不会清理相关信息(比如position、state等),因此刚移出屏幕的ViewHolder,再次被移回屏幕时,只要从CachedView中查找并显示即可,不需要重新绑定(bindViewHolder)。

而缓存到RecycledViewPool中的ViewHolder会被清理状态和位置信息,因此从RecycledViewPool查找到ViewHolder,需要重新调用bindViewHolder绑定数据。

1.7 你是从哪些方面优化RecyclerView的?

我总结了几点,主要可以从以下几个方面对RecyclerView进行优化:

尽量将复杂的数据处理操作放到异步中完成。RecyclerView需要展示的数据经常是从远端服务器上请求获取,但是在网络请求拿到数据之后,需要将数据做扁平化操作,尽量将最优质的数据格式返回给UI线程。

优化RecyclerView的布局,避免将其与ConstraintLayout使用

针对快速滑动事件,可以使用addOnScrollListener添加对快速滑动的监听,当用户快速滑动时,停止加载数据操作。

如果ItemView的高度固定,可以使用setHasFixSize(true)。这样RecyclerView在onMeasure阶段可以直接计算出高度,不需要多次计算子ItemView的高度,这种情况对于垂直RecyclerView中嵌套横向RecyclerView效果非常显著。

当UI是Tab feed流时,可以考虑使用RecycledViewPool来实现多个RecyclerView的缓存共享。

转载:Android 工程师进阶 34 讲

          https://www.jianshu.com/p/eabb00c500ef


http://chatgpt.dhexx.cn/article/cBRe0LSs.shtml

相关文章

RecyclerView(三)—— RecyclerView的缓存机制

RecyclerView内存优越性,得益于它独特的缓存机制。 1 如何复用表项 如果列表中的每个表项在移出屏幕时被销毁,移入时又被重新创建,是很消耗资源,所以RecyclerView引入了缓存机制。缓存是为了复用,复用的好处是有可能…

RecyclerView详解一,使用及缓存机制

本文大致会先讲解RecyclerView的基础知识及使用,最后会深入讲解一点原理。当然,本人知识水平有限哈,太深入的东西我现在还没接触到,还请大家包容,阿里嘎多~ 一、RecyclerView的历史与发展 既然讲到了RV,那…

Android开发—RecyclerView使用

1.RecyclerView是什么 RecyclerView 在Android中用于创建列表。 官网的解释为: RecyclerView 可以让您轻松高效地显示大量数据。您提供数据并定义每个列表项的外观,而 RecyclerView 库会根据需要动态创建元素。 当RecyclerView的列表项滚出屏幕的时候&a…

Android RecyclerView使用简述

RecyclerView使用简述 前言正文一、创建项目二、RecyclerView基本使用① item布局和适配器② 显示数据③ 添加Item点击事件④ 添加Item子控件点击事件⑤ 添加长按事件⑥ 多个子控件点击事件 三、RecyclerView ViewBinding使用① 适配器② 显示数据③ 添加控件点击和长按 四、R…

es6新特性总结及使用说明

目录 简介 新特性说明 let语法 const语法 解构赋值 模板字符串 对象简写 对象操作--深拷贝 箭头函数 小结 简介 1. ECMAScript 6.0是 JavaScript 语言的下一代标准, 2015 年 6 月发布。 ES6 设计目标是达到 JavaScript 语言可以用来编写复杂的大型程序&a…

ES6有哪些新特性

ES6有哪些新特性(1)变量声明:由var变为let和const; (2)模板字符串:使用反引号;在模板字符串里面支持换行,并可以在里面使用${}来包裹一个变量或表达式; (3)解构:有数组解构和对象解构;可以快速获取数组和对象的值; (4) 展开运算符:在ES6中用…来表示展开运算符,它可以将数组…

ES6 新特性知识点总结

文章目录 ES6let及const解构赋值模板字符串Symbol类型Set和Map数据结构箭头函数类 ES6 ES 的全称是 ECMAScript , 它是由 ECMA 国际标准化组织,制定的一项脚本语言的标准化规范。 ES6 实际上是一个泛指,泛指 ES2015 及后续的版本。 每一次标准的诞生都意味着语言的…

ES6 新特性

1 、ES6 新特性 现在使用主流的前端框架中,如ReactJS、Vue.js、angularjs等,都会使用到ES6的新特性,作为一名高级工程师而言,ES6也就成为了必修课,所以本套课程先以ES6的新特性开始。 1.1、了解ES6 ES6,…

ES6新特性总结-面试必会

文章目录 一、let和const二、Symbol三、模板字符串3.1 什么是模板字符串3.2 字符串新方法 四、解构表达式4.1 数组解构4.2 对象解构 五、Set()、map()数据结构5.1 map()是什么及写法?5.1.1 map()是什么及写法?5.1.2 map()下的内置方法: 5.2 S…

JavaScript ES6新特性

JavaScript ES6带来了新的语法和特性,使得代码更加的现代和可读。它包括许多重要功能,如箭头函数、模板字符串、解构赋值等等。 const 和 let const 是 ES6 中用于声明变量的新关键字。const 比 var 强大。一旦使用,变量就不能重新分配。换…

ES6必须知道的六大新特性

ES6 ES6新特性-let&const 使用const表示常量(声明之后不允许改变,一旦声明必须初始化,否则会报错) //ES6 常量 不能修改const b2;b3;//Uncaught TypeError: Assignment to constant variable.console.log(b);使用var声明的…

ES6中有哪些新特性?

ES6中的新特性(一) ECMAScript6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。 我们来看看…

面试题!es6新特性

es6新特性 ECMAScript 6(ES6) 目前基本成为业界标准,它的普及速度比 ES5 要快很多,主要原因是现代浏览器对 ES6的支持相当迅速,尤其是 Chrome 和 Firefox 浏览器,已经支持 ES6 中绝大多数的特性。 以下是一些常用到的es6新特性&…

es6的8条新特性总结

es6的8条新特性总结 认识es61.块级作用域变量(let和const)2.箭头函数3.模板字符串4.解构赋值5.默认参数6. 扩展运算符7. 类和继承8.Promise 认识es6 ES6(ECMAScript 2015)是JavaScript的新版本,引入了许多新特性和语法…

最全的—— ES6有哪些新特性?

目录 ES6新特性1、let和const2、symbol3、模板字符串3.1 字符串新方法(补充) 4、解构表达式4.1 数组解构4.2 对象解构 5、对象方面5.1 Map和Set5.1.1 Map5.1.2 Set 5.3 数组的新方法5.3.1 Array.from()方法5.3.2 includes()方法5.3.3 map()、filter() 方…

GWAS分析中协变量的区分(性别?PCA?初生重?)

1. 电子书领取 前几天发了一篇GWAS电子书分享,异常火爆,阅读量8000,很多人评价比较基础。这本电子书主要特点是比较基础,GLM模型用软件和R语言进行比较,如何添加数字协变量、因子协变量、PCA等内容,可以说…

时间序列 工具库学习(5) Darts模块-多个时间序列、预训练模型和协变量的概念和使用

1.实验目的 此笔记本用作以下目的: 在多个时间序列上训练单个模型使用预训练模型获取训练期间未见的任何时间序列的预测使用协变量训练和使用模型 2.导库 # fix python path if working locally from utils import fix_pythonpath_if_working_locallyfix_python…

Mplus数据分析:随机截距交叉之后的做法和如何加协变量,写给粉丝

记得之前有写过如何用R做随机截距交叉滞后,有些粉丝完全是R小白,还是希望我用mplus做,今天就给大家写写如何用mplus做随机截距交叉滞后。 做之前我们需要知道一些Mplus的默认的设定: observed and latent exogenous variables a…

中介变量、调节变量与协变量

在平时看论文过程中偶会接触到这几个概念,然而都没想过弄明白,每次总觉得只要看明白个大概反正自己又不用这种方法…作为科研人,还是应该保持谦逊,保持学习 一、中介变量 1.概念 中介变量(mediator)是自…

协变量偏移/标签偏移/概念偏移

协变量偏移 这里我们假设,虽然输入的分布可能随时间而改变,但是标记函数,即条件分布P(y∣x)不会改变。虽然这个问题容易理解,但在实践中也容易忽视。 想想区分猫和狗的一个例子。我们的训练数据使用的是猫…