鸿蒙 APP 还是卡顿?API 21 性能优化这 3 招,立竿见影!

Hello,兄弟们,我是 V 哥!

昨天有个粉丝在群里哭诉:"V 哥,我用鸿蒙 API 21 写的 App,在模拟器上跑得像法拉利,一到真机老款机型上,划一下屏幕顿两下,简直像在开拖拉机!产品经理都快把我的键盘砸烂了!"

我心想,有没有可能不是手机不行,这是代码没写对呢!

很多兄弟从 Android 或者 Vue 转过来,习惯性地把以前那套"暴力渲染"的逻辑搬到 ArkTS 上。在 API 21 这个新版本上,鸿蒙的渲染引擎虽然强,但你不按它的套路出牌,它照样给你摆烂。

今天,V 哥就掏出压箱底的**"性能三板斧"**。这三招,只要你能消化哪怕一招,你的 App 流畅度立马提升一个档次。咱们直接上 DevEco Studio 6.0 的实战代码,开整!


第一招:长列表别用 ForEach,LazyForEach 才是YYDS

痛点在哪?

很多兄弟写列表,习惯性上 ForEach。V 哥必须提醒你:ForEach 是一次性渲染。如果你的数据有几百条、几千条,它会啪一下一下子把所有组件全创建出来。内存瞬间爆炸,CPU 飙升,卡顿是必然的!

解决方案

API 21 下,必须要用 LazyForEach (懒加载)。它的核心逻辑是:只渲染屏幕可见的那几个 Item,你滑下来一个,我再创建一个,滑上去销毁一个。内存占用极低,丝般顺滑。

代码实战

兄弟们,这部分代码比较经典,建议直接复制到你的 DevEco Studio 里跑一跑。

typescript 复制代码
// 1. 定义基础的数据源接口。这是 LazyForEach 必须要实现的规矩
interface IBasicDataSource {
  totalCount(): number;
  getData(index: number): Object;
  registerDataChangeListener(listener: IDataChangeListener): void;
  unregisterDataChangeListener(listener: IDataChangeListener): void;
}

// 2. 重命名以避免冲突 - 修复第10行错误
interface IDataChangeListener {
  onDataReloaded(): void;
  onDataAdded(index: number): void;
  onDataChanged(index: number): void;
  onDataDeleted(index: number): void;
  onDataMoved(from: number, to: number): void;
}

// 3. 实现数据变化监听器 - 使用新名称
class DataChangeCallback implements IDataChangeListener {
  onDataReloaded(): void {}
  onDataAdded(index: number): void {}
  onDataChanged(index: number): void {}
  onDataDeleted(index: number): void {}
  onDataMoved(from: number, to: number): void {}
}

// 4. 核心数据源类(V哥精简版)
class MyDataSource implements IBasicDataSource {
  private listeners: IDataChangeListener[] = [];
  private dataList: string[] = [];

  constructor(list: string[]) {
    this.dataList = list;
  }

  totalCount(): number {
    return this.dataList.length;
  }

  getData(index: number): Object {
    return this.dataList[index];
  }

  registerDataChangeListener(listener: IDataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener);
    }
  }

  unregisterDataChangeListener(listener: IDataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      this.listeners.splice(pos, 1);
    }
  }

  public addData(data: string) {
    this.dataList.push(data);
    this.notifyDataReloaded();
  }

  private notifyDataReloaded() {
    this.listeners.forEach(listener => {
      listener.onDataReloaded();
    });
  }
}

// 简化数据变更监听器
class SimpleDataChangeCallback extends DataChangeCallback {
  onDataReloaded(): void {
    console.log("数据已重新加载,UI可以刷新");
  }
}

@Entry
@Component
struct LazyForEachDemo {
  @State dataSource: MyDataSource = new MyDataSource([]);

  // 使用新的监听器类型
  private listener: SimpleDataChangeCallback = new SimpleDataChangeCallback();

  aboutToAppear() {
    // 预先生成数据
    const initData: string[] = [];
    for (let i = 0; i < 1000; i++) {
      initData.push('V哥带你飞 - 第 ' + (i + 1) + ' 条数据');
    }
    this.dataSource = new MyDataSource(initData);

    // 注册数据变化监听器
    this.dataSource.registerDataChangeListener(this.listener);
  }

  aboutToDisappear() {
    // 取消注册数据变化监听器
    this.dataSource.unregisterDataChangeListener(this.listener);
  }

  build() {
    Column() {
      // 标题
      Text('LazyForEach 性能演示')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin(10)

      List({ space: 5 }) {
        LazyForEach(
          this.dataSource,
          (item: string, index?: number) => {
            ListItem() {
              this.ListItemChild(item)
            }
          },
          (item: string, index?: number) => {
            // 返回索引作为唯一标识
            if (index === undefined) {
              return Math.random().toString();
            }
            return index.toString();
          }
        )
      }
      .width('100%')
      .height('85%')
      .layoutWeight(1)

      Button('模拟增加数据')
        .onClick(() => {
          this.dataSource.addData('新数据 ' + (this.dataSource.totalCount() + 1));
        })
        .margin(10)
        .width('50%')
    }
    .width('100%')
    .height('100%')
  }

