起因
想给一段代码加性能打点,搜出来的写法有两种。
一种是 startTrace 配 finishTrace,另一种是 startSyncTrace 配 finishSyncTrace。名字不一样,参数也不一样,但看起来干的是同一件事。
翻完声明文件才明白:这不是两种写法,是两代接口。老的那代从 API 8 就有了,新的那代到 API 19 才补齐。
一、两代接口并排看
文件:ets/api/@ohos.hiTraceMeter.d.ts。
老的一代,@since 8:
function startTrace(name: string, taskId: number): void; // 第 131 行
function finishTrace(name: string, taskId: number): void; // 第 148 行
function traceByValue(name: string, count: number): void; // 第 165 行
新的一代,@since 19:
function startSyncTrace(level: HiTraceOutputLevel, name: string, customArgs?: string): void; // 第 183 行
function finishSyncTrace(level: HiTraceOutputLevel): void; // 第 196 行
function startAsyncTrace(level: HiTraceOutputLevel, name: string, taskId: number,
customCategory: string, customArgs?: string): void; // 第 225 行
function finishAsyncTrace(level: HiTraceOutputLevel, name: string, taskId: number): void; // 第 240 行
function traceByValue(level: HiTraceOutputLevel, name: string, count: number): void; // 第 255 行
中间差了 11 个 API 版本。
新老两代的区别,看一眼参数就知道:新的每个函数第一个参数都是 level。而且从这一代起,多了一个类型 HiTraceOutputLevel。
注意 traceByValue 出现了两次。它是个重载,老签名不带 level,新签名带 level,都在同一个模块里。这不是笔误,是为了兼容老代码故意保留的。
二、HiTraceOutputLevel 有四档,还有个别名
第 61 行的枚举:
enum HiTraceOutputLevel {
DEBUG = 0, // 第 70 行
INFO = 1, // 第 79 行
CRITICAL = 2, // 第 88 行
COMMERCIAL = 3, // 第 97 行
MAX = COMMERCIAL // 第 106 行
}
四个真实取值,加上第五个成员。
MAX = COMMERCIAL 这一行值得单独说。它没引入新数值,只是给 COMMERCIAL(3)起了个别名。
这类"上限哨兵"在别的语言里常见,但在 HarmonyOS 这套声明文件里不常有。它带来两个实际影响:
- 遍历枚举时会遇到重复值。DEBUG 到 MAX 走一圈,COMMERCIAL 和 MAX 的数值都是 3,按值去重会少一个成员
- switch 里不能同时列 COMMERCIAL 和 MAX。两个 case 值相同,编译器会报重复 case
所以如果某处代码写了 case HiTraceOutputLevel.MAX:,别以为它是"比 COMMERCIAL 更高的一档"。它就是 COMMERCIAL。
三、sync 和 async 不是快慢之分
这是我觉得最容易误读的一处。
startSyncTrace 和 startAsyncTrace,名字里的 sync 和 async 不是指"同步执行还是异步执行",而是指打点覆盖的代码范围是不是在同一个调用栈里。
- startSyncTrace / finishSyncTrace:同一线程内的连续代码段,中间不跨异步回调。它只需要 level 和 name,不需要 taskId
- startAsyncTrace / finishAsyncTrace:跨线程或跨异步回调的代码段。它需要 taskId 来配对,还需要一个 customCategory 参数做分类
判断方法很简单:打点的开始和结束之间,如果代码会 yield(发起异步、等待回调、切线程),就必须用 async 那套。
用错了会怎样?注释里没写会报错。但既然 sync 那套不需要 taskId,它大概只能在同一调用栈里靠顺序配对,一旦中间切了线程,配对就会错乱。
四、不判断 isTraceEnabled() 就是白付开销
第 267 行:
function isTraceEnabled(): boolean;
这个函数回答一个问题:现在有没有人在抓 trace。
为什么要在埋点前问它?因为埋点本身是要花代价的。每次调 startTrace,系统都要做记录;如果你在热点路径上埋了几十个点,而这些 trace 平时根本没人看,那这些开销就是纯浪费。
正确写法是先判断:
if (hiTraceMeter.isTraceEnabled()) {
hiTraceMeter.startSyncTrace(hiTraceMeter.HiTraceOutputLevel.INFO, 'loadData');
}
// … 业务代码
if (hiTraceMeter.isTraceEnabled()) {
hiTraceMeter.finishSyncTrace(hiTraceMeter.HiTraceOutputLevel.INFO);
}
把开关判断放在埋点外面,而不是指望系统帮你忽略。
五、还能监听 trace 开关本身
第 305 行和第 322 行,@since 22:
type TraceEventListener = (traceStatus: boolean) => void; // 第 278 行
function registerTraceListener(callback: TraceEventListener): number; // 第 305 行
function unregisterTraceListener(index: number): number; // 第 322 行
注册一个回调,当 trace 的开启状态变化时会被通知,回调参数是 boolean。
这对"打点开销敏感的模块"有用。比如一个高频循环:平时不抓 trace 时用轻量路径,一旦 trace 打开,切换到带详细埋点的路径。比每轮循环都去问 isTraceEnabled() 更省。
注册返回一个 number,是个索引;注销时把它传回去。记得注销,否则回调会一直挂着。
六、把版本门槛排一下
这篇涉及的东西,起始版本分三档:
- API 8:模块本身(第 47 行)、startTrace(129 行)、finishTrace(146 行)、traceByValue(name, count)(163 行)
- API 19:HiTraceOutputLevel(59 行)、startSyncTrace(181 行)、finishSyncTrace(194 行)、startAsyncTrace(223 行)、finishAsyncTrace(238 行)、traceByValue(level, …)(253 行)、isTraceEnabled(265 行)
- API 22:TraceEventListener(276 行)、registerTraceListener(303 行)、unregisterTraceListener(320 行)
如果你的 minAPIVersion 低于 19,就只能用老的那三件套,而且拿不到输出等级控制。
几处和常见理解不一样的地方
- 打点接口有两代,老的从 API 8 就有,新的从 API 19 才有,差 11 个版本
- 新老两代的区别是新的每个函数第一个参数都是 level
- traceByValue 有两个重载,老的不带 level,新的带,同模块并存
- HiTraceOutputLevel 里 MAX 是 COMMERCIAL 的别名,不新增取值;switch 里不能同时列这两个
- startSyncTrace 的 sync 指"同一调用栈",不是"同步执行"。跨异步回调要用 startAsyncTrace
- 不判断 isTraceEnabled() 就埋点,是在生产环境白付开销
- registerTraceListener 是 API 22 才有的,能监听 trace 开关状态变化
写打点代码之前,先确认两件事:你的最低支持版本是多少,以及这段代码会不会跨异步回调。前者决定你能用哪一代接口,后者决定你该用 sync 还是 async。
补充说明:本篇没有跑通截图。手边没有真机,模拟器也没有跑通打点采集链路,所以文中只做声明层面的核对,每个结论都标了行号,可以自行打开对照。涉及运行期表现的地方,我按声明文件与注释原文转述,没有替官方补充结论。
本系列其他文章
- 我把 HarmonyOS 的折叠屏 API 翻了一遍:10 个折叠状态,你大概只处理了 3 个
- 我读了 HarmonyOS 的断点声明:有 5 个档位,高度断点算的其实是宽高比
- 画中画起不来的三种原因:设备支持、设置开关、窗口可见是三件事
- 悬浮窗有 6 个状态,第 5 个叫"在悬浮球里"
- 窗口舞台的两套生命周期枚举:4 个成员同名,其中 2 个的值不一样


