HarmonyOS应用开发实战:猫猫大作战-如何精准设置缓存数量来平衡内存与滚动流畅度
前言在移动应用开发中长列表是最常见的 UI 形态之一。HarmonyOS 提供了ForEach和LazyForEach两种渲染控制方式——前者一次性创建所有组件适合少量数据后者按需加载适合大量数据的列表场景。本文以「猫猫大作战」的高分排行榜1000 条成绩记录为实战锚点深入对比ForEach与LazyForEach的性能差异详细拆解IDataSource数据源实现、键值生成规则、滚动加载策略以及LazyForEach与Reusable、cachedCount如何组成列表性能优化的“三件套“。提示本系列不讲 ArkTS 基础语法与环境搭建假设你已跟完第 1–67 篇。本篇是阶段二第 68 篇列表性能优化三部曲的第二篇。一、ForEach vs LazyForEach核心差别1.1 渲染流程对比维度ForEach循环渲染LazyForEach懒加载数据加载一次性全量加载按需加载只加载可视区所需组件创建为每条数据创建组件并挂载到组件树只为可视区缓存区的数据创建组件内存占用大所有组件常驻内存小只保持可视区缓存区组件首次加载耗时O(N)N 为总数据量O(M)M 为可视区可见项数适用数据量 100 条100 条以上、甚至数万条配合 cachedCount不支持✅ 支持1.2 性能差距实测以渲染 1000 条「猫猫大作战」排行榜记录为例// ForEach — 一次性全量加载 加载 1000 条数据: 320ms 创建 1000 个组件: 280ms 构建组件树: 180ms 首次渲染耗时: 780ms 页面长时间白屏 内存峰值: 42MB // LazyForEach — 按需加载每屏约 10 条 加载 10 条数据: 3ms 创建 10 个组件: 3ms 构建组件树: 2ms 首次渲染耗时: 8ms 瞬间展示 内存峰值: 4MB 内存只有 ForEach 的 1/101.3 何时选 ForEach// ✅ 适合 ForEach固定且少于 100 项的列表 State gameLevels: Level[] [ { id: 1, name: 新手村 }, { id: 2, name: 猫咪森林 }, { id: 3, name: 合并峡谷 }, // ... 总共 50 个关卡 ]; build() { List() { ForEach(this.gameLevels, (level: Level) { ListItem() { Text(level.name) } }, (level: Level) level.id.toString()) } }// ✅ 适合 LazyForEach排行榜、消息列表、动态流 ≥ 100 条 State records: IDataSource new LeaderboardDataSource(); // 1000 条 build() { List() { LazyForEach(this.records, (record: GameRecord) { ListItem() { RecordCard({ record: record }) } }, (record: GameRecord) record.id.toString()) } }选型金标准数据量 50 条或数据量不确定 → 默认选LazyForEach。二、IDataSource 接口详解2.1 接口定义LazyForEach的数据源必须实现IDataSource接口该接口定义在kit.ArkUI中interface IDataSource { totalCount(): number; // 数据总量 getData(index: number): Object; // 获取指定索引的数据 registerDataChangeListener(listener: DataChangeListener): void; unregisterDataChangeListener(listener: DataChangeListener): void; }DataChangeListener接口提供了数据变更通知方法interface DataChangeListener { onDataReload(): void; // 全量刷新 onDataAdd(index: number): void; // 新增一条 onDataMove(from: number, to: number): void; // 移动一条 onDataDelete(index: number): void; // 删除一条 onDataChange(index: number): void; // 修改一条 onDataAdd(index: number): void; // 新增旧接口 }2.2 完整实现排行榜数据源// LeaderboardDataSource.ets import { IDataSource, DataChangeListener } from kit.ArkUI; export class GameRecord { id: number; rank: number; playerName: string; score: number; date: string; constructor(id: number, rank: number, name: string, score: number, date: string) { this.id id; this.rank rank; this.playerName name; this.score score; this.date date; } } export class LeaderboardDataSource implements IDataSource { private data: GameRecord[] []; private listeners: DataChangeListener[] []; constructor(count: number 1000) { for (let i 0; i count; i) { this.data.push(new GameRecord( i, i 1, 玩家${i 1}, Math.floor(Math.random() * 99999), 2026-07-24 )); } } totalCount(): number { return this.data.length; } getData(index: number): GameRecord { return this.data[index]; } registerDataChangeListener(listener: DataChangeListener): void { if (!this.listeners.includes(listener)) { this.listeners.push(listener); } } unregisterDataChangeListener(listener: DataChangeListener): void { const idx this.listeners.indexOf(listener); if (idx 0) { this.listeners.splice(idx, 1); } } // ---- 数据变更方法 ---- // 末尾追加新记录 addRecord(record: GameRecord): void { this.data.push(record); const insertIndex this.data.length - 1; // 通知所有监听器数据已新增 this.listeners.forEach(l l.onDataAdd(insertIndex)); } // 删除指定记录 deleteRecord(index: number): void { this.data.splice(index, 1); this.listeners.forEach(l l.onDataDelete(index)); } // 更新指定记录 updateRecord(index: number, record: GameRecord): void { this.data[index] record; this.listeners.forEach(l l.onDataChange(index)); } // 全量刷新如从服务器拉取新数据 reloadRecords(records: GameRecord[]): void { this.data records; this.listeners.forEach(l l.onDataReload()); } }2.3 增量更新 vs 全量更新更新方式方法性能适用场景增量新增onDataAdd(index)✅ 仅新建一个组件追加新战绩增量删除onDataDelete(index)✅ 仅删除一个组件删除误录记录增量修改onDataChange(index)✅ 仅刷新指定项更新排名变化批量移动onDataMove(from, to)✅ 仅调整两项位置排行榜重排全量刷新onDataReload()⚠️ 重建所有可见组件从服务器重新拉取// 错误粗暴方式直接替换整个数据源 this.records newDataSource; // ❌ 触发 LazyForEach 重建全部组件 // 正确增量方式使用 IDataSource 的变更通知 dataSource.addRecord(newRecord); // ✅ 只创建一个新的 ListItem dataSource.updateRecord(0, updatedRecord); // ✅ 只刷新第 0 项三、键值生成策略3.1 keyGenerator 的重要性LazyForEach的第三个参数keyGenerator决定了 ArkUI 如何追踪列表项的身份LazyForEach( this.dataSource, // 数据源 (item: GameRecord) { /* ... */ }, // 组件生成函数 (item: GameRecord) item.id.toString() // 键值生成函数 )keyGenerator 实现效果建议item.id.toString()✅ 唯一且稳定强烈推荐item item.playerName⚠️ 可能重复不推荐JSON.stringify(item) 性能差 每次换新key禁止使用item Math.random() 每帧都重建组件绝对禁止3.2 JSON.stringify 的陷阱// 错误key 生成器中使用 JSON.stringify LazyForEach(this.records, (item) { ListItem() { RecordCard({ record: item }) } }, (item) JSON.stringify(item)) // ❌ 每次渲染 key 都不同为什么不行JSON.stringify对大型对象序列化耗时在滑动时频繁调用导致卡顿数据对象即使内容相同但引用不同时key 也会变化导致 LazyForEach 认为“全是新数据“重建所有组件// ✅ 正确使用稳定且唯一的 id LazyForEach(this.records, (item) { ListItem() { RecordCard({ record: item }) } }, (item) item.id.toString()) // ✅ 唯一且持久的 key3.3 key 生成规则总结正确 key 的三大原则 1. 唯一性同一数据在不同渲染周期中 key 相同 2. 稳定性数据内容不变时 key 不变 3. 高效性生成 key 的计算开销极小最好只是一个属性访问四、三件套组合Reusable LazyForEach cachedCount4.1 为什么需要三件套单独使用LazyForEach虽然实现了按需加载但快速滑动时仍然存在两个问题白块问题滑动太快新组件来不及创建创建开销每次划入都重新创建组件仍有一定耗时三件套各司其职技术解决问题效果LazyForEach避免全量创建首屏秒开cachedCount预先生成附近组件滑动无白块Reusable复用滑出组件创建零开销4.2 三件套完整代码import { IDataSource, DataChangeListener } from kit.ArkUI; Entry Component struct LeaderboardPage { private dataSource: LeaderboardDataSource new LeaderboardDataSource(10000); // 1万条数据 build() { Column() { // 标题栏 Text( 全球排行榜) .fontSize(24) .fontWeight(FontWeight.Bold) .padding(16) // 三件套组合LazyForEach cachedCount Reusable List({ space: 8 }) { LazyForEach(this.dataSource, (item: GameRecord) { ListItem() { RecordCard({ record: item }) // 已在第 67 篇中标记 Reusable } }, (item: GameRecord) item.id.toString()) } .cachedCount(10) // 预加载上下各 10 个 .width(100%) .layoutWeight(1) .backgroundColor(#F5F6FA) } .height(100%) } } // 已在第 67 篇标记 Reusable 的复用组件 Reusable Component struct RecordCard { Prop record: GameRecord new GameRecord(); aboutToReuse(params: Recordstring, Object) { // 复用时的数据更新由 Prop 自动完成 } build() { Row() { Text(#${this.record.rank}) .width(45) .fontSize(16) .fontWeight(FontWeight.Bold) .textAlign(TextAlign.Center) Text(this.record.playerName) .layoutWeight(1) .fontSize(16) .margin({ left: 8 }) Text(this.record.score.toString()) .fontSize(18) .fontWeight(FontWeight.Bold) .fontColor(#2ECC71) } .padding({ left: 12, right: 12, top: 10, bottom: 10 }) .backgroundColor(#FFFFFF) .borderRadius(10) .shadow({ radius: 2, color: rgba(0,0,0,0.05), offsetY: 1 }) .width(100%) } }4.3 性能对比// 1 万条数据排行榜滚动性能 指标 | ForEach | LazyForEach | LazyForEachcachedCountReusable --------------------|-------------|-------------|---------------------------------- 首次渲染耗时 | 5s (崩溃) | 8ms | 8ms 滑动帧率 (低端机) | 无法运行 | 35fps | 58fps 峰值内存 | — | 12MB | 8MB 组件节点数 | 10000 | 12 | 32 (10 可见 20 缓存) GC 暂停频率 | — | 频繁 | 几乎为 0 白块现象 | — | 快速滑动有 | 无五、项目实战排行榜动态排名更新5.1 场景说明排行榜需要实时更新——玩家每局结束得分可能会超越其他人排名需要重新排序。使用IDataSource的增量更新方法只刷新变化的部分。5.2 实现代码// 玩家完成一局后更新排行榜 function submitNewScore(playerName: string, newScore: number) { // 1. 找到玩家现有记录 const existingIndex dataSource.findIndexByPlayer(playerName); if (existingIndex 0) { // 2. 更新得分 const oldRecord dataSource.getData(existingIndex); oldRecord.score newScore; // 3. 重新排序并通知 dataSource.reSortByScore(); // 4. 通知 LazyForEach 全量刷新因为排名顺序变了 dataSource.reloadRecords(dataSource.getAllData()); } else { // 新玩家追加记录 const newRecord new GameRecord( nextId, 1000, playerName, newScore, 2026-07-24 ); dataSource.addRecord(newRecord); } }在实际项目中可以使用onDataMove和onDataChange实现更精细的增量更新而非全量reloadRecords// LeaderboardDataSource.ets — 精细增量更新 moveRecord(fromIndex: number, toIndex: number): void { const [moved] this.data.splice(fromIndex, 1); this.data.splice(toIndex, 0, moved); this.listeners.forEach(l l.onDataMove(fromIndex, toIndex)); } updateScoreAndRank(playerIndex: number, newScore: number): void { const oldRank this.data[playerIndex].rank; this.data[playerIndex].score newScore; // 重排 this.data.sort((a, b) b.score - a.score); this.data.forEach((r, i) r.rank i 1); // 只通知变更不是全量 reload const newIndex this.data.findIndex(r r.id this.data[playerIndex].id); if (newIndex ! playerIndex) { this.moveRecord(playerIndex, newIndex); // 移动 } this.listeners.forEach(l l.onDataChange(newIndex)); // 刷新 }六、LazyForEach 在 Grid 和 WaterFlow 中使用6.1 Grid 中的 LazyForEachGrid() { LazyForEach(this.catsDataSource, (cat: CatConfig) { GridItem() { Image(cat.icon) .width(80) .height(80) } }, (cat: CatConfig) cat.id.toString()) } .columnsTemplate(1fr 1fr 1fr) // 三列 .rowsTemplate(1fr 1fr 1fr 1fr) .cachedCount(6) // 缓存 6 个6.2 WaterFlow 中的 LazyForEachWaterFlow() { LazyForEach(this.flowDataSource, (item: MediaItem) { FlowItem() { VideoCard({ video: item }) } }, (item: MediaItem) item.id.toString()) } .columnsTemplate(1fr 1fr) .cachedCount(8)在 Scroll LazyVGridLayout/LazyVWaterFlowLayout 中的使用方式类似详见第 69 篇 cachedCount。七、LazyForEach 与 V2 状态管理在 V2 模式下LazyForEach的使用方式基本相同但数据源中的对象需要用ObservedV2Trace装饰ObservedV2 class GameRecordV2 { Trace id: number 0; Trace rank: number 0; Trace playerName: string ; Trace score: number 0; } ComponentV2 struct RecordCardV2 { Param record: GameRecordV2 new GameRecordV2(); // ... }V2 注意LazyForEach的键值生成器在 V2 中同样遵循唯一且稳定的原则。八、常见踩坑8.1 坑一keyGenerator 返回不唯一的 key// 错误以排名为 key排名会变 LazyForEach(this.records, (item) { ListItem() { RecordCard({ record: item }) } }, (item) item.rank.toString()) // ❌ 排名变化时key 变化组件重建后果排行榜重排后所有组件的 key 都变了LazyForEach 会销毁所有旧组件、创建新组件相当于全量刷新。解决用不变的唯一标识如数据库自增 id(item) item.id.toString() // ✅ id 永不变8.2 坑二IDataSource 不通知变更// 错误修改数据但不通知 this.dataSource.data[0].score 99999; // ❌ LazyForEach 不知道数据变了UI 不刷新 // ✅ 正确通过接口通知 this.dataSource.updateRecord(0, updatedRecord);8.3 坑三列表项高度频繁变化当 LazyForEach 的列表项高度在渲染过程中频繁变化时会导致cachedCount的预加载数量不足出现白块。解决方法给列表项设置确定的高度或constraintSize。九、最佳实践清单数据量 50 条时默认使用 LazyForEachkeyGenerator 使用唯一且稳定的 id禁止 JSON.stringify总是配合 cachedCount Reusable三件套使用数据变更通过IDataSource 的增量通知禁止全量替换优先使用onDataAdd/onDataDelete/onDataChange而非onDataReload列表项高度尽量固定避免动态高度导致缓存不足LazyForEach Scroll 做混合布局时Scroll 方向必须为VerticalaboutToReuse 中不做耗时操作十、总结LazyForEach是 HarmonyOS 处理大数据量列表的核心渲染控制手段与Reusable、cachedCount组成列表性能优化的“三件套“——按需加载解决首屏速度预缓存解决滑动白块组件复用解决创建开销。核心要点ForEach全量加载适合 50 条LazyForEach按需加载适合 50 条IDataSource负责数据提供和变更通知增量更新优于全量刷新keyGenerator使用唯一 id禁止JSON.stringify和Math.random三件套组合LazyForEachcachedCount(N)Reusable下一篇预告第 69 篇将深入cachedCount— 预加载缓存策略讲解如何精准设置缓存数量来平衡内存与滚动流畅度。如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力相关资源HarmonyOS LazyForEach 官方文档LazyForEach API 参考懒加载优化最佳实践IDataSource 接口参考开源鸿蒙跨平台社区第 67 篇Reusable 组件复用第 69 篇cachedCount 预加载第 70 篇IDataSource 自定义数据源

