深入优化RecycleView 三人齐聚忘忧愁

1,496 阅读20分钟

本系列为 Android 技术职场题材虚构小说,所有登场人物、公司名称、组织架构及相关情节均为创作所需虚构而来,若有雷同,纯属偶然。书中涉及的技术知识经专业梳理,仅供参考。

0.png

十)优化RecycleView 三人齐聚忘忧愁

有了上一次 View 优化的大量经验,组内渐渐形成了一种默契。凡是涉及 View 优化相关的需求,都会自然而然地交到林卓手中。

这不,新闻模块的迭代优化,也顺理成章由他牵头负责。

作为该模块 UI 的核心开发优化人员。林卓深知车机端新闻展示的痛点,不仅要保障滑动流畅,更要适配复杂车载场景,从布局架构到细节呈现,每一处都容不得半点马虎。

这天一早,刚到工位,设计岗的产品同事就找过来:“林卓,新闻卡片里聊天对话的背景图,拉伸后边缘都糊了,你看看怎么处理?”

林卓查看 UI 走查的结果,果然,普通 PNG 格式的对话气泡,在不同长度的文本适配下,四角拉伸变形,边缘线条模糊不清。

“啧,果然这搞车载的 UI 就是拉胯,这点细节都没处理好。”他暗自抱怨一句,心里却已快速敲定优化方案——用 .9 图来解决。

“这得用 .9 图来优化。”林卓立刻给出方案,顺手打开工具演示起来,“.9 图全称是 NinePatch 图,本质是带拉伸区域定义的 PNG,核心就是通过 1px 的透明边框,划分出固定区域和可拉伸区域。”

他对着工具界面喃喃自语,手指点着屏幕梳理逻辑:“左侧和上侧的黑色线段是可拉伸区域,只在指定范围拉伸,不会动四角的圆角;右侧和下侧是内容区域,控制文本边界防贴边。这样一来,不管文本多长,气泡边缘都能清晰,圆角也不会变形。”

设计同事见他思路清晰,又能快速上手处理,放心地点点头便独自走开了。

林卓对着工具继续操作,一边将普通 PNG 处理成 .9 图,一边喃喃补充着使用细节,像是在给自己梳理注意事项:“可拉伸区域不能画关键元素,不然拉伸后肯定错位;透明边框必须严格控制在 1px,多了少了都会解析失败。”

他替换原有资源预览,见气泡拉伸自然、边缘锐利,又接着自语:“还得适配不同密度屏幕,得准备多套图放进对应 drawable 文件夹,不然在高清屏上会模糊。”

解决完 .9 图的问题,林卓对着屏幕反复走查了几遍,对话气泡拉伸自然、边缘锐利,在不同文本长度和屏幕尺寸下都表现稳定,确认无误后,便把需求打回给 UI 团队继续后续走查。

刚关掉设计工具,电脑右下角的邮箱就弹出了新提醒,标注着“紧急 Bug”。

林卓顺手点开,发现 Bug 归属模块正是自己负责的新闻模块,标题赫然写着“新闻列表数据量较大时滑动掉帧,卡顿明显”。

他往下滑动详情页,目光落在提交者一栏时,嘴角不自觉地弯了弯——提交人是小安。林卓心里暗自窃喜,指尖已经悬在Q书聊天框上,正想发消息问她是不是也来支援车载项目了。

“小林,看到新闻列表的 Bug 了吗?”一道声音突然从身后传来,林卓回头一看,张磊站在他工位旁,语气带着几分急切,“这些 Bug 优先级很高,测试那边反馈车机高速滑动时卡顿、掉帧,问题比较多,你仔细看看,今天必须搞定。”

“好的张哥,我马上排查原因。”林卓立马应声点头,生怕张磊重提旧事。

收起笑意,收敛心神,快速浏览 Bug 详情里的补充说明:列表数据超过50条后掉帧频发,滚动时帧率波动剧烈。

他当即打开 RecyclerView 的相关代码和运行日志,眉头微微蹙起,着手逐行分析问题根源。

果不其然,现有适配器存在两处关键问题:一是仍在使用 notifyDataSetChanged() 全量刷新,哪怕只有单条新闻更新,也会触发所有 Item 重绘,导致 CPU 负载飙升;二是未合理配置缓存池,不同类型的新闻卡片共用同一缓存,复用效率低下,造成内存波动。

