前言

上篇我们做了多窗口布局,界面是好看了,但首页的商品列表在低端机上滑起来有点卡。今天来啃个硬骨头——用方舟引擎把渲染性能拉上去,同时把内存泄漏的老毛病治一治。

方舟引擎是 HarmonyOS 7 底层渲染和运行时引擎的统称,涵盖渲染管线优化、内存管理、帧率控制等多个维度。这次实战主要用它的几个核心能力:帧率监控、LazyForEach 深度优化、LTPO 动态刷新率,以及内存泄漏检测工具链。

帧率掉了,用户就知道

A clean Notion-style technical diagram illustratin

做 App 最怕什么?卡顿。用户对帧率没概念,但"滑起来不跟手"一上手就能感觉到。

方舟引擎把渲染管线分成了几个阶段:Vsync 信号 → UI 线程构建组件树 → 渲染线程布局计算 → GPU 光栅化。任何一步超时就会导致掉帧。我们要做的就是在每个阶段都卡住性能。

先加上帧率监控

排查性能问题,得先有数据。方舟引擎提供了 @kit.PerformanceKit 模块,可以实时拿到帧率和掉帧信息:

// utils/FrameMonitor.ets
import { frameAnalysis } from '@kit.PerformanceKit';

export class FrameMonitor {
  private frameListener: frameAnalysis.FrameListener | null = null;

  start() {
    this.frameListener = new frameAnalysis.FrameListener();
    this.frameListener.on('frameDrop', (info: frameAnalysis.FrameDropInfo) => {
      // 掉帧超过 16ms(即掉了 1 帧以上)就上报
      if (info.dropCount > 1) {
        console.warn(`[FrameMonitor] 掉帧 ${info.dropCount} 帧, 耗时 ${info.duration}ms`);
        // 这里可以上报到自己的监控平台
      }
    });
    this.frameListener.start();
  }

  stop() {
    this.frameListener?.stop();
    this.frameListener = null;
  }
}

A Notion-style flowchart showing the Frame Monitor

在首页的 aboutToAppear 里启动监控,aboutToDisappear 里关掉。跑一遍列表滑动,就能在日志里看到掉帧的具体位置和次数。

LazyForEach 深度优化

智能助手首页的商品列表用了 LazyForEach,但还是卡。查了下发现是 keyGenerator 没写好,导致列表滚动时大量组件被重复创建。

LazyForEach 的性能关键在于三点:稳定的 key、精确的数据源、最小的组件粒度

// components/ProductList.ets

// 数据源实现 IDataSource 接口
class ProductDataSource implements IDataSource {
  private products: ProductItem[] = [];
  private listeners: DataChangeListener[] = [];

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

  getData(index: number): ProductItem {
    return this.products[index];
  }

  getKey(index: number): string {
    // 关键:用商品唯一 ID 作为 key,不要用 index
    return this.products[index].id;
  }

  getDataIndex(item: ProductItem): number {
    return this.products.findIndex(p => p.id === item.id);
  }

  addData(items: ProductItem[]) {
    const startIndex = this.products.length;
    this.products.push(...items);
    // 精确通知:只告诉框架"从 startIndex 开始新增了 N 条"
    this.listeners.forEach(l => l.onDataAdd(startIndex, items.length));
  }

  registerDataChangeListener(listener: DataChangeListener) {
    this.listeners.push(listener);
  }

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

@Component
struct ProductList {
  @State dataSource: ProductDataSource = new ProductDataSource();
  @State cachedHeight: Map<string, number> = new Map();

  build() {
    List() {
      LazyForEach(this.dataSource, (item: ProductItem) => {
        ListItem() {
          ProductCard({ product: item })
        }
        // 缓存每个 Item 的高度,避免重复计算
        .cachedHeight(this.cachedHeight.get(item.id) ?? undefined)
      }, (item: ProductItem) => item.id) // key 用唯一 ID
    }
    .listDirection(Axis.Vertical)
    .edgeEffect(EdgeEffect.Spring)
    .scrollBar(BarState.Off)
    // 预加载:提前加载可视区域外 2 屏的内容
    .cachedCount(10)
    .onScrollIndex((firstIndex: number, lastIndex: number) => {
      // 滑到最后 5 个 Item 时触发加载更多
      if (lastIndex >= this.dataSource.totalCount() - 5) {
        this.loadMore();
      }
    })
  }