相关新闻

H3C交换机远程镜像实现同一源端口,多个目的端口

H3C交换机远程镜像实现同一源端口,多个目的端口

要将G1/0/2 的流量镜像到 G1/0/47口 这个倒没啥,用普通的local镜像就可以 mirroring-group 1 local mirroring-group 1 mirroring-port G1/0/2 mirroring-group 1 monitor-port G1/0/47但是我还有另1台分析设备,接在G1/0/48口,也需要收同样的…

2026/7/28 0:27:52 阅读更多 →
HarmonyOS应用开发实战:猫猫大作战-JSON stringify/parse、Protobuf 紧凑二进制、JSON vs Protobuf

HarmonyOS应用开发实战:猫猫大作战-JSON stringify/parse、Protobuf 紧凑二进制、JSON vs Protobuf

前言 前面我们用 HTTP 拉排行榜、上报得分——服务器返回的是字符串,本地 state 是对象,两者间要序列化/反序列化转换。HarmonyOS 默认用 JSON(文本格式,可读易调试),但移动端省流量省电场景可选 Protobuf…

2026/7/28 0:27:52 阅读更多 →
别再用剪映AI了!抖音官方认证的5款企业级AI视频工具对比(含API调用成本/审核通过率/商用授权红线)

别再用剪映AI了!抖音官方认证的5款企业级AI视频工具对比(含API调用成本/审核通过率/商用授权红线)