林卓先从刷新机制入手,集成 DiffUtil 工具类。

他创建不可变数据类 NewsItem,封装新闻 ID、标题、内容、类型等字段,确保数据不可变以避免并发问题。

随后实现自定义 DiffUtil.ItemCallback<NewsItem>,重写两个核心方法:areItemsTheSame 通过新闻唯一ID 判断是否为同一数据,避免重复刷新相同 ItemareContentsTheSame 对比 Item 的核心内容字段,精准定位数据变化的部分。

“之前用全量刷新,RecyclerView 不知道哪些 Item 变了,只能全盘重绘。”林卓一边将适配器继承自 ListAdapter,一边自语,“现在用 submitList() 提交新数据,DiffUtil 会自动计算新旧列表的差异,只刷新变化的 Item,性能至少能提升 40%。”

接着他优化缓存策略,针对图文、纯文字两种新闻卡片,配置 RecycledViewPool 的缓存数量:“图文卡片布局复杂,创建成本高,缓存数量设为 5;纯文字卡片简单,设为 8,这样既能保证复用效率,又不会因缓存过多占用内存。”

同时,他在 RecyclerView 不再使用之后,清理图片加载任务和临时数据,避免内存泄漏和波动。

优化完刷新和缓存,林卓将目光投向布局结构。他打开新闻卡片的 XML 布局文件,发现原有布局用了三层 LinearLayout 嵌套:外层包裹卡片整体,中层分隔图片和文本区域,内层排列标题和内容。

嵌套层级太多了,这也是卡顿的重要原因。”

车机端的 ViewGroup 测量和布局是自上而下递归进行的,每多一层嵌套,就会增加一次测量和布局运算,尤其在 RecyclerView 快速滑动时,多层嵌套会大幅增加 CPU 负担。

林卓果断将布局替换为 ConstraintLayout,通过约束机制实现扁平化布局,将三层嵌套压缩为一层。

他重新梳理布局逻辑:将卡片根布局设为 ConstraintLayout,图文卡片的图片锚定在左侧,通过 app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" 固定位置,宽度设为 80dp,高度设为 0dp 并设置 app:layout_constraintDimensionRatio="1:1",保证图片比例固定。标题文本则通过 app:layout_constraintStart_toEndOf="@id/iv_news_img" 锚定在图片右侧,内容文本对齐标题下方,用 app:layout_constraintBottom_toBottomOf="parent" 确保底部对齐。

ConstraintLayout 的核心优势就是扁平化和灵活约束,不仅能减少层级,还能通过 GuidelineBarrier 实现自适应。”林卓添加了垂直 Guideline,将卡片右侧内边距设为屏幕宽度的 8%,确保在不同尺寸的车机屏幕上,卡片布局比例一致;同时用 Barrier 将文本区域与图片关联,当图片高度变化时,文本区域自动调整,避免重叠。

布局优化完成后,林卓立刻连接车机进行真机自测。手指快速滑动新闻列表,即使数据量超过80条,帧率仍稳定在 60FPS 左右,无任何掉帧卡顿,缓存复用也十分顺畅,之前的 Bug 已彻底解决。

他截图留存测试效果,打开 Bug 管理平台,将该 Bug 状态更新为“已修复”,同时@小安并附上备注:“列表卡顿问题已解决,优化了 RecyclerView 刷新机制、缓存策略及布局层级,自测通过,麻烦你回归验证下。”

发送完毕后,林卓又点开聊天框,指尖悬在输入栏上,心里的疑惑再次冒了出来——他正想敲字问一句“你是不是来支援车载项目了”,办公桌上的电话突然响了。

接起电话,产品经理的声音传来,语气带着几分急切:“林卓,新闻模块要加个紧急需求,得在列表里加视频预览功能,支持短视频缩略图自动播放,还得能跟随车机屏幕旋转自适应,客户那边催得紧,你这边尽快评估落地。”

林卓闻言一愣,随即想起自己刚才一心扑在 Bug 修改上,压根没来得及看工作群消息。他快速点开聊天记录,果然看到产品经理一小时前就发了需求文档,还@了他,只是当时被 RecyclerView 的优化逻辑缠住,完全没留意。

