尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

RecyclerView三件事:数据、视图、位置,架构拆解

发布时间:2026/9/29 15:22:03

资讯中心
01
ARTICLE

RecyclerView三件事:数据、视图、位置,架构拆解

RecyclerView三件事:数据、视图、位置,架构拆解
架构拆解之后RecyclerView其实就三件事数据、视图、位置很多人刚接触Android开发时一遇到RecyclerView就觉得头大。写适配器、搞ViewHolder、处理滑动还要应付数据刷新。看起来东西很多但如果你把它的结构独立出来理解会发现它本质上就是一套非常清晰的生产消费模型——你负责告诉它数据长什么样它负责处理数据怎么摆放和数据怎么回收。这篇文章我不打算堆砌API而是从结构设计者的角度把这套框架最核心的三个角色捋一遍顺便把实际开发中那些让人迷惑为什么要这样写的点全部解开。不论你是刚入门的新手还是已经写了几年RecyclerView的老手建议都先跳出代码把整个框架在脑子里重新建模一遍。你会发现在这套看似复杂的层级之下结构逻辑比大多数组件都要简单简单到可以用一句话概括Adapter管产出LayoutManager管摆放ViewHolder管回收复用。1. 框架的核心主线Adapter、LayoutManager、ViewHolder各自到底在演什么戏1.1 Adapter不是容器它是流水线上的生产工位很多人在写Adapter时会习惯性地把它当成一个数据集合的展示层脑海里默认它就是给列表填充内容的工具于是不知不觉把大量逻辑塞进去导致Adapter越来越臃肿越来越难维护。这是个经典的误用场景。从结构上看RecyclerView本身是不管数据内容的。它只负责两件事当需要显示某个位置时向Adapter要对应的视图当看到某个视图滚出屏幕时安排视图进入回收池。也就是说屏幕能显示什么、左边右边是否有边距、每个Item长什么样全部由Adapter这个生产工位来决定。Adater的核心产出点其实是它暴露的四个基础接口getItemCount()告诉系统总共有多少条数据onCreateViewHolder()决定了我需要新生产一个空壳子的时候造出哪种类型的视图容器onBindViewHolder()负责把真实数据填进这个空壳子里。这三个方法串起来就是一条完整的流水线盘点库存、制造容器、装载货物。如果你在写程序时能时刻明确我这个Adapter只是负责把数据变成视图不负责业务逻辑、不负责网络请求、不负责状态保存代码的清晰度瞬间就会提升一个档次。事实上这也正是RecyclerView把Adapter独立成接口而不是写进RecyclerView内部的原因——结构设计时需要把数据生产与UI渲染彻底解耦方便任何数据类型接进来。1.2 LayoutManager决定拼图规则Linear、Grid还是Staggered另一类一直被忽视却在结构里扮演空间大戏的主角是LayoutManager。很多人对LayoutManager的印象只是我初始化RecyclerView要new一个LinearLayoutManager然后它就消失了。但真正驱动Item移动、换行、偏移的是LayoutManager。它的职责本质上就一句话计算出屏幕内每一个可见Item应该放在哪个坐标上。以LinearLayoutManager为例它其实就是用一个垂直或水平方向滚动栈的逻辑排列每个Item的起始偏移量GridLayoutManager则是在Linear基础上多存了一个每行放几个的列数参数StaggeredGridLayoutManager更进一步允许每列Item高度不一致形成瀑布流错落效果。有意思的地方在于LayoutManager决定位置时并不直接操作视图而是通过RecyclerView内部的一套测量布局流程把每个Item的LayoutParams算好。这就引出结构设计的另一个关键点RecyclerView脚手架只提供位置计算能力真正摆放完Item、让用户看得见摸得着的是ItemView自身的measure/layout过程而这一过程同样会被LayoutManager统一触达。所以当你想修改Item的间距时第一时间想到的不该是给外层布局加padding而应该去考虑ItemDecoration或LayoutParams设置。理解了这条结构链你会在处理各种列表布局时少走非常多的弯路。1.3 ViewHolder被误读的本质作用存储视图引用更是复用凭证初学者常见的一个困惑是ViewHolder到底是什么为什么必须持有itemView有人把ViewHolder单纯理解为为了findViewById缓存。这个理解只对了一半。在RecyclerView的结构中ViewHolder是承载一个ItemView完整生命周期状态的载体。RecyclerView回收的并不是一个View而是一个ViewHolder。这个ViewHolder里面存着整个Item的引用、data binding相关的绑定状态、itemId和type信息。回收是把ViewHolder连同它所包含的视图整体丢进缓存池下次需要时直接从池里捞出来重新调用onBindViewHolder把新数据填充进去即可。把ViewHolder设计出来最核心的意义在于让视图模板被重复利用。竖向千条数据的列表屏幕内实际只有十几个可见Item但滑动过程中如果频繁创建新View性能就会急剧恶化。通过ViewHolder复用系统能在一次创建后后续滑动几乎完全避免重新new视图只做数据绑定。结构上是简单的池化管理效果上是立竿见影的流畅。2. 缓存的复用机制为什么滚动几万条数据依然稳如老狗2.1 RecyclerView内置缓存层级由浅入深很多Android开发者知道复用这个词语但真正能说明白复用发生在哪个层级的并不多。RecyclerView内部有三级缓存顺序依次是mAttachedScrap、mCachedViews、RecycledViewPool。mAttachedScrap这层缓存很特俗它是给正在显示的Item临时准备的。当数据更新、调用notifyItemChanged等操作时RecyclerView需要重新布局但不想让屏幕上的这对Item全部销毁重建于是把它们统一挪到scrap池中暂存区分哪些Item位置没变、哪些变了再做局部刷新。这层数据大多还连着父容器操作非常快速。mCachedViews当Item滚出屏幕时它不会被立即销毁而是先进这层缓存。系统设定默认容量2对于默认情况下LinearLayoutManager每个方向通常2个。如果再次需要这个Item直接取自这层数据甚至可以被复用连onBindViewHolder都不会调用速度飞快——这也是为什么快速向上滑动时位于缓存区的行会极其跟手。RecycledViewPool当mCachedViews容量满了再滚出去的老Item会被小心地送进共享池中。这个池默认每个viewType最多保留5个ViewHolder且它是与Adapter绑定的支持在多个RecyclerView之间共享。池里的ViewHolder被取出后还必须重新执行onBindViewHolder绑定数据。理解这三级缓存你在实际排查滑动卡顿问题时就能从是不是缓存命中太低这个维度切入比盲目改XML布局属性要有用得多。2.2 为什么说ViewHolder复用是幂等的写onBindViewHolder时很多人有个坏习惯内容字段先清空再赋值。其实如果理解了ViewHolder的复用机制你会发现这不是必需的操作——因为每次进入onBindViewHolder都意味着前台需要展示一组全新的数据赋值即可把上一轮残留覆盖掉。真正会影响性能的是在onBindViewHolder中做耗时操作比如磁盘读取、创建一个全新的对象作为临时变量。这些操作每次绑定都要执行一旦Item复用率高它们的执行次数会成倍放大。我实际观察过不少卡顿性能问题定位到最后居然是开发者在一个列表Item里用SimpleDateFormat格式化时间字段导致主线程GC频繁。凡是在绑定时期高频率创建的东西都属于结构性隐患建议改成成员变量或静态缓存。2.3 设置setHasFixedSize的真正含义setHasFixedSize(true)这个API很常见但很多人不清楚为什么要写。它的作用是告诉RecyclerView我的Item尺寸变化不会影响到整个RecyclerView的宽高。如果自从知道宽高固定不变RecyclerView就能跳过重新测量自身尺寸那一步直接在原有布局上做局部更新减少一次整页重排。结构简单意味着明确明确意味着性能可以被优化。反过来如果你明确知道Item高度可能动态变化比如折叠卡片就不能设置这个标志否则布局会出现意想不到的跳动。它不是一个开关也不是一个属性格式问题而是一个你是否掌握了自身结构的声明。这个语义也侧面印证了RecyclerView的底层结构设计者是希望开发者能对自己的页面行为做到完全掌控。3. 布局摆放逻辑的简单化事件在处理Item的位置时到底发生了什么3.1 LayoutManager核心工作流fill与layoutChunk很多人看LayoutManager源码会睡倒在layoutChunk()和fill()这两个方法面前。我用一份抽象的逻辑把它简化所谓布局就是LayoutManager在拿到一个RecyclerView之后按照自身的排列策略反复执行取下一个位置的ViewHolder→测量View尺寸→设置它在坐标系中的位置→放入布局容器这一套循环动作。fill()负责遍历可填充区域比如用户往下滑需要从底部不断填充新的数据向上滑则需要从顶部补位它本质是一个按需生产的调度器。layoutChunk()则是单次动作的具体执行者拿到一个ViewHolder调用measure测出宽高依据锚点位置计算偏移然后调用layout把View放置到位。这套流程对Linear、Grid、Staggered全部适用只是每个布局管理器在layoutChunk中的坐标算法不同。3.2 滚动是谁在负责RecyclerView本身还是ItemDecor还有一个容易混淆的结构点滚动。RecyclerView的滚动并不像普通ScrollView那样是直接修改子View的位置而是由一个Scroller驱动不断调整所有Item相对于父容器的偏移量让新的数据从上下两侧进入可视视口。这里牵涉到一个常见问题为什么getChildAt(0)拿到的永远去第一个完全可见的Item因为RecyclerView的子View列表不是全量数据它只保留当前可见的那部分。可见子View都是挂在RecyclerView的实际子节点列表里的结构和数据是一一映射的关系。理解了这一点你在滚动监听里处理判断是否到底时就能清楚应当用findLastVisibleItemPosition()来获取逼近尾部的位置再结合getItemCount()判读到底。3.3 间距为什么用ItemDecoration实现而不是XML嵌套有的朋友在列表项外再套一层带margin的父布局这样也能“实现间距”。但结构上这样做除了增加嵌套层级以外还导致一个更隐性负担每次onBindViewHolder时填充的视图层级更重測量时间增加。虽然单个Item的差异未必能明确感知但当列表达到几百上千行时这种画笔“每一行都多一层FrameLayout”就会造成可测量的帧率损耗。RecyclerView特有的ItemDecoration是结构上的优雅解法——它把画分割线和计算Item之间间隔独立成一个装饰器角色在Item绘制前后回调中可以直接修改Item的offsets。装饰器一个用来算间距、一个用来画背景彻底把样式与应用逻辑剥离开。你新建一个SpaceItemDecoration继承RecyclerView.ItemDecoration在getItemOffsets中给四个方向赋值这就算简洁的完成了间距控制。4. 交互与更新的简单原则接口解耦和数据差异化4.1 点击事件为什么要交给OnItemTouchListener或OnClickListener列表的点击事件是一个绕不开的需求。但常见的写法是先为每个Item View设置OnClickListener再通过一个顶层监听回调把position传出去。这种写法在结构上并没有问题可它埋了一个隐患——每次onBindViewHolder都会重新创建一个匿名对象设置监听器绑定次数多了就会产出无谓的对象。更优的做法是把全局监听器定义为Adapter的一个字段在onCreateViewHolder阶段就绑定一次后面只负责转发。RecyclerView对触摸事件还有一个更高层的设计OnItemTouchListener。它能拦截RecyclerView整个Child的触摸事件流做到手势的全局处理。如果你实现了拖拽排序或滑动删除这层是必经之路。它的存在再次体现了RecyclerView的模块化理念触摸交互不在Adapter里不在Item布局中而是抽离成独立的触控策略。4.2 数据更新的艺术从notifyDataSetChanged到DiffUtil如果说结构上有一个最容易被初学者用成性能毒药的地方那就是数据刷新。很多人拿到新数据后无脑调用notifyDataSetChanged()。这个方法的实现原理是让适配器把所有Item统统标记为全部变化然后清空缓存触发全量重绘。一旦数据量达到500行卡顿就不可避免。RecyclerView结构简单体现在这里是它支持精准的局部更新通知——notifyItemInserted()、notifyItemRemoved()、notifyItemMoved()、notifyItemChanged()。你可以根据数据源的变化类型通知具体索引RecyclerView就会触发局部动画不需要级联刷新所有行。在实际项目中数据源变动往往不是单一类型最省事且性能好的做法是用DiffUtil。DiffUtil用areItemsTheSame和areContentsTheSame两个回调把新旧数据集合做一次O(n)级别差异比较产出完整的最小更新操作集。它可以自动判断哪些Item要增、哪些要减、哪些仅content变化再自动调用对应的务实通知方法。配上ListAdapter基于List的Adapter基类整个数据驱动流程变成了一行代码就能完成的简洁闭环。我自己的习惯是在对待差别较大的本地数据时用submitList配合DiffUtil在不希望出现闪烁动画的位置干脆直接用adapter.submitList之后加notifyItemRangeChanged弥补。总之能移动局部就不全量刷新能让差异算法控制的就别手工调度。4.3 完整可运行的简易示例写作到这里有必要给出一段可运行的示例代码让文章前面的结构分析真正落地。class SimpleAdapter(private val items: ListString) : RecyclerView.AdapterSimpleAdapter.VH() { class VH(view: View) : RecyclerView.ViewHolder(view) { val text: TextView view.findViewById(R.id.item_text) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { val view LayoutInflater.from(parent.context) .inflate(R.layout.item_simple, parent, false) return VH(view) } override fun onBindViewHolder(holder: VH, position: Int) { holder.text.text items[position] } override fun getItemCount(): Int items.size }val recyclerView findViewByIdRecyclerView(R.id.recycler_view) recyclerView.layoutManager LinearLayoutManager(this) recyclerView.adapter SimpleAdapter(dataList)只有10行核心代码一个列表就运作起来了。你不需要关注RecyclerView内部是怎么测量、布局的因为框架全处理好了你也不需要在更新数据时重建整个Adapter那会给复用机制添乱。你把最小必要业务交给它它给你的是稳定扎实的基础能力。5. 认清简单背后的复杂代价开发中常见的结构误区和取舍5.1 误区一把所有逻辑塞进Adapter导致巨型类我见过最复杂的Adapter有800行里面包括布局初始化、网络图片加载、点击转发、动画控制甚至还有HL和时区的转换逻辑。这说明写代码的人根本没理解RecyclerView的结构——Adapter只是生产线工位业务逻辑是车间之外的指令中心。如果Item类型多直接使用getItemViewType区别并行地生产多个ViewHolder视图如果某种逻辑复杂到超过100行就抽成一个独立类放本文件之外如果发现Adapter某些方法高度一致完全可以抽共同的父类。记住结构简单不代表类少而代表每个类职责单一。这样做的好处是等产品提需求要加个footer或header你只需要改动对应视图类型不会顾及那些牵连。5.2 误区二在滑动监听里做大量计算滚动监听回调onScrolled触发非常频繁一帧可能调用数次。结构上如果你在这里做Json解析、网络请求同步执行就会导致主线程拥堵到无法响应触摸事件。正确的做法是将滚动状态推导做一个节流处理只关心onScrollStateChanged里的空闲状态SCROLL_STATE_IDLE结合当前first/last可见Item与总数来判定是否加载更多。这样滑动过程中的计算量降到最低只在你真正想处理的时刻执行一次。另一个常被忽略的滑动性能点是对图片库的处理。如果你的列表Item含网络图片且没有对重复资源做内存缓存它的复用机制将会被打破——每次onBindViewHolder都要重新请求字段数据并按ImageLoader生成bitmap。请务必给RecyclerView用的图片加载器配置统一的复用数和内存Cache否则列表快速滑动时图片闪烁和OOM几乎是必然的。5.3 误区三为了结构简单而忽略RecyclerView的生命周期RecyclerView结构上也在依赖Fragment/Activity生命周期来管理资源。最佳实践是如果你在Adapter里持有context必须使用ApplicationContext窄化生命周期或妥善处理销毁逻辑如果你在Adapter持有datasource和图片加载器在Fragment里应当保留同一个Adapter实例避免重建时把旧代码和接口一并丢失。特别提醒一个细节当你删除数据时很多新手只更新了数据源后在UI线程调用notifyItemRemoved却忘了解绑Item的监听器或释放持有资源。虽然RecyclerView不会因为这样立刻崩溃但长期累积下来内存泄漏就是这么形成的。结构越简单越要警惕那些边界次序没有做清理的地方。5.4 结构上的前后向兼容嵌套滚动也很简单掌握了RecyclerView基础结构之后嵌套滚动的实现也会变得非常直接。所谓嵌套滚动其实就是RecyclerView之间通过NestedScrollingParent2接口进行事件分权和距离消耗协调。它的存在让顶部大图内部列表、外层纵向内层网格这类架构以极其可控的方式来组织而不至于出现两个滚动体抢占事件的情况。组件从结构上已经很清晰地分好了主从职责外部RecyclerView认内部是子Coordinator滚动的事件先交给内部尝试消费内部不能消费的再反馈给外部。这样的设计让相互嵌套在代码结构上依然保持简洁——你不需要在手指activity里处理两个事件的联动架构已经把链路定义好。对开发者来说你只需要按常态写布局按志向选嵌套模式逻辑即可。6. 从结构看生命周期每一次状态变化为什么可预测6.1 RecyclerView是自动流水线的缩影状态决定行为如果把RecyclerView看成一个状态机它的核心状态包括空闲SmoothScroll、正在滚动Dragging、正在设置数据LayoutPending等。这些状态与缓存池、ViewHolder、数据源一起组成了一个闭环系统。当你在滚动时这个系统在不断执行着取出新的、回收旧的、重新绑定的动作而这一切都是由内部状态驱动、精确编排的。这个结构带来的最大优点是可预测性。只要你能保证数据源中数据是正确的、position没有越界RecyclerView的执行结果就一定正确。不会出现我明明改了数据但列表不刷新这种玄学因为那个大概率是你忘了调adapter.notifyDataSetChanged()或使用了错误的position。6.2 多Type列表的统一视图结构当列表需要混合多种类型例如文字段落、图片卡片、分割线时RecyclerView用getItemViewType轻松地管理了多类型骨架。在结构上它相当于一条流水线按标签开关生产不同样式的容器。代码里顺序判断type时多加几个else分支在所难免但若类型超过4-5种建议用when表达式配合密封类做主控这样Adapter依然能保持它只是一个连接器的简单本质。多Type适配器中一个高发的Bug是类型返回的viewType与创建ViewHolder不匹配。你只要在创建时让onCreateViewHolder响应正确的viewType并在对应的布局文件里保持结构一致一个正确的多Type列表写起来和第3.3节示例差别并不大。6.3 一切从简为读者建造自己的列表心智模型如果你通读了前面几节会发现在RecyclerView的体系里流转的信息始终是数据List→Adapter→ViewHolder→缓存池。当你遇到任何问题只要你把这个链路在脑海里完整跑一遍就有过半概率瞬间定位出问题所在。我以前解决过一个顽固的Item布局错乱问题最后发现是对应Item ViewHolder里的ImageView复用了上一行的Bitmap引用没有清零。这不是框架的问题也不是结构的问题是因为在复用时我忘了“对应数据会覆盖旧View”。只有理解它是对空壳重复利用才会在写onBindViewHolder时下意识检查view的复用状态然后加上if条件判断把错误防范在源头。7. 写在最后一个老开发者的经验沉淀RecyclerView能做到结构简单不是因为需要写的代码少而是因为它的分层足够清晰、边界足够明确。作为开发者你完全可以不去读源码、不去看实现只需要站在它设计的指挥高度把数据、视图、位置三件事精确地交给对应模块就能得到稳定流畅的体验。实际操控项目的几年里我沉淀下来的心得可以浓缩成几条写在这里供你参考永远不要让Adapter感知业务逻辑如果哪一天发现自己不得不在Adapter里判断传进来的字符串是否合法那大概率是数据层改造没做到位。让Adapter保持天真只做view对象与data对象的最平铺的一一对应。数据和UI之间只通过回调通信不要在Adapter里直接调用Activity方法不要持有Activity实例字段。接口回调或LiveData是更持久的解耦这在结构上是守住简单的保险绳。善用post执行刷新当你从子线程更新数据后切回主线程再提交数据源和Adapter通知。如果你在List更新后立刻调notify可能处于激烈布局中数据状态不稳定。规则是数据到位再提交列表。试试沉浸式列表体验用RecyclerView加上FlowLayoutManager或FlexboxLayoutManager做流式布局用SwipeRefreshLayout或NestedScrolling做下拉联动。你会发现只要基础结构扎实这些功能其实都可以以外挂的形式延展在Adapter之外不污染它的核心逻辑。我自己也经历过因为一个列表卡到掉帧不得不彻夜排查性能瓶颈的阶段。但回头分析那一晚的焦虑恰恰来自于没有把结构吃透导致我到处乱加hack代码。第二天我发现把ImageLoader缓存打开再顺带着把setHasFixedSize(true)标上问题直接消失。大部分所谓的复杂问题本质上还是结构认知不足。最后再分享一个小技巧当你面对一个新的列表需求时别急着写代码先在注释里把三句话描述出来——我这里展示的是什么数据类型每个Item由哪些基础控件组成滑动到这个位置用户可能做什么操作想清楚这三个问题代码的骨架自然就清晰了。RecyclerView的简单不是在嘴上说的是那套从设计之初就定下来的解耦哲学赠给认真理解它的人的礼物。愿你在阅读之后能在下一次列表开发时感受到那些下变形层安静运转的部分带来的底气。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。