日志体系与调试:Logger 工具类与 hilog 实践

一、引言

短视频应用是典型的"问题潜伏型"工程:播放器状态机流转、窗口断点变化、沉浸式切换、多设备差异化分支,任何一个环节出错,现场往往一闪而过。如果没有一套统一、分级、可过滤的日志体系,排查问题只能靠"加打印 → 复现 → 删打印"的原始循环,效率极低。multi-short-video 工程在公共模块中封装了统一的 Logger 工具类,所有特性模块与产品模块复用同一套日志出口,配合 hilog 的级别与隐私过滤能力,形成从"打日志"到"看日志"的完整闭环。

本文先讲 hilog 的能力底座,再拆解工程 Logger.ets 的封装设计,随后以窗口管理、播放器、入口 Ability 的真实调用为例,最后给出日志查看、过滤与调试的实战技巧。 breakpoint-system

二、hilog 基础与日志级别

HarmonyOS 的日志系统由 @kit.PerformanceAnalysisKit 提供 hilog 接口。其核心概念有三个:domain(业务域,0~0xFFFF 的十六进制整数,用于按域过滤)、tag(标签字符串,标识日志来源)、format(格式化模板与参数列表)。hilog 提供五个级别,自低到高为:

级别接口语义典型场景

DEBUGhilog.debug调试细节数据源内容、断点值
INFOhilog.info关键流程窗口创建、全屏设置成功
WARNhilog.warn可恢复异常缓冲不足、重试前提示
ERRORhilog.error功能失败播放失败、窗口接口报错
FATALhilog.fatal致命错误数据损坏、不可恢复

级别不仅是语义约定,还决定输出与过滤行为:系统日志缓冲区按级别分层,DevEco Studio 与命令行均可按级别过滤。工程中大部分业务日志使用 INFO 与 ERROR 两级,调试过程数据用 DEBUG,避免刷屏。

三、Logger 工具类封装

直接在业务代码中调用 hilog.debug(domain, tag, format, args) 存在三个问题:domain/tag 到处重复、格式串不统一、后续想加时间戳或脱敏要改动所有调用点。工程在 common/multishortvideobase/src/main/ets/utils/Logger.ets 中做了统一封装,完整实现如下:

// d:\HarmonyOS\WorkSpace\multi-short-video\common\multishortvideobase\src\main\ets\utils\Logger.ets
import { hilog } from '@kit.PerformanceAnalysisKit';

/**
 * Common logger utility class.
 */
class Logger {
  private domain: number;
  private prefix: string;
  private format: string = '%{public}s, %{public}s';

  public constructor(prefix: string) {
    this.prefix = prefix;
    this.domain = 0x0000;
  }

  public debug(...args: Object[]): void {
    hilog.debug(this.domain, this.prefix, this.format, args);
  }

  public info(...args: Object[]): void {
    hilog.info(this.domain, this.prefix, this.format, args);
  }

  public warn(...args: Object[]): void {
    hilog.warn(this.domain, this.prefix, this.format, args);
  }

  public error(...args: Object[]): void {
    hilog.error(this.domain, this.prefix, this.format, args);
  }
}

export default new Logger('[multishortvideo]');

这段封装的设计要点:

  • 统一 TAG:单例以 [multishortvideo] 为前缀导出,全工程日志共享同一标签,Log 面板中一条过滤条件即可看全应用日志;
  • 统一 domain0x0000 集中在构造器内,避免散落各处;
  • 统一格式模板'%{public}s, %{public}s' 把两个参数都标记为 public(详见第五节脱敏说明),调用方只需传内容;
  • API 简洁debug/info/warn/error 与 hilog 同名,学习成本为零。

调用方通常再叠加一层"模块 TAG"前缀,例如窗口工具中 const TAG = 'WindowUtil',然后 Logger.error(TAG, ...),这样既能按应用过滤,又能按模块定位。

四、Logger 在工程中的真实使用

工程中 Logger 覆盖了三个典型场景。窗口管理common/multishortvideobase/.../utils/WindowUtil.ets)在获取主窗口、注册监听、设置沉浸式的关键节点打日志,并统一捕获异步异常:

setFullScreen(isFullScreen: boolean): void {
  try {
    this.mainWindow!.setWindowLayoutFullScreen(isFullScreen)
      .then(() => {
        Logger.info(TAG, `Succeeded in setting full-screen mode.`);
        this.mainWindowInfo.isFullScreen = isFullScreen;
      })
      .catch((err: BusinessError) => {
        Logger.error(TAG, `Failed to set full-screen mode. Code: ${err.code}, message: ${err.message}`);
      });
  } catch (error) {
    let err = error as BusinessError;
    Logger.error(TAG, `setWindowLayoutFullScreen sync error. Code: ${err.code}, message: ${err.message}`);
  }
}

播放器features/multishortvideoadaptivevideo/.../view/AdaptiveAVPlayer.ets)在创建、prepare、play、pause、seek、release 全生命周期打日志,异常分支一律带 err.codeerr.message

avPlayer.on('error', (err: BusinessError) => {
  Logger.error(TAG, `Invoke prepare failed, code is ${err.code}, message is ${err.message}`);
});
Logger.info(TAG, `changePortraitVideo immersionInfo = ${JSON.stringify(immersionInfo)}`);

入口 Abilityproducts/default/.../defaultability/MultiShortVideoDefaultAbility.ets)则暴露了两类用法的对比——工程模板自带的裸 hiloghilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate'))与业务代码采用的 LoggerLogger.info('Succeeded in setting the system bar properties.'))。裸 hilog 的 tag testTag 与工程的 [multishortvideo] 不统一,过滤时容易遗漏,这也是封装 Logger 的初衷之一:入口层也应收口到统一日志出口

五、日志脱敏与分级

日志中常包含设备信息、业务数据甚至用户输入,直接打印存在隐私风险。hilog 的格式化参数支持两种隐私标识:%{public}s 表示该参数明文输出,%{private}s 表示脱敏输出(真机上显示为 {private})。工程 Logger 的默认模板 '%{public}s, %{public}s' 把参数统一标记为 public,适用于 tag 类内部标识;一旦需要记录用户评论、账号等敏感字段,应改用显式 %{private}s 模板,或只记录长度、哈希等非敏感派生值。

分级方面建议遵循"INFO 记流程、ERROR 记失败、DEBUG 记细节":正常路径的进入/成功用 INFO,异常分支用 ERROR 并携带错误码与消息,需要追踪的具体数值(如窗口尺寸、断点枚举)用 DEBUG。避免用 ERROR 打印"可预期的分支"(如用户取消操作),否则 ERROR 级别会失去信噪比,真正的问题反被淹没。

日志的性能开销同样值得关注。hilog 在调试态会全量输出,但发布版应裁剪日志级别:建议把 Logger.debug 级别的输出在 release 包中关闭,仅保留 INFO 及以上,避免高频日志(如播放进度、窗口尺寸回调)在线上产生额外 IO 与功耗。工程可在 Logger 封装中增加一个编译期开关(如 isDebug 常量),在 hvigor 的 release 构建中注入 false,让 debug 调用在构建期被裁剪,而不是在运行期做判断——这与 ArkTS 强类型约束下"编译期解决"的理念一脉相承。另外,打印大对象(如整个数据源数组)会显著放大日志体积,建议打印摘要字段(长度、首元素、关键状态),需要全量时再临时放开。

六、日志查看与过滤

日志输出后,两种主流查看方式:

  • DevEco Studio Log 面板:真机或模拟器运行后,Log 面板按"级别 + 关键字"实时过滤,可直接按 tag 输入 multishortvideo 过滤全工程日志,再叠加 WindowUtilAdaptiveAVPlayer 等模块关键字缩小范围;日志默认按时间排序,可配合"按进程/按域"分组。
  • 命令行 hilog:连接设备后执行 hdc shell hilog,可用管道与 grep 组合过滤,例如 hdc shell hilog | findstr multishortvideo,适合在自动化脚本或设备端抓取长时日志;hilog 还支持按 domain 范围(hilog -D 0x0000)、按级别(-L)等参数精确裁剪。