“抱歉,我刚才在紧急处理列表卡顿的 Bug,没及时看消息。”林卓连忙致歉,语气带着几分歉意,“需求我现在就看,技术方案尽快给你反馈,争取今天先把核心功能落地。”

林卓压下对小安的疑惑,挂了电话,收起聊天框,重新聚焦到代码上,着手分析视频预览功能的技术选型。

林卓最初选择用 SurfaceView 实现视频预览,这是安卓早期常用的视频渲染组件。

他快速集成播放器,实现了视频播放功能,但测试时发现了明显问题:当车机屏幕旋转时,视频预览画面缩放和方向都有问题,无法流畅的适配新大小和方向,用户体验极差。

“这就是 SurfaceView 的局限性。”林卓翻开老杨的在线笔记,对照着知识点梳理,“SurfaceView 有独立的绘图表面,不与 UI 层级共享,虽然渲染性能好、不阻塞主线程,但旋转、缩放等 UI 变换支持差,问题多,而且它的显示不受 View 树控制,容易出现层级错乱。”

他决定替换为 TextureView,两者虽都用于高性能渲染,但特性差异显著。

TextureView 将视频帧渲染为纹理,无缝集成到 View 树中,支持所有 UI 变换,还能与其他 View 组件叠加显示;但缺点是渲染效率略低于 SurfaceView

不过眼下的情况,正适合。

林卓动手修改代码,将布局中的 SurfaceView 替换为 TextureView,并实现 SurfaceTextureListener 接口:在 onSurfaceTextureAvailable 中初始化播放器,绑定 SurfaceTextureonSurfaceTextureSizeChanged 中调整播放器尺寸,适配屏幕旋转;onSurfaceTextureDestroyed 中释放播放器资源和 SurfaceTexture,避免内存泄漏。

为了平衡性能,他还做了补充优化:开启硬件加速(车机端默认开启),避免 TextureView 渲染卡顿;在屏幕旋转时,通过 onConfigurationChanged 方法手动调整 TextureView 的旋转角度,无需重启播放器。修改后测试,视频预览不仅播放流畅,还能跟随屏幕旋转无缝适配,与其他 UI 组件层级清晰,完全满足需求。

不过林卓并未就此止步,他考虑到用户点击预览图进入全屏播放的场景,特意做了分层设计——全屏播放时,将渲染组件切换回 SurfaceView

这正是 SurfaceView 的用武之地:全屏场景下无需与其他 UI 组件叠加,也不需要复杂的旋转缩放变换,核心需求是极致的播放流畅度和低延迟,而 SurfaceView 独立绘图表面的特性,能最大程度降低主线程阻塞,即便在车机硬件资源紧张时,也能保证视频帧渲染稳定,避免出现卡顿、花屏。

他通过监听全屏状态切换逻辑,在用户点击预览进入全屏时,销毁 TextureView 并初始化 SurfaceView,绑定播放器资源;退出全屏时则反向切换,释放 SurfaceView 资源并重建 TextureView

同时补充了资源衔接处理,确保切换过程中视频不中断、声音不卡顿,实现预览与全屏播放的无缝过渡。“各取所长才是最优解,”林卓喃喃自语,既用 TextureView 解决了列表预览的 UI 适配问题,又靠 SurfaceView 保障了全屏播放的性能底线。

理清组件切换逻辑、补全资源释放代码,林卓又对着车机反复测试了多轮预览与全屏切换场景,确保视频播放流畅、无卡顿中断,才终于松了口气。从接到需求到完成核心功能落地,整整三个小时,他连水都没顾上喝一口。

确认视频播放功能完全符合需求,林卓截图留存各场景效果,整理好功能说明文档,一并提交给 UI 团队进行走查。刚发送完毕,电脑右下角的 Bug 管理平台就弹出了新提醒,红色的“紧急”标识格外醒目。

林卓顺手点开,发现又是新闻模块的 Bug,提交人一栏赫然显示着“小安”。

他心头一暖,笑着摇了摇头——果然是小安,作为测试岗的她向来细致,总能精准捕捉到容易被忽略的细节。这次的 Bug 描述很清晰:当用户将系统字体缩放倍率调整到 150% 时,没有任何效果。