更多请点击: https://intelliparadigm.com 第一章:别再用剪映AI了!抖音官方认证的5款企业级AI视频工具对比(含API调用成本/审核通过率/商用授权红线) 抖音生态正加速向企业级AI视频生产体系演进。2024年Q2起&#xff…

2026/7/28 0:27:52 阅读更多 →

最新新闻

[具身智能-671]:亚博 STM32 机器人控制板 + 电机就可以构建一个运动机器人,可以不需要主控,如 X5?

[具身智能-671]:亚博 STM32 机器人控制板 + 电机就可以构建一个运动机器人,可以不需要主控,如 X5?

结论先说清楚硬件上:亚博这块 STM32 控制板 编码器电机 锂电池,确实可以构成一台能运动的机器人,不强制需要 RDK X5 / Jetson 这类 Linux 主控。 但是要严格区分两种工作模式,能力上限差距巨大:一、模式 1&#xff1…

2026/7/28 0:34:54 阅读更多 →
如何快速使用League Akari:英雄联盟本地自动化工具完整指南

如何快速使用League Akari:英雄联盟本地自动化工具完整指南

如何快速使用League Akari:英雄联盟本地自动化工具完整指南 【免费下载链接】League-Toolkit An all-in-one toolkit for LeagueClient. Gathering power 🚀. 项目地址: https://gitcode.com/gh_mirrors/le/League-Toolkit 你是否曾在英雄联盟游戏…