  // 将子组件改为build方法内的组件构建器
  @Builder
  ListItemChild(content: string) {
    Row() {
      Text(content)
        .fontSize(14)
        .flexGrow(1)
        .textAlign(TextAlign.Start)
        .padding(10)
    }
    .width('100%')
    .height(60)
    .backgroundColor('#f0f0f0')
    .borderRadius(8)
    .margin({ left: 10, right: 10, top: 2, bottom: 2 })
  }
}

V 哥划重点:

  1. 千万别懒,一定要实现 IDataSource
  2. LazyForEach 的第三个参数(key生成函数)一定要写,而且要保证唯一性!这是组件复用的身份证,写错了渲染必乱。

第二招:别在主线程算数,TaskPool 帮你搬砖

痛点在哪?

你是不是经常在点击事件里直接写大量逻辑?比如解析巨大的 JSON、图片滤镜处理、复杂算法排序?兄弟,那是主线程(UI线程)啊! 你在那算数,UI 就得等着,屏幕当然卡死不动。

解决方案

API 21 推荐使用 TaskPool(任务池)。把重活累活扔给后台线程池去干,算完了结果一扔,主线程只负责展示。分工明确,效率翻倍。

代码实战

咱们模拟一个"复杂排序"的场景,看 V 哥怎么用 TaskPool 优化。

typescript 复制代码
import taskpool from '@ohos.taskpool';

// 1. 定义一个并发函数(这是在后台线程跑的)
// 注意:@Concurrent 装饰器是必须的,这是 ArkTS 并发编程的标识
@Concurrent
function heavyComputation(data: number[]): number[] {
  // V 哥模拟一个超级耗时的排序操作
  // 比如这里可以换成复杂的 JSON 解析、加密解密等
  let arr = [...data];
  arr.sort((a, b) => a - b);

  // 模拟耗时,让兄弟们看到效果
  let start = new Date().getTime();
  while (new Date().getTime() - start < 500) {
    // 故意卡住 500毫秒,如果在主线程,UI会完全冻结
  }

  console.info("V哥后台线程计算完毕!");
  return arr;
}

@Entry
@Component
struct TaskPoolDemo {
  @State message: string = '点击按钮开始计算';
  @State resultString: string = '结果等待中...';
  @State isCalculating: boolean = false;

  build() {
    Column() {
      Text(this.message)
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20, bottom: 20 })

      if (this.isCalculating) {
        LoadingProgress()
          .width(50)
          .height(50)
          .color(Color.Blue)
      } else {
        Text(this.resultString)
          .fontSize(16)
          .fontColor(Color.Gray)
          .margin({ bottom: 20 })
      }

      Button('使用 TaskPool 后台计算')
        .enabled(!this.isCalculating)
        .onClick(() => {
          this.startCalculation();
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .padding(20)
  }

  // 将计算逻辑提取为独立方法
  private async startCalculation(): Promise<void> {
    this.isCalculating = true;
    this.message = "正在后台拼命算数中...";

    try {
      // 准备一些乱序数据
      let rawData: number[] = [];
      for(let i = 0; i < 10000; i++) {
        rawData.push(Math.random() * 10000);
      }

      // 修复:使用正确的 TaskPool API
      // 方式1:直接执行函数(推荐)
      const result = await taskpool.execute(heavyComputation, rawData) as number[];

      // 计算完成,回到主线程(这里会自动切回来,放心用UI)
      this.isCalculating = false;
      this.message = "计算完成!UI丝滑不卡顿!";
      this.resultString = `前5个数据: ${result.slice(0, 5).join(', ')}`;

    } catch (err) {
      this.isCalculating = false;
      console.error(`V哥报错: ${JSON.stringify(err)}`);
      this.message = "计算失败!";
      this.resultString = `错误信息: ${err}`;
    }
  }
}

这个案例需要真机测试,V 哥使用新入手的MatePad Pro:

以下是单击按钮后运行的结果:

V 哥划重点:

  1. 记得给函数加 @Concurrent,否则扔不进 TaskPool。
  2. TaskPool 是自动管理线程的,你别自己 new Thread,那样太低级且容易OOM。
  3. 记住,UI 只能更新状态,不能做重活

第三招:组件别总造新的,@Reusable 复用才省钱

痛点在哪?

在列表滑动或者页面切换时,如果频繁创建和销毁组件(比如 new ChildComponent()),GC(垃圾回收)压力会非常大,导致内存抖动,表现就是掉帧

解决方案

API 21 提供了一个非常强力的装饰器:@Reusable 。它的作用是:组件不从树上卸载,而是回收到缓存池里,下次需要的时候直接拿过来改个数据接着用。这简直是"物尽其用"的典范!

代码实战

咱们看怎么改造刚才的 ListItemChild 组件。

typescript 复制代码
// 定义一个复用的数据模型,方便传递
class ListItemParams {
  content: string = "";
  color: string = "#ffffff";
}

@Entry
@Component
struct ReusableDemo {
  // 模拟数据
  private dataList: ListItemParams[] = [];