林卓立刻打开测试环境复现场景,将系统字体缩放倍率调至 150% 后,纯文字卡片的标题文本果然毫无变化,没有跟随缩放变大。

他快速排查代码,很快锁定问题根源:原有文本大小误用了 dp 单位,内边距和卡片宽度还混杂了 px 单位,尺寸单位使用极不规范,导致出现该 Bug。

“关键问题就在单位上。”林卓对着代码喃喃自语,理清核心逻辑,“dp 是密度无关像素,只能适配不同屏幕密度,确保组件在不同设备上尺寸一致,但完全不响应系统字体缩放倍率——不管用户把字体调大还是调小,用 dp 设置的文本都不会有任何变化,这不符合可访问性需求。”

“而 sp 单位才是文本的标准单位,它不仅能像 dp 一样适配屏幕密度,更核心的是会精准跟随系统字体缩放倍率变化,用户调整字体大小后,文本能同步缩放,这也是解决当前问题的关键。林卓当即着手优化,先将所有文本大小从 dp 统一改为 sp,让文本能够响应系统字体缩放设置。”

他还给 TextView 补充布局约束:设置 maxLines="2"ellipsize="end",确保文本跟随系统缩放后过长时,能以省略号优雅截断。

同时将卡片根布局宽度设为 match_parent,内边距、间距全部改为 dp 单位,避免因单位混杂导致布局错乱,确保字体放大后,卡片能自适应文本高度,彻底解决溢出问题。

优化后林卓再次测试,将系统字体缩放倍率分别调至 125%、150%、175%,纯文字卡片的文本均能同步缩放,且无任何溢出,布局保持规整。

为了规避团队后续再出现类似问题,林卓打开团队共享的开发规范文档,特意新增并强化了一条尺寸单位使用准则:布局组件大小、边距、间距统一用 dp,保证多设备尺寸一致性;文本大小必须用 sp,强制响应系统字体缩放设置,满足不同用户的阅读需求;特殊场景(如自定义 View 绘图)需将 dp 转为 px 计算,通过 Resources.getSystem().getDisplayMetrics().density 获取密度系数,严禁硬编码或混用单位。

他补充完规范后,又顺手整理了 dpsppx 的核心区别备注在旁,方便团队新人快速理解:px 是固定像素单位,跨设备显示差异大,禁止用于布局;dp 适配屏幕密度,适合布局属性;spdp 基础上兼容字体缩放,专为文本设计。

林卓长舒一口气,终于把今天所有的优化任务和 Bug 修复都收尾了。他抬手揉了揉酸胀的眼睛,瞥了眼电脑右下角的时间——晚上八点半。紧绷了一天的神经骤然放松,他才想起还没问小安支援的事,赶紧点开Q书聊天框,敲下一行字:“小安,你是不是来支援车载项目了?怎么都是你提的 Bug 呀”,点击发送后便静静等着回复。

消息刚发出去,一道带着埋怨的声音就从身后传来:“还问呢,可不就是来支援了,多亏了你这位‘优化大神’,我也得陪着你加班到这会儿。”

林卓浑身一僵,随即猛地回头,只见小安站在身后,平日里精神利落的高马尾因熬了半天加班,早已松松垮垮地垂成了低马尾,发尾微微凌乱,眉头轻蹙却难掩眼底笑意,语气里的埋怨更像是带着几分疲惫的撒娇。

看清来人,林卓瞬间喜出望外,连日来加班的疲惫一扫而空,激动得差点从椅子上跳起来,手舞足蹈地说道:“太好了,咳咳,有你测试,我就放心多了。”他只顾着和小安说话,完全没留意周围的动静。

小安忍着笑,用下巴指了指他前方:“停,停,你看看前面是谁。”林卓疑惑地顺着她指的方向看去,只见老杨端着那只标志性的搪瓷杯,慢悠悠地站在工位过道上,杯沿还冒着淡淡的热气,脸上带着温和的笑意,正似笑非笑地看着他。