2026/7/28 0:34:54 阅读更多 →
HarmonyOS应用开发实战:猫猫大作战-onKeyEvent 三阶段触发、KeyCode 判定具体键、KeyEventSource 判定输入源、与

HarmonyOS应用开发实战:猫猫大作战-onKeyEvent 三阶段触发、KeyCode 判定具体键、KeyEventSource 判定输入源、与

前言 前面我们用 onTouch 处理手势、onHover 处理悬停——但都是「指针」类输入。还有种「按键」类输入:PC 键盘(WASD/方向键/空格)、TV 遥控器(方向键/确认/返回)、手机外接手柄/键盘。这类输入不走触摸/悬停&#x…

2026/7/28 0:33:54 阅读更多 →
HarmonyOS应用开发实战:猫猫大作战-被动批量(同帧)、主动批量(batchUpdate)、跨帧批量的陷阱、批量与深观察的协同

HarmonyOS应用开发实战:猫猫大作战-被动批量(同帧)、主动批量(batchUpdate)、跨帧批量的陷阱、批量与深观察的协同

前言 第 32 篇我们讲过「同帧批量更新」——一个回调里改多个 State,ArkUI 合并成一次重渲染。但那是「被动批量」——靠回调天然同帧。实战中还有「主动批量」场景:连续多次逻辑操作改 state,要强制合并成一次重渲染,避免中途触…

