1. 为什么要为一个列表组件磨上半年HarmonyOS 6 发布之后我在自己主导的 App 里重构了列表模块最终沉淀出一个名为 RcList 的组件。项目标题里写半年磨一剑很多人听到第一反应是一个列表组件至于搞这么久吗说实话最开始我也觉得不至于。但真正把需求理清楚之后才发现列表滚动本身只占工作量的一小部分真正的深水区全在缩略图、角标和图标系统这三个外围能力上。先说背景。我们 App 的信息流页面大概有十几处入口首页推荐、个人主页、搜索历史、收藏夹每一处的列表样式都略有差异。早期方案是每个页面单独写列表实现结果就是代码严重重复图片加载策略不统一角标逻辑满天飞。到了 HarmonyOS 6 这个节点开发语言和 UI 框架都发生了不小变化我借这个机会把列表模块整体收敛成一个带缩略图、角标、图标能力的通用组件也就是 RcList。这半年里踩过的坑、推翻过的设计、调出来的性能数据我觉得比组件本身更有分享价值。这篇文章就把 RcList 从零到一的过程中那些最容易被忽略、又最影响体验的细节拆开讲特别是缩略图的缓存链路、角标的坐标计算、以及图标系统的资源管理方式。如果你也在 HarmonyOS 上做类似的自定义组件这篇文章应该能帮你少走不少弯路。1.1 RcList 的设计定位不是替代官方 List而是封装列表生态很多人会问ArkUI 不是已经有 List 组件了吗自己封装一个 RcList 是不是重复造轮子我的回答是RcList 做的是列表生态的整合不是重写滚动逻辑。List 本身提供的滚动、布局、复用能力已经足够稳定我没有任何理由去动它。RcList 的核心价值在于把列表里出现的各种通用元素统一收口暴露给业务方一套简洁的配置接口。比如缩略图怎么加载、加载到什么尺寸、内存缓存多大、角标显示规则是什么、图标按什么主题渲染这些原本分散在各页面的逻辑全部下沉到组件内部。这样设计的好处非常明显。业务侧只需要声明数据模型和配置项不用关心图片怎么解码、缓存怎么命中、角标要不要动画、图标该用浅色还是深色。对 HarmonyOS 应用开发来说这种配置式组件特别适合团队协作新人上手成本低出问题的概率也小得多。从架构上看RcList 分成四层最底层是官方 List 的容器能力往上是数据适配层再往上是视图渲染层最外层是配置面板。缩略图、角标、图标系统作为三个独立模块挂载在视图渲染层互不耦合。这样一来任何一个模块出了问题都可以单独替换不影响另外两个。1.2 拆解核心需求缩略图、角标、图标系统各自解决什么问题先把三个核心需求单独拎出来看明确各自的边界才能谈设计。缩略图的痛点集中在量大、图小、反复出现。信息流列表动辄几十上百条数据每一条都有封面图如果按原图加载内存和流量都扛不住。所以缩略图系统要解决的问题是用什么尺寸解码、用什么策略缓存、怎么在快速滑动时避免卡顿和闪烁。角标的痛点在于规则复杂、位置敏感。角标不只是右上角一个红点。数字角标要处理超过 99 的显示逻辑红点角标要控制显隐和动画还有新消息的NEW文字角标位置又分左上、右上、左下、右下和中心五档。加上不同设备屏幕的适配坐标计算稍有不慎就会偏移。图标系统的痛点在于数量大、状态多、主题动态切换。一个成熟的应用列表里出现的图标至少几十种还得分普通态、选中态、禁用态。系统如果换了深色模式图标颜色要跟着变。图标系统要想清楚的是怎么管理资源、怎么切换主题、怎么减少解码开销。这三块独立看都不复杂但放进同一个组件互相之间就会产生交互。比如缩略图加载完成之后角标要重新定位因为图片的圆角裁剪会改变角标锚点的坐标。这类的联动问题在开发过程中反复出现也是我花时间最多的地方。2. 缩略图管线从尺寸解码到两级缓存每一步都有讲究缩略图是 RcList 里工作量最大的模块没有之一。HarmonyOS 6 的图片框架提供了 ImageSource、PixelMap 等能力但直接拿它们做列表缩略图很容易踩坑。2.1 先定解码尺寸不要用原图也别用屏幕像素缩略图的第一原则是不要加载原图。HarmonyOS 的图片解码是按像素图PixelMap进入内存的一张 4000x3000 的封面图解码之后的内存占用大约是 48MB4000×3000×4字节哪怕只是列表里的一个小缩略图内存也会瞬间爆炸。我的做法是显式指定解码尺寸。根据列表项里 Image 控件的实际显示尺寸乘以一个缩放因子算出一个合理的解码宽高。比如列表项封面图显示尺寸是 160x120解码尺寸就取 240x1801.5 倍保证在 2 倍分辨率屏幕下清晰同时内存只有原图的几十分之一。在 HarmonyOS 里解码尺寸用 ImageSource.createIncrementalSource 配合 DecodingOptions 的 desiredSize 参数来设定。顺手做了一个按比例裁剪封面图大多不是标准比例直接拉伸会变形。我在解码阶段就根据目标宽高比做了居中裁剪避免解码出整图再裁剪白费一倍内存。2.2 两级缓存磁盘缓存管复用内存缓存管速度缩略图要快光靠解码优化不够还得有缓存。RcList 用的是两级缓存磁盘缓存和内存缓存。磁盘缓存基于文件存储。第一次加载一张缩略图解码完成之后把缩略图编码成 JPEG 或 WebP 写入应用沙箱目录文件名用图片 URL 的哈希值。这样同一张图第二次出现就不需要重新下载原图直接读磁盘文件。磁盘缓存的淘汰策略用 LRULeast Recently Used上限按设备可用空间动态设置一般来说 100MB 到 200MB 之间比较保守不会挤占系统空间。内存缓存存放的是 PixelMap 对象本身。HarmonyOS 的 PixelMap 有一个特性解码后的像素数据占的是原生堆内存不受 JS 堆大小限制但也不能无限膨胀。所以内存缓存必须严格控制数量。RcList 的内存缓存上限设置为 64MB超出之后按 LRU 逐出最近的 PixelMap。这里有个关键细节PixelMap 被逐出后它引用的内存是否会立刻释放实测下来如果其他地方还持有这个 PixelMap 的引用底层内存不会退只有所有引用都断开了ImageSource 和 PixelMap 才会真正释放。所以我在缓存里保存的是 PixelMap 的弱引用配合一个后台清理机制定期扫描并强制回收超出上限的部分。这样做的好处是缓存命中时还能用一旦内存压力上来系统能顺利回收。2.3 异步加载与防抖机制让快速滑动不卡顿缩略图的加载不能同步执行。列表快速滑动时如果有几十张图同时发起加载主线程会被解码操作拖死。RcList 的所有缩略图加载都放在线程池里完成主线程只负责接收结果并更新 Image。但异步加载带来一个新问题竞态。用户快速滑动列表Item 会复用如果一张慢速图片加载完成时它的 Item 已经被复用来展示另一条数据直接设置图片就会张冠李戴。解决办法是给每次加载请求绑定一个自增 ID或者在网络请求回调里检查当前 Item 绑定的数据是否还是原来的数据。RcList 用的是后者检查数据标识符比如数据 id不一致就丢弃这次结果。还有一个优化是防抖。列表滚动期间不发起新的缩略图加载请求只加载当前屏幕可视区域内的图片。等滚动停止再处理等待队列里的其他请求。这个策略在 HarmonyOS 上效果显著滚动时列表帧率稳定在 60fps滚动停止后 300ms 左右就能补齐屏幕外的缩略图。2.4 占位图与加载失败态不要一黑一白切换缩略图加载过程中的占位图看似不起眼其实影响体验很大。直接用纯色块占位图片加载完成后会闪一下视觉上很生硬。RcList 的做法是加载成功之后做一个 150ms 的透明度渐变从占位图过渡到真实图片。这个动画在 HarmonyOS 上用属性动画即可实现成本极低但感官上顺滑很多。加载失败态也一样。网络错误、图片 URL 失效、解码失败都会导致缩略图无法显示。RcList 会在缩略图区域中央显示一个默认的图片已失效图标同时提供一个重试按钮。这里我踩过坑最初失败态和占位图用的是同一个图标结果用户分不清是正在加载还是彻底失败了。后来改成失败图标带一个半透明底旁边显示点击重试问题就清晰多了。// 缩略图加载核心流程简化版 async loadThumbnail(item: ListItem) { // 1. 先查内存缓存 const cached this.memoryCache.get(item.thumbnailUrl) if (cached) { item.image cached return } // 2. 查磁盘缓存 const disk await this.diskCache.get(item.thumbnailUrl) if (disk) { this.memoryCache.put(item.thumbnailUrl, disk) item.image disk return } // 3. 都没有才走网络下载 this.enqueue(item) // 放入等待队列滚动时延迟处理 }这套流程看起来没什么玄机但细节全在 cache 的实现里。磁盘缓存的写入和读取都用了异步锁防止同一个 URL 被多个线程同时写入导致文件损坏读取磁盘文件时还校验了文件大小小于 1KB 的文件直接视为损坏重新下载。3. 角标系统从单一红点到可配置的规则引擎角标也就是 Badge在很多场景里是有或没有的问题但在 RcList 里我把它做成了一套完整的子系统。原因很简单不同页面对角标的要求不一样。3.1 四类角标各自的实现方案RcList 支持四类角标数字角标、红点角标、文字角标、自定义角标。数字角标是最常见的。右上角显示一个数字数字超过 99 的时候显示99。这里要处理的细节是角标宽度会随数字位数变化。两位数以内是固定圆形超过两位数变成胶囊形。我在组件里用了一个自适应布局角标的宽度由内容宽度决定同时设置一个最小宽高保证单个数字时也是圆润的形状。红点角标最简单就是一个固定大小的圆点。但红点的显示不能生硬地出现和消失。RcList 给红点加了缩放渐变动画出现时从 0.6 倍缩放到 1 倍消失时反向动画时长 200ms。这个手感上的细腻度用户可能说不出来但整体质感确实不一样。文字角标用来显示NEWHOT这类短文本一般出现在列表项的左上角或封面图左上角用于标识内容属性。文字角标要处理的是字体大小、颜色、内边距和圆角背景四个参数都得开放给业务方配置。自定义角标是最灵活的。允许业务方传入一个自定义组件或图片资源放在指定位置。这个能力是给运营位准备的比如某些推荐位会放一个小人偶图标加领券两个字。3.2 位置计算锚点、偏移量和安全区域角标位置是坑最多的地方。RcList 支持左上、右上、左下、右下、中心五个锚点位置每个位置都有水平和垂直偏移量可调。光是这样还不够角标的父容器可能是列表项的整个区域也可能是缩略图所以锚点支持两种参考系组件级定位和缩略图级定位。组件级定位角标相对整个列表项排版缩略图级定位角标相对缩略图图片常用于图片上叠加角标的场景。两种参考系的切换本质上是计算锚点基准坐标的偏移。缩略图有圆角裁剪、加载失败态、占位图角标位置参考的边界也会变化。比如缩略图加载失败后图片区域中央出现了点击重试如果角标还挂在图片左上角视觉上会叠在重试图标上很怪。所以加载失败时要自动把角标从该区域移除或平移。角标的坐标计算还牵扯到安全区域。HarmonyOS 对状态栏、挖孔屏、刘海屏都有安全区概念。列表本身在安全区内布局但角标如果做得太靠近边缘可能在视觉上贴边尤其横屏时。RcList 在角标偏移量基础上统一叠加一个最小内边距默认是 4vp。用户显式设置的偏移量在此之上生效避免角标被截断。另一个容易忽略的是角标层级。角标要显示在图片、文字之上但又不能超出列表项的裁剪边界。HarmonyOS 的 Stack 布局天然支持层级叠加我把角标作为最后一层放在 Stack 的顶部然后给整个 Stack 设置 .clip(true)既能保证角标始终可见又不会溢出到底部或下一个列表项。这个裁剪设置在实际测试中救了好几次特别是开启列表项阴影动画时角标容易被挤到外面。3.3 角标的更新策略局部刷新而不是整行刷新列表数据是动态的角标数字会变红点会消失。最初版本我采用数据变化就刷新整个列表项的策略结果发现性能损耗很大。一个列表项刷新意味着重新布局、重新排版、可能触发图片重新绑定代价太高。后来改成角标单独刷新的方案。角标数据单独绑定变化时只更新角标节点不更新列表项的其他部分。在 HarmonyOS 组件里可以给角标节点加一个 Observed 装饰器或者用状态管理把角标数据分开监听变化时只 setState 角标相关属性。实际测试中一个包含 10 个可见列表项的页面整体刷新需要 8ms而只刷新角标只需要 1ms 左右效果差异明显。这里再分享一个细节角标数字从 9 变成 10 的时候角标宽度会变宽如果动画处理不好会显得跳。RcList 的角标宽度用了隐式动画属性宽度变化时自动做 120ms 的过渡视觉上很平滑。这个功能不起眼但在消息列表那种数字频繁变化的页面上体验差异非常大。4. 图标系统从资源文件到主题联动的完整路径图标系统在 RcList 里的角色是把列表里可能出现的小图标统一管起来。包括文件类型图标、操作图标、状态图标。相比缩略图和角标图标系统更偏向于资源和主题管理但也有不少实现细节。4.1 图标资源管理从 PNG 到字体图标的迁移第一版 RcList 的图标全部使用 PNG 资源。问题是图标多了之后资源文件体积变大颜色无法动态跟随主题深色模式下效果很差。后来我做了迁移所有基础图标都从 PNG 换成字体图标iconfont方案。字体图标的核心是一个 .ttf 文件里面以 Unicode 字符位点对应不同的图形。运行时用 Text 控件显示对应字符通过 fontFamily 指定字体文件即可。这样做有几个好处颜色可以任意修改大小可以任意缩放而保持清晰多个图标共用一个文件资源体积远比一堆 PNG 小。HarmonyOS 支持在 Resource 资源目录放置字体文件然后在组件里通过 Font 对象或字符串指定。我在 RcList 里封装了一个 Icon 组件内部就是一个 Text 控件传入图标 key 时从映射表里取 Unicode 码点然后设置字体颜色和大小。这样业务侧用起来跟调用普通组件一样完全感受不到底层是字体还是图片。// 图标映射表示意图 const ICON_FONT_UNICODE: Mapstring, string new Map([ [fold, \uE001], [more, \uE002], [refresh, \uE003], [download, \uE004] ]) Component struct RcIcon { Prop iconKey: string Prop color: string #666666 Prop fontSize: number 16 build() { Text(ICON_FONT_UNICODE.get(this.iconKey) || \uE000) .fontFamily($r(app.media.icon_font)) .fontSize(this.fontSize) .fontColor(this.color) } }4.2 状态图标与主题联动的处理方式列表里的图标往往有两种状态普通态和激活态。最常见的场景是收藏按钮未收藏是一个空心心形已收藏是一个实心心形且颜色不同。如果每个状态单独放一个字体图标映射表规模会翻倍。RcList 的做法是同一个图标只存一个实体用颜色和透明度的变化来区分状态。具体来说普通态用统一的基础色比如 #CCCCCC激活态用主题色再配合 1.0 和 0.6 的透明度变化。这样只用一套图标实体就能覆盖四种状态组合。如果某些图标必须换形状比如空心变实心才单独配置第二套图标。这种方式在列表场景中已经足够而且视觉一致性更好。主题联动方面HarmonyOS 6 支持深色模式和浅色模式切换。RcList 通过读取系统的颜色模式自动切换图标的基础色。这里要提一个坑直接用系统的主题颜色值会导致图标在不同页面上颜色不一致。我采取的方案是定义一组语义化颜色 token比如列表图标-默认对应浅色下的 #666666 和深色下的 #999999在不同的 theme 资源文件里分别定义图标组件统一引用 token。这样不管系统怎么切列表里图标颜色始终协调。4.3 图标的异步加载与缓存虽然字体图标比位图轻量但在列表复杂场景下图标的渲染也得优化。字体文件如果超过几百 KB在首帧就加载会拖慢页面启动。RcList 在页面生命周期里提前预加载图标字体并通过 Font.registerFont 注册后续 Text 组件使用这个注册名时字体数据已经在内存中了不会造成卡顿。对于少数仍然使用位图的自定义图标比如运营活动的插画类图标走的是缩略图类似的缓存链路只是解码尺寸定为图标显示尺寸的 2 倍并且内存缓存单独划一块避免和列表缩略图互相挤占。这里有一个容易忽略的点自定义位图图标在深色模式下需要替换成深色版本。RcList 的映射表支持按颜色模式返回不同资源浅色模式加载 sku_a_light.png深色模式加载 sku_a_dark.png。这个分支逻辑在资源目录里用限定符即可实现不需要在业务代码里做判断。5. 实测数据与打磨过程中遇到的典型问题组件最终要拿数据说话。RcList 在真机上做了两轮专项测试一轮是性能压测一轮是稳定性测试。我把关键数据和几个印象最深的问题整理出来。5.1 性能压测滚动帧率与内存占用测试设备是麒麟芯片的两款 HarmonyOS 6 手机一高一中档位。测试场景是信息流列表每屏显示 8 条数据每条数据包含一张 160x120 缩略图、一个数字角标、文件类型图标和操作图标。滑动场景下的帧率在无缩略图加载时不掉帧稳定 60fps快速滚动时由于有防抖策略图片不在滚动期间解码帧率也保持在 55fps 以上。滚动停止后等待队列里的缩略图开始补齐这时候能观察到帧率短暂降到 45fps 左右但持续时间不超过 300ms。这个表现比较符合预期可视区域内的体验是流畅的。内存占用打开列表 10 分钟持续上下滑动应用内存稳定在 350MB 左右其中缩略图内存缓存 64MB磁盘缓存由系统管理。整个过程没有出现内存持续增长LRU 逐出策略生效稳定。这里要特别说明HarmonyOS 6 的 DevEco Studio 自带的内存分析工具非常有用可以查看到每个 PixelMap 的存活情况和占用大小。我一度发现内存占用不断上涨排查了很久才发现是磁盘缓存读取后没有及时释放 ImageSource 对象导致底层解码器一直持有内存。这个问题的排查过程很有价值。5.2 三个印象深刻的坑与解决过程第一个坑磁盘缓存文件名冲突。最初磁盘缓存直接用 URL 做文件名校验结果 URL 里带了特殊字符在沙箱目录里创建文件失败。后来改成 MD5 哈希作为文件名冲突问题解决。但随之出现了新问题不同 URL 可能哈希碰撞虽然概率极低但一旦发生用户看到的缩略图就是错的。最终方案是文件名用URL 哈希 文件大小校验读取时再校验图片的实际宽高是否匹配期望值不匹配就废弃。第二个坑角标与缩略图的圆角冲突。缩略图设置了圆角裁剪之后角标的锚点如果放在缩略图的左上角圆角会让角标的背景露出一小块空隙。最初用绝对坐标偏移解决但不同设备上圆角半径不同偏移量很难统一。后来改成角标定位相对于缩略图的裁剪区域而不是原始边界组件内部先获取实际裁剪路径的包围盒再计算角标坐标。这样任何圆角半径都不会出问题。第三个坑图标字体在低端机上的首帧白屏。字体文件首次加载时如果直接在主线程同步读取几百 KB 的文件会导致页面白屏 200ms 以上。解决方式是把字体加载改到异步线程在字体加载完成前先显示一个占位色块。等字体加载完成再触发一次局部刷新。这个方案的代价是首次打开页面图标会闪一下但相比白屏体验已经好很多了。后续优化方向是把字体文件直接打进 App 包通过资源管理器同步访问就彻底不存在这个问题了。6. 半年沉淀下来的组件化设计心得最后聊点框架层面的东西。RcList 做了半年技术上最值钱的部分不是某个功能点而是把组件如何设计才能长期可维护这个问题想清楚了。有几点体会很值得分享。第一组件内部一定要分层。缩略图、角标、图标系统虽然是三个独立模块但它们在渲染层有共同的基础设施比如统一的主题 token、统一的缓存失效策略、统一的事件回调机制。把这些基础设施抽出来作为公共层三个模块各自实现自己的业务逻辑互不感知。这个设计在后续迭代中帮了大忙比如深色模式新增一套颜色配置只改公共层三个模块自动适配。第二给业务方暴露的配置项要克制。RcList 刚做第一版时我把所有参数都开放出来了角标偏移量、圆角半径、加载动画时长、缓存大小全部可配。结果就是调用代码非常冗长大家都在复制粘贴配置。后来我砍掉了一半以上的配置项把高频使用的场景固化成默认值只保留真正需要差异化的参数比如角标类型、显示数字、图标 key。这个做减法的过程让 RcList 的 API 清晰了一倍接入成本也大幅下降。第三稳定优先于炫技。几个月里有很多次我想往 RcList 里加一些高级交互比如列表项拖拽、缩略图手势缩放、角标堆叠动画但冷静评估后很多功能在真实业务里用不上反而会增加维护成本。最终 RcList 的定位非常明确把列表里最基础的体验做到极致而不是做成一个全家桶。缩略图加载做到秒开、角标不错位、图标跟主题同步这三个基本盘做好用户和业务方就都满意了。RcList 在 HarmonyOS 6 上的这套实现放在整个大前端领域看不算多新颖的技术但在鸿蒙生态里每一项能力的落地方式都带上了平台特性。比如 PixelMap 的内存管理方式、ImageSource 的增量解码行为、字体注册的时机都是在这个平台上才有的问题排查和解决的思路也完全是鸿蒙式的。如果你也在做 HarmonyOS 组件开发希望这篇文章里的路径和坑位能帮你缩短一些摸索时间。