  aboutToAppear() {
    // 在生命周期中初始化数据,避免在构建时执行复杂逻辑
    for (let i = 0; i < 100; i++) {
      let item = new ListItemParams();
      item.content = `可复用组件 Item ${i + 1}`;
      item.color = i % 2 === 0 ? '#e0e0e0' : '#ffffff';
      this.dataList.push(item);
    }
  }

  build() {
    Column() {
      Text('Reusable 组件演示')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin(10)

      List() {
        ForEach(this.dataList, (item: ListItemParams) => {
          ListItem() {
            // 使用我们的复用组件
            ReusableChild({ param: item })
          }
        }, (item: ListItemParams) => item.content + Math.random()) // 唯一Key,避免使用index
      }
      .width('100%')
      .height('90%')
      .layoutWeight(1)
      .scrollBar(BarState.Off)
    }
    .width('100%')
    .height('100%')
  }
}

// 核心重点:可复用组件
@Component
struct ReusableChild {
  // 使用 @Prop 装饰器来接收父组件传递的参数
  @Prop param: ListItemParams;

  // 组件自己的状态
  @State private reuseCount: number = 0;

  /**
   * 生命周期:当组件从缓存池被重新拿出来复用时触发
   * 注意:ArkTS 中正确的复用生命周期是 aboutToReuse
   */
  myAboutToReuse(param: ListItemParams): void {
    // 更新参数
    this.param = param;
    this.reuseCount++;

    console.info(`V哥:组件被复用了!复用次数: ${this.reuseCount}`);
  }

  build() {
    Row() {
      Text(this.param.content)
        .fontSize(16)
        .fontColor(Color.Black)
        .flexGrow(1)

      Blank()

      Column() {
        Text('复用组件')
          .fontSize(10)
          .fontColor(Color.Gray)
        Text(`${this.reuseCount > 0 ? '已复用' : '新建'}`)
          .fontSize(10)
          .fontColor(this.reuseCount > 0 ? Color.Green : Color.Blue)
      }
    }
    .width('100%')
    .height(60)
    .backgroundColor(this.param.color)
    .padding({ left: 15, right: 15 })
    .borderRadius(8)
    .alignItems(VerticalAlign.Center)
  }
}

V 哥划重点:

  1. 加上 @Reusable 装饰符,你的组件就开启了"绿色环保"模式。
  2. 必须实现 aboutToReuse 方法。这是复用组件的灵魂,它决定了你把旧组件拿回来后,怎么给它"洗心革面"(更新数据)。
  3. 配合 LazyForEach 使用,那是绝配,性能起飞!

V 哥总结一下

兄弟们,API 21 的鸿蒙开发,其实就是在跟**"渲染""资源"**打交道。

  • 长列表 ?上 LazyForEach,按需加载。
  • 重任务 ?上 TaskPool,后台多线程。
  • 组件多 ?上 @Reusable,回池复用。

这三招你哪怕只学会了一招,你那个像"拖拉机"一样的 App 也能立马变"法拉利"。V 哥话就撂这儿了,代码都给你整理好了,直接去 DevEco Studio 6.0 里敲一遍,感受一下那种丝滑的快感!

我是 V 哥,咱们下期技术复盘见!别忘了给文章点个赞,这是 V 哥持续输出的动力!👋

相关推荐
威哥爱编程2 小时前
List 组件渲染慢?鸿蒙API 21 复用机制深度剖析,一行代码提速 200%!
harmonyos·arkts·arkui
2501_944521003 小时前
rn_for_openharmony商城项目app实战-语言设置实现
javascript·数据库·react native·react.js·harmonyos
程序猿追3 小时前
【鸿蒙PC桌面端开发】使用ArkTS做出RGB 色环选择器
华为·harmonyos
zhujian826375 小时前
二十五、【鸿蒙 NEXT】@ObservedV2/@Trace实现组件动态刷新
华为·harmonyos·trace·lazyforeach·observedv2
wszy18095 小时前
rn_for_openharmony_空状态与加载状态:别让用户对着白屏发呆
android·javascript·react native·react.js·harmonyos
SameX5 小时前
鸿蒙应用的“任意门”:Deep Linking 与 App Linking 的相爱相杀
harmonyos
AlbertZein5 小时前
HarmonyOS一杯冰美式的时间 -- @Watch 到 @Monitor
harmonyos
奋斗的小青年!!5 小时前
Flutter跨平台开发适配OpenHarmony:下拉刷新组件的实战优化与深度解析
flutter·harmonyos·鸿蒙
lili-felicity6 小时前
React Native for Harmony:订单列表页面状态筛选完整实现
react native·react.js·harmonyos