做性能优化最让人沮丧的,不是找不到优化点,而是找到了耗时长的模块,但工具里显示那一段代码的执行时间明明很短。特别是在 ArkTS 里,当你把大量计算塞进了 TaskPool 里,主线程的 UI 确实不卡了,但如果任务本身慢,用户等的时间依然很长。你打开 Profiler,发现主线程的帧率很平稳,而 TaskPool 里的任务耗时又很难和 UI 的交互建立起直接的联系。

因为跨线程的原因,工具默认只抓主线程的调用栈,或者各个工作线程的调用栈是独立展示的。当业务逻辑跨越了多个线程(主线程 -> TaskPool -> Worker),你很难看清楚这中间到底谁在等谁。

使用 HiTraceMeter 做自定义打点

系统的 API 只能帮你到线程级别,真正的业务耗时节点,必须自己在代码里埋点。HiTraceMeter 就是用来干这个的。它不像 console.log 那样会拖慢执行速度,它只是在系统底层的时间线记录一对起始和结束标记。

typescript

import { hiTraceMeter } from '@kit.PerformanceAnalysisKit'

// 开始打点
hiTraceMeter.startTrace('ProcessImageTask', 1)

// 模拟耗时业务逻辑
await this.doHeavyCalculation()

// 结束打点
hiTraceMeter.finishTrace('ProcessImageTask', 1)

startTrace 和 finishTrace 必须成对出现,第一个参数是任务名,第二个参数是标识(通常用个整数)。只要名字和标识匹配,系统就会把它们在一根线上连接起来。在 DevEco Profiler 的 Trace 视图中,你就能看到一条名字为 ProcessImageTask 的线段,它的长度就代表了从 start 到 finish 的总耗时。

跨线程的 Trace 追踪

刚才的打点是在同一个线程里的。如果任务投递到了 TaskPool,你需要把打点信息也带过去。虽然 hiTraceMeter 在底层支持链路追踪,但最简单的做法是,在执行任务的函数内部(也就是 @Concurrent 修饰的函数里)再次埋点。

typescript

@Concurrent
function processImageTask(imageData: ArrayBuffer) {
  hiTraceMeter.startTrace('InnerImageProcess', 2)
  // 原生处理逻辑
  hiTraceMeter.finishTrace('InnerImageProcess', 2)
  return result
}

// 主线程
hiTraceMeter.startTrace('OuterDispatch', 3)
await taskpool.execute(processImageTask, data)
hiTraceMeter.finishTrace('OuterDispatch', 3)

在 Profiler 里,你会看到主线程上的 OuterDispatch 拉得很长,因为它包含了 await 的等待时间。点开它旁边的子线程(比如 TaskPool 线程),你能看到一个并行的 InnerImageProcess 线段。如果 OuterDispatch 很长,而 InnerImageProcess 很短,说明时间不是花在计算上,而是花在了排队或者数据传递上。如果两者长度几乎一样,那计算确实是瓶颈,得去优化 processImageTask 内部的逻辑。

任务调度与序列化的时间暗流

在跨线程的通信里,有一个极容易被忽视的耗时点:序列化。通过 taskpool.execute 传过去的参数,如果是普通的 JS 对象,底层需要把它序列化,再到工作线程里反序列化。

如果你的任务执行时间只有 5 毫秒,但传了 5MB 的 JSON 数据过去,序列化和反序列化可能花掉 50 毫秒。在 Profiler 里,这 50 毫秒可能被算在了主线程的 await 消耗中,工作线程只显示那 5 毫秒的执行时间。

所以,大数据的传递,千万别用普通对象。如果是文件,传沙箱路径;如果是大段二进制,传 ArrayBuffer 或者使用转移(transfer)能力。只要能避开不必要的序列化,跨线程的性能就能上一个台阶。

内存指标与 CPU 占用的结合分析

打点只能看时间,如果任务是卡在了内存分配或者 GC 上,时间打点反而会误导你。在 DevEco Profiler 里,把 CPU 时间线和内存时间线对齐看。

如果 ProcessImageTask 的耗时很长,但 CPU 占用并不高,那可能是发生了频繁的 GC。去查看那段时间内的内存曲线,是不是有大对象被不断分配和回收。如果是,就得去优化内部的数据结构,比如不要在循环里不断 push 新对象,而是复用数组或者使用对象池。

如果把一个耗时任务拆成很小的碎片去频繁调度,CPU 就会频繁在上下文中切换,调度器的开销会吃掉整体性能。尽量把一次能做完的批量计算合并成一个大任务,用空间换时间,才是多线程开发里的硬道理。

Logo

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

更多推荐