排查技巧上,工程经验是"复现一次、抓全三样":完整日志(确定错误码与堆栈)、现场状态(窗口尺寸、断点、播放器 state)、操作序列(用户路径)。三者交叉比对,通常能把问题收敛到某个 Logger.error 附近。

七、调试技巧:断点、ArkTS 调试器与状态面板

日志之外,DevEco Studio 的调试能力与日志互补:

  • 断点调试:在 ArkTS 源码行号左侧打点,运行到断点后查看变量、表达式与调用栈;配合"条件断点"(如 curIndex === 2)可在循环或列表场景精准命中。
  • ArkTS 调试器:支持单步、步入/步出、求值表达式,适合追踪 @Monitor 触发链路与状态变化顺序。
  • 状态面板(UI Inspector):运行中通过 ArkUI Inspector 查看组件树与属性、@State/@Trace 实时值,定位"状态已变但 UI 未刷新"类问题——这类问题日志往往无迹可寻,状态面板一锤定音。
  • V2 状态观察:对 @Trace 字段在 @Monitor 回调中临时加 Logger.debug,可确认状态变化是否被监听、监听是否晚于预期。
工程中最常见的两类"日志看了白看"的坑也值得记录:其一,异步回调里打印的变量在打印前被后续代码修改(模板字符串在参数求值时就已展开,不受影响,但对象引用需注意);其二,真机日志缓冲被刷掉导致看不到早先日志,可适当调高 Logger.debug 的采样或缩短抓取窗口。

八、常见日志问题排查案例

把前文理论落到真实问题,三个案例最具代表性。案例一:播放器反复起播失败。现象是视频页面偶发黑屏,AdaptiveAVPlayerLogger.error 记录 Invoke prepare failed, code is ...。排查过程:先在 Log 面板按 multishortvideo + AdaptiveAVPlayer 过滤,确认错误集中在 avPlayer.prepare() 被调用时播放器仍未进入 initialized 状态——根源是快速滑动列表时同一个播放器实例被并发控制,修复方式是保证状态机流转严格串行(先 reset 再 prepare),并在每次状态变更处补 Logger.info 记录当前 state,让"状态顺序"有日志可查。

案例二:窗口断点不刷新。现象是折叠屏展开后布局未切换。日志中 onWindowSizeChange 与断点获取均无异常输出,但 widthBp 没有变化——进一步在 WindowUtil.updateWindowInfo 的监听注册处补打日志后确认,监听注册发生在 setUIContext 之前,uiContext 为 undefined 导致断点取不到。修复调用顺序后问题消失。这个案例说明:日志要打在"能力调用的边界",而不是只打在成功/失败的结果上,否则中间态丢失,问题无法定位。

案例三:日志"看着像没走"。现象是某分支明明执行了,Log 面板却搜不到。原因通常是过滤条件过窄(domain/tag/级别任一不匹配)或真机日志缓冲被新日志刷掉。排查时先放开过滤条件确认日志确实存在,再逐步收紧;必要时在 onWindowStageCreate 这类早期入口打 Logger.info 确认进程生命周期正常。三个案例的共同经验是:日志点位的设计比日志量更重要——在状态机的每一次迁移、窗口注册的每一个边界、异常抛出的每一个位置打点,才能让问题"看见即定位"。

九、总结与最佳实践

日志体系与调试沉淀为五条最佳实践:

  • 统一出口:所有模块复用 common/.../utils/Logger.ets 的单例,统一 domain、tag([multishortvideo])与格式模板,Log 面板一条过滤条件看全应用。
  • 分级克制:INFO 记流程、ERROR 记失败(必带 err.code/err.message)、DEBUG 记细节;可用预期的分支不要用 ERROR。
  • 敏感字段脱敏:默认模板 %{public}s 用于内部标识,用户数据等敏感字段改用 %{private}s 或只记派生值。
  • 模块 TAG 前缀:在调用方叠加 const TAG = 'WindowUtil' 之类模块标识,实现"应用级过滤 + 模块级定位"两级收敛。
  • 日志与调试器互补:问题不明时优先看日志收敛范围,再上断点与状态面板定位状态/UI 不一致问题。

Logo

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

更多推荐