HarmonyOS实战开发系列课程

egwegerhtyf · · 113 次点击 · · 开始浏览    

获课地址:666it.top/14013/ 性能优化利器——攻克长列表滑动卡顿,打造极致流畅体验 在移动应用开发中,性能是决定用户体验的生命线。一个功能再丰富的应用,如果伴随着卡顿、延迟和高耗电,也难以赢得用户的青睐。特别是当应用需要展示大量数据,如新闻列表、商品流、或者我们“在线答题”应用中的历史记录列表时,长列表的滑动流畅度便成为了一个巨大的挑战。本篇文章将直面这一痛点,深入探讨HarmonyOS的性能优化机制,特别是针对长列表场景的优化策略。我们将学习如何使用LazyForEach进行数据懒加载,以及如何利用组件复用(@Reusable)机制,从根本上解决滑动卡顿问题,为用户打造极致流畅的交互体验。 在优化之前,我们必须先理解问题的根源。当使用ForEach渲染一个包含成百上千条数据的长列表时,如果一次性将所有数据对应的UI组件全部创建出来,将会带来两个致命问题:第一,UI线程(主线程)会因为需要处理大量的组件创建、布局和绘制任务而阻塞,导致界面掉帧,用户感知到明显的卡顿;第二,这些组件会占用大量内存,可能导致应用内存溢出(OOM)或系统频繁进行垃圾回收,进一步加剧卡顿。HarmonyOS的性能专区文档指出,长列表滑动卡顿的根本原因在于“网络数据加载与UI组件频繁创建与销毁”带来的巨大压力。 为了解决这一问题,HarmonyOS提供了两个核心的优化武器:LazyForEach和组件复用。 LazyForEach,顾名思义,是“懒”的ForEach。它与ForEach最大的区别在于,它不会一次性为所有数据创建子组件。相反,它只创建当前屏幕可视区域内以及少量缓冲区内的组件。当用户滑动列表时,LazyForEach会动态地根据新进入可视区域的数据项创建组件,并回收那些滑出可视区域的组件。这种按需创建的模式,极大地减少了同一时间内存中的组件数量,降低了主线程的瞬时压力,从而保证了滑动的流畅性。 让我们来改造一下代码,假设我们要在ResultPage中展示一个用户的历史答题记录列表,这个列表可能非常长。首先,我们需要从云数据库查询历史记录,然后使用LazyForEach来渲染。 // ResultPage.ets import { DatabaseService } from '../service/DatabaseService'; import { AuthService } from '../service/AuthService'; // ... 历史记录的数据模型 interface HistoryRecord { questionStem: string; userAnswer: string; correctAnswer: string; isCorrect: boolean; timestamp: string; } @Entry @Component struct ResultPage { @State score: number = 0; @State totalQuestions: number = 0; @State historyRecords: HistoryRecord[] = []; // LazyForEach需要的数据源,必须实现IDataSource接口 private historyDataSource: BasicDataSource = new BasicDataSource([]); aboutToAppear() { const params = router.getParams() as Record<string, number>; this.score = params.score || 0; this.totalQuestions = params.totalQuestions || 0; this.loadHistoryRecords(); } private async loadHistoryRecords() { const userId = AuthService.getInstance().getCurrentUserId(); if (userId) { // 假设DatabaseService有查询历史记录的方法 const records = await DatabaseService.getInstance().queryHistoryRecords(userId); this.historyRecords = records; // 更新LazyForEach的数据源 this.historyDataSource.updateData(records); } } build() { Column() { // ... 原有的得分展示UI // 历史记录列表 List({ space: 10 }) { LazyForEach(this.historyDataSource, (item: HistoryRecord, index: number) => { ListItem() { this.HistoryItem(item) } }, (item: HistoryRecord, index: number) => `history_${item.timestamp}`) // keyGenerator } .listDirection(Axis.Vertical) .width('100%') .layoutWeight(1) // 占据剩余空间 .margin({ top: 20 }) } .width('100%') .height('100%') .backgroundColor('#FFFFFF') } // 历史记录项的自定义组件 @Builder HistoryItem(record: HistoryRecord) { Row() { Column() { Text(record.questionStem) .fontSize(16) .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) Row() { Text(`你的答案: ${record.userAnswer}`) .fontSize(14) .fontColor(record.isCorrect ? '#34C759' : '#FF3B30') Text(`正确答案: ${record.correctAnswer}`) .fontSize(14) .fontColor('#666666') .margin({ left: 15 }) } .margin({ top: 5 }) } .alignItems(HorizontalAlign.Start) .layoutWeight(1) Text(record.timestamp) .fontSize(12) .fontColor('#999999') } .width('90%') .padding(15) .backgroundColor('#F1F3F5') .borderRadius(8) .justifyContent(FlexAlign.SpaceBetween) } } // 实现IDataSource接口的基础数据源类 class BasicDataSource implements IDataSource { private listeners: DataChangeListener[] = []; private data: HistoryRecord[] = []; constructor(data: HistoryRecord[]) { this.data = data; } totalCount(): number { return this.data.length; } getData(index: number): HistoryRecord { return this.data[index]; } registerDataChangeListener(listener: DataChangeListener): void { if (this.listeners.indexOf(listener) < 0) { this.listeners.push(listener); } } unregisterDataChangeListener(listener: DataChangeListener): void { const pos = this.listeners.indexOf(listener); if (pos >= 0) { this.listeners.splice(pos, 1); } } // 通知数据变化 notifyDataReload(): void { this.listeners.forEach(listener => { listener.onDataReloaded(); }); } // 更新数据的方法 updateData(data: HistoryRecord[]): void { this.data = data; this.notifyDataReload(); } } 在这段代码中,我们创建了一个BasicDataSource类,它实现了IDataSource接口。这是使用LazyForEach的必要条件,LazyForEach通过这个接口来获取数据总数、指定索引的数据,并注册数据变化监听器。当loadHistoryRecords从云端获取到数据后,它调用updateData方法更新数据源,数据源会通知所有注册的监听器(即LazyForEach)数据已刷新,LazyForEach便会重新渲染列表。 仅仅使用LazyForEach,虽然解决了组件数量过多的问题,但在快速滑动时,组件的频繁创建和销毁仍然会带来一定的性能开销。为了进一步压榨性能,HarmonyOS提供了组件复用机制,通过@Reusable装饰器来标记一个自定义组件,告诉系统这个组件是可以被复用的。 当一个被@Reusable标记的组件从列表中滑出(即将被销毁)时,它不会被立即销毁,而是被放入一个“回收池”中。当列表需要创建一个新的同类组件时,系统会优先从回收池中取出一个缓存的组件,而不是重新创建。开发者只需要在组件中实现aboutToReuse生命周期回调,在这个回调中根据新的数据更新组件的状态即可。 让我们将HistoryItem组件改造为可复用的。 // 在ResultPage.ets中 // 可复用的历史记录项组件 @Reusable @Builder HistoryItem(record: HistoryRecord) { HistoryItemView({ record: record }) } } // 定义一个独立的组件结构体,以便使用@Reusable @Component struct HistoryItemView { @Prop record: HistoryRecord; // 使用@Prop接收来自父组件的数据 aboutToReuse(params: Record<string, HistoryRecord>) { // 当组件被复用时,会调用此方法 // 使用新的数据更新@Prop变量 this.record = params.record; } build() { // ... 原有的HistoryItem的build内容 Row() { // ... UI代码 } // ... 样式代码 } } 这里我们做了一些调整。@Reusable装饰器需要用在struct定义的组件上,而不是@Builder方法上。因此,我们创建了一个新的HistoryItemView组件,并用@Reusable和@Component标记它。原来的@Builder HistoryItem方法现在只负责创建这个可复用组件的实例。HistoryItemView通过@Prop装饰器接收数据。@Prop是单向数据传递,当父组件的数据变化时,子组件会更新。关键在于aboutToReuse方法,当组件从回收池中被取出复用时,LazyForEach会将新的数据项通过params传递进来,我们在aboutToReuse中用这个新数据更新this.record,从而完成组件状态的刷新。 通过LazyForEach懒加载和@Reusable组件复用这两板斧的组合,我们构建了一个高性能的长列表。根据HarmonyOS性能专区的真实案例,优化前滑动卡顿率高达14.14ms/s,而采用这两种优化策略后,卡顿率能降至5ms/s以内,性能提升效果显著。这意味着用户在滑动我们的历史记录列表时,将感受到如丝般顺滑的体验,几乎感觉不到任何延迟。 性能优化是一个持续的过程,除了针对特定场景的专项优化,我们还应遵循一些通用的最佳实践,例如:避免在build方法中进行耗时计算、减少不必要的状态变量、合理使用@Watch监听状态变化、以及利用DevEco Studio提供的Profiler工具进行性能分析和瓶颈定位。HarmonyOS开发者官网的“最佳实践-性能专区”就是一个宝库,它系统化地提供了从问题定位到优化落地的全流程解决方案,是开发者攻克性能难关的强大助力。 掌握性能优化,是开发者从“能用”迈向“好用”的关键一步。它体现了对底层原理的深刻理解和对用户体验的极致追求。在HarmonyOS的世界里,系统为我们提供了强大的工具和清晰的指引,让我们能够更从容地应对性能挑战,为用户打造出真正流畅、精美的轻应用。

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

113 次点击  
加入收藏 微博
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传