2026/7/28 0:33:54 阅读更多 →
HarmonyOS应用开发实战:猫猫大作战-秒级计时器周期、gameTime 递增与格式化、暂停不计时间、计时器与主循环的分工

HarmonyOS应用开发实战:猫猫大作战-秒级计时器周期、gameTime 递增与格式化、暂停不计时间、计时器与主循环的分工

前言 上一篇我们搭好了 100ms 物理主循环——猫咪下落、合并、得分都靠它驱动。但游戏里还有个独立时钟:从开局到结束的累计时间(gameTime)。这个时钟和主循环分离——主循环 100ms 更新物理,计时器 1000ms 递增秒数,…

2026/7/28 0:33:54 阅读更多 →
不懂乐理也能创作商用歌曲?MELO 音乐生成实战:手把手教你打造版权自有神曲

不懂乐理也能创作商用歌曲?MELO 音乐生成实战:手把手教你打造版权自有神曲

前言:在这个时代,创意是唯一的门槛 如果我告诉你,现在创作一首发行级音质、包含人声演唱、编曲完整、且完整版权完全属于你的歌曲,只需要你会打字、会拍照、甚至会随口哼一段调调,你相信吗? 不需要你买昂…

2026/7/28 0:33:54 阅读更多 →

日新闻

告别臃肿!3步让你的暗影精灵笔记本重获新生