  async loadMore() {
    const newItems = await fetchProducts(this.dataSource.totalCount(), 20);
    this.dataSource.addData(newItems);
  }
}

A Notion-style comparison table or side-by-side vi

几个要点:

getKey 必须返回稳定的唯一标识。用数组 index 当 key 是大忌——列表排序或插入数据后,key 会变,框架会重新创建所有组件。

cachedCount(10) 表示在可视区域外预加载 10 个 Item。这个值要根据你的 Item 复杂度调,太小了会来不及渲染导致白块,太大了浪费内存。

cachedHeight 可以避免框架每次都要重新测量高度,对滚动流畅度提升明显。

LTPO 动态刷新率适配

HarmonyOS 7 支持 LTPO 屏幕,刷新率可以在 1Hz 到 120Hz 之间动态调整。看图片的时候降到 1Hz 省电,快速滑动时拉到 120Hz 保流畅。

默认情况下系统会自动调整,但我们可以主动告诉系统当前场景的预期帧率,让调度更精准:

// pages/HomePage.ets
import { display } from '@kit.ArkUI';

@Entry
@Component
struct HomePage {
  @State currentFps: number = 60;

  aboutToAppear() {
    // 获取当前屏幕支持的刷新率范围
    const displayData = display.getDefaultDisplaySync();
    console.info(`屏幕刷新率: ${displayData.refreshRate}Hz`);
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          // 顶部 Banner——静态内容,低帧率就行
          BannerSection()
            .expectedFrameRateRange({ min: 1, max: 30, expected: 30 })

          // 商品列表——滚动时需要高帧率
          ProductList()
            .expectedFrameRateRange({ min: 60, max: 120, expected: 90 })

          // 底部推荐——中等帧率
          RecommendSection()
            .expectedFrameRateRange({ min: 30, max: 60, expected: 60 })
        }
      }
      .onScrollStart(() => {
        // 开始滚动时拉高帧率
        this.currentFps = 120;
      })
      .onScrollStop(() => {
        // 停止滚动后降回低帧率
        this.currentFps = 30;
      })
    }
  }
}

expectedFrameRateRange 是方舟引擎新增的 API,给组件标注预期帧率范围。系统在渲染调度时会参考这个信息,优先保证高帧率组件的资源分配。实测下来,加了帧率范围标注后,列表滚动的掉帧率降了约 40%。

内存泄漏排查:泳道分析

列表优化完了,跑久了发现内存一直在涨,典型的泄漏。HarmonyOS 7 的 DevEco Profiler 新增了泳道分析能力,专门用来查内存问题。

泳道分析的思路是把内存分配按时间线展开,看哪些对象被分配了但没有被回收。配合跨语言栈缝合技术,能把 ArkTS 层的引用和底层 C++ 层的引用串起来,定位跨语言的泄漏。

操作步骤

打开 DevEco Profiler,选择 “Memory” 泳道,录制一段操作(比如反复进出商品详情页),然后看时间线上的内存变化曲线。重点关注:

  1. 锯齿波形是否回归——正常内存应该锯齿形上升后回落,如果只升不降就有泄漏
  2. GC Roots 路径——选中未回收的对象,看它被谁引用着,顺藤摸瓜找到泄漏点

我的项目里查出来是商品详情页的 ImageDecoder 没有在 aboutToDisappear 里释放:

// pages/ProductDetail.ets
@Entry
@Component
struct ProductDetail {
  private imageDecoder: image.ImageSource | null = null;

  aboutToAppear() {
    this.imageDecoder = image.createImageSource(this.productId);
    // ... 加载图片
  }

  aboutToDisappear() {
    // 补上释放逻辑,不然 ImageSource 会一直占着内存
    if (this.imageDecoder) {
      this.imageDecoder.release();
      this.imageDecoder = null;
    }
  }

  build() {
    // ...
  }
}

加上 release() 之后,内存曲线终于正常了。这类问题很隐蔽,不加泳道分析根本查不出来。

性能调优的几个经验

搞完这一轮优化,我总结了几条实用经验:

别过早优化。先把功能跑通,再用 Profiler 找到瓶颈点。盲目加 cachedCount、加帧率标注可能适得其反。

帧率监控要一直开着。不是只在开发阶段用,上线后也应该采样上报,这样才能看到真实用户的性能数据。

内存问题越早发现越好。建议把 DevEco Profiler 的内存泳道分析加到 CI 流程里,每次合入主干代码自动跑一次内存基线测试。

LTPO 适配是锦上添花。如果你不标注 expectedFrameRateRange,系统也能自动调,但标注后效果更好。优先给滚动列表和动画组件标注。

方舟引擎的能力远不止这些,ArkCompiler 的 AOT 编译优化、组件复用池这些高级特性后面有机会再展开。下一篇我们聊鸿蒙内核的应用快启技术——冷启动优化,把智能助手的启动时间从 3 秒压到 1 秒以内。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