林卓立刻收敛了雀跃的动作,站起身挠了挠头,有些不好意思地打招呼:“老杨,怎么,你也来了?”老杨笑着抿了口茶,晃了晃手里的搪瓷杯:“听说我们小林今天啃下了新闻模块的硬骨头,过来看看。顺便,也接你们俩吃个夜宵。”

......

三天后,新闻模块迎来了最终验收测试。测试团队围绕全场景逐项核验,小安带着测试用例逐一排查,所有优化点均达标,无新增 Bug。

“完美通过!”小安拿着测试报告走到林卓工位,松了口气笑道,“你这波太稳了,哈哈!”

林卓看着报告上的“全部通过”,连日加班的疲惫彻底消散,正想开口,就见张磊带着老杨走了过来,脸上满是喜色。

“小林,这次新闻模块优化做得漂亮,客户那边看了演示视频,特意发消息表扬了我们团队。”张磊拍了拍林卓的肩膀,又侧身指了指老杨,“对了,跟你说个事,老杨这次也来支援车载项目了,只不过没跟我们一起负责 UI 模块。”

林卓一愣,随即看向老杨,眼里满是疑惑:“老杨,您也来支援了?我还以为您只是过来看看我们。”

老杨笑着晃了晃手里的搪瓷杯,杯沿的热气袅袅升起,语气温和却带着专业感:“是啊,过来支援一下,主要负责大模型底层段模型的集成与优化,跟你们 UI 模块属于并行推进,没特意打扰你们赶进度。”

他顿了顿,补充道:“车载大模型的底层段模型,关乎语义理解速度和资源占用率,得反复调试适配车机硬件,确保在低功耗下也能快速响应。你们 UI 模块优化好了交互体验,我这边把底层模型打磨稳定,两者配合才能给客户呈现最优的整体效果。”

林卓恍然大悟,心里满是敬佩:“原来是这样,我说您那天晚上过来,不光是接我们吃夜宵,也是在盯着我跟进项目进度。难怪这几天感觉系统整体响应快了不少,原来是您在底层做了优化。”

“互相配合罢了。”老杨摆了摆手,目光落在林卓的电脑屏幕上,看着优化后的新闻模块,赞许地点点头,“你这次把 RecyclerView、组件选型这些基础技术用得很扎实,尤其是根据场景灵活切换 SurfaceViewTextureView,还补充了开发规范,看得出来是真的吃透了,成长很快。”

张磊接过话茬,翻看着测试报告,语气里满是自豪:“每个技术点都用在了刀刃上,完全解决了车载场景的痛点。现在 UI 模块和底层模型都稳步推进,这个版本的迭代算是稳了。”

林卓笑着挠了挠头,语气诚恳:“都是跟着您的笔记学的,再加上小安测试时帮我揪出不少细节问题。其实这些技术看似基础,只有真正在项目中遇到问题、动手优化,才能真正掌握,还得感谢您和小安的帮衬。”

小安在一旁打趣道:“别谢我,我只是做好本职工作,倒是你这位‘大神’,好好加油,省得天天陪你加班找 Bug。”一句话逗得众人都笑了起来,连日加班的紧绷氛围彻底变得轻松。

......

深夜的办公区,只剩零星几盏灯还亮着。林卓看着屏幕上流畅运行的界面,指尖划过代码中熟悉的技术实现,心里满是踏实。车机屏幕上,新闻卡片有序排列,视频预览流畅播放,文本显示清晰规整,每一处细节都凝聚着他对技术的敬畏与打磨。

窗外的月光洒在键盘上,照亮了他接下来的计划——把这次优化的经验整理成笔记,就像老杨那样,为团队沉淀有价值的技术资产,也为自己的成长留下清晰的印记。

林卓关掉代码编辑器,顺手点开Q书,置顶位置两个头像,一个是老杨的搪瓷杯专属头像,一个是小安笑眼弯弯的自拍头像。他愣了愣,随即嘴角扬起一抹温和的笑意。

身边有经验丰富的老杨在底层兜底支撑,有细致的小安帮着把控测试细节,这份曾让他倍感压力的车载开发工作,不知不觉间竟变得轻松惬意起来。

他关掉电脑,起身走向小安,询问是否下班,连日加班的疲惫早已被这份踏实与暖意冲淡,脚步都变得轻快了许多。