告别臃肿!3步让你的暗影精灵笔记本重获新生

告别臃肿!3步让你的暗影精灵笔记本重获新生 【免费下载链接】OmenSuperHub Control Omen laptop performance, fan speeds, and keyboard lighting, and unlock power limits. 项目地址: https://gitcode.com/gh_mirrors/om/OmenSuperHub 你是否也曾为官方Om…

2026/7/28 0:00:43 阅读更多 →
RAG必踩坑!财报法规检索不准?这款开源工具让答案浮出水面,准确率飙升98.7%!

RAG必踩坑!财报法规检索不准?这款开源工具让答案浮出水面,准确率飙升98.7%!

做 RAG 的人应该都踩过这个致命的坑:把几百页的财报、法规、技术手册扔给向量库,问一个具体问题,搜出来的全是沾边但没用的内容 —— 关键信息要么被硬切块拆碎了,要么藏在几十条结果的最下面。语义相似≠真正相关,这个…

2026/7/28 0:00:43 阅读更多 →
抖音视频文案提取工具全指南:免费2026版、手机App、在线工具一网打尽

抖音视频文案提取工具全指南:免费2026版、手机App、在线工具一网打尽

2026年做短视频运营,从抖音上扒文案早就不是偷偷抄笔记的事了。我刚开始做内容的时候,每天刷半小时抖音,手动把爆款视频的口播敲进备忘录,一条2分钟的视频得花十来分钟,碰到语速快的还要反复回听。后来试了一圈工具&am…

2026/7/28 0:00:43 阅读更多 →

周新闻

深度学习道路桥梁裂缝检测系统 道路桥梁裂缝检测数据集 道路桥梁病害识别检测数据集

深度学习道路桥梁裂缝检测系统 道路桥梁裂缝检测数据集 道路桥梁病害识别检测数据集

深度学习道路桥梁裂缝检测系统 数据集6000张 完整源码已标注数据集训练好的模型环境配置教程程序运行说明文档,可以直接使用!系统支持图片、视频、摄像头等多种方式检测裂缝,功能强大实用。 1数据集6000张 8各类别

2026/7/27 4:33:59 阅读更多 →
深度学习YOLO模型如何训练 PUBG 绝地求生目标检测数据集

深度学习YOLO模型如何训练 PUBG 绝地求生目标检测数据集

pubg数据集 精选原图1.42万数据 1.49万标签 无任何重复、算法增强或冗余图像! pubg绝地求生目标检测数据集 1分类:e_body,14905个标签,txt格式 共计14244张图,99%为640*640尺寸图像 适合yolo目标检测、AI训练关键词&am…

2026/7/27 6:31:56 阅读更多 →
Apex英雄目标检测数据集 深度学习框架YOLO如何训练APEX数据集

Apex英雄目标检测数据集 深度学习框架YOLO如何训练APEX数据集

Apex检测数据集数据集详情检测类别: allies enemy tag图片总量:7247张训练集:5139张验证集:1425张测试集:683张标注状态:全部已标注,即拿即用数据格式:支持YOLO格式及其他格式&#…

2026/7/27 4:01:12 阅读更多 →

月新闻