HarmonyOS开发:系统调试与日志——HiLog与hdc进阶

举报
Jack20 发表于 2026/06/28 21:13:33 2026/06/28
【摘要】 HarmonyOS开发:系统调试与日志——HiLog与hdc进阶📌 核心要点:HiLog不只是console.log的替代品,hdc也不只是装应用的工具——掌握高级日志采集、hdc远程调试和系统诊断,你才能在复杂问题面前不抓瞎。 背景与动机你写了个系统应用,跑起来出了bug。怎么看日志?console.log?那玩意儿在Release包里直接没了,而且没有分级、没有过滤、没有持久化,稍微...

HarmonyOS开发:系统调试与日志——HiLog与hdc进阶

📌 核心要点:HiLog不只是console.log的替代品,hdc也不只是装应用的工具——掌握高级日志采集、hdc远程调试和系统诊断,你才能在复杂问题面前不抓瞎。

背景与动机

你写了个系统应用,跑起来出了bug。怎么看日志?console.log?那玩意儿在Release包里直接没了,而且没有分级、没有过滤、没有持久化,稍微复杂点的场景就歇菜。

鸿蒙的HiLog才是正经的日志方案。它有分级、有标签、有域、能过滤、能持久化、能远程采集。但很多开发者只会hilog.info(0, 'tag', 'message')这种最基础的用法,根本没发挥出HiLog的真正能力。

再说hdc(Hardware Device Connector)。大多数人只知道hdc installhdc shell,但hdc能做的事远不止这些——文件推送、端口转发、日志采集、性能分析、远程调试,它是一个全能的设备调试工具。

这篇就来讲HiLog的高级用法、hdc的进阶命令、系统日志采集与分析、远程调试与诊断。

核心原理

HiLog的日志体系

HiLog的日志分四个维度管理:

维度 说明 示例
Domain 日志域,标识模块 0x0001(系统UI)、0x9001(自定义)
Tag 日志标签,标识子模块 WiFiManagerBluetoothService
Level 日志级别 Debug → Info → Warn → Error → Fatal
Timestamp 时间戳 自动添加

日志级别的使用原则:

  • Debug:开发调试用,Release包不输出
  • Info:关键流程节点,如"服务启动成功"
  • Warn:可恢复的异常,如"配置缺失,使用默认值"
  • Error:不可恢复的错误,如"网络请求失败"
  • Fatal:致命错误,进程即将退出

hdc的架构

flowchart LR
    A[开发机] -->|USB/TCP| B[hdc daemon]
    B --> C[设备端服务]
    
    C --> C1[Shell服务]
    C --> C2[文件服务]
    C --> C3[端口转发]
    C --> C4[日志服务]
    C --> C5[应用管理]
    
    A -->|hdc命令| D[命令解析]
    D -->|shell| C1
    D -->|file send/recv| C2
    D -->|fport| C3
    D -->|hilog| C4
    D -->|install/uninstall| C5
    
    classDef dev fill:#e8f5e9,stroke:#4caf50,color:#1b5e20
    classDef daemon fill:#e3f2fd,stroke:#2196f3,color:#0d47a1
    classDef service fill:#fff3e0,stroke:#ff9800,color:#e65100
    classDef cmd fill:#f3e5f5,stroke:#9c27b0,color:#4a148c
    
    class A, D dev
    class B daemon
    class C,C1,C2,C3,C4,C5 service

日志采集与分析流程

flowchart TD
    A[应用输出HiLog] --> B[hilog服务]
    B --> C[内存日志缓冲区]
    C -->|hdc hilog| D[实时查看]
    C -->|持久化| E[/data/log/faultlog/]
    
    D --> F[过滤分析]
    E --> G[故障日志分析]
    
    F --> F1[按Domain过滤]
    F --> F2[按Tag过滤]
    F --> F3[按Level过滤]
    
    G --> G1[Crash日志]
    G --> G2[ANR日志]
    G --> G3[Watchdog日志]
    
    classDef source fill:#e8f5e9,stroke:#4caf50,color:#1b5e20
    classDef buffer fill:#e3f2fd,stroke:#2196f3,color:#0d47a1
    classDef output fill:#fff3e0,stroke:#ff9800,color:#e65100
    classDef analysis fill:#fce4ec,stroke:#f44336,color:#b71c1c
    
    class A source
    class B,C buffer
    class D,E output
    class F,F1,F2,F3,G,G1,G2,G3 analysis

代码实战

基础用法:HiLog封装

先封装一个好用的日志工具,别到处写hilog.xxx

// common/Logger.ets - HiLog封装工具
import hilog from '@ohos.hilog';

// 日志域定义(0xD000 - 0xDFFF 为厂商自定义范围)
const APP_DOMAIN = 0xD001;

// 日志级别
enum LogLevel {
  DEBUG = 3,
  INFO = 4,
  WARN = 5,
  ERROR = 6,
  FATAL = 7,
}

class Logger {
  private domain: number = APP_DOMAIN;
  private prefix: string = '';
  private isDebug: boolean = true; // 根据构建配置动态设置

  constructor(prefix: string = '') {
    this.prefix = prefix;
  }

  // 设置日志域
  setDomain(domain: number): void {
    this.domain = domain;
  }

  // Debug级别日志(Release包不输出)
  debug(format: string, ...args: Object[]): void {
    if (this.isDebug) {
      hilog.debug(this.domain, this.prefix, format, args);
    }
  }

  // Info级别日志
  info(format: string, ...args: Object[]): void {
    hilog.info(this.domain, this.prefix, format, args);
  }

  // Warn级别日志
  warn(format: string, ...args: Object[]): void {
    hilog.warn(this.domain, this.prefix, format, args);
  }

  // Error级别日志
  error(format: string, ...args: Object[]): void {
    hilog.error(this.domain, this.prefix, format, args);
  }

  // Fatal级别日志
  fatal(format: string, ...args: Object[]): void {
    hilog.fatal(this.domain, this.prefix, format, args);
  }

  // 带性能计时的日志
  time(label: string): { end: () => void } {
    const start = Date.now();
    return {
      end: () => {
        const duration = Date.now() - start;
        this.info(`${label} 耗时: ${duration}ms`);
      }
    };
  }

  // 分组日志(用于复杂流程追踪)
  group(label: string): Logger {
    this.info(`┌── ${label} ──`);
    return this;
  }

  groupEnd(): void {
    this.info('└──');
  }
}

// 创建模块级别的Logger实例
export function createLogger(moduleName: string): Logger {
  return new Logger(moduleName);
}

export default Logger;

进阶用法:日志采集与远程诊断

// common/LogCollector.ets - 日志采集与诊断工具
import hilog from '@ohos.hilog';
import fs from '@ohos.file.fs';
import { BusinessError } from '@ohos.base';

const APP_DOMAIN = 0xD001;

interface LogEntry {
  timestamp: string;
  level: string;
  domain: string;
  tag: string;
  message: string;
}

class LogCollector {
  private tag: string = 'LogCollector';
  private logBuffer: LogEntry[] = [];
  private maxBufferSize: number = 1000;
  private isCollecting: boolean = false;

  // 开始采集日志
  startCollect(): void {
    this.isCollecting = true;
    this.logBuffer = [];
    hilog.info(APP_DOMAIN, this.tag, '日志采集已开始');
  }

  // 停止采集日志
  stopCollect(): LogEntry[] {
    this.isCollecting = false;
    hilog.info(APP_DOMAIN, this.tag, `日志采集已停止,共 ${this.logBuffer.length}`);
    return [...this.logBuffer];
  }

  // 添加日志条目(在应用内拦截日志)
  addLogEntry(level: string, tag: string, message: string): void {
    if (!this.isCollecting) return;

    const now = new Date();
    const timestamp = `${now.getFullYear()}-${this.padZero(now.getMonth() + 1)}-${this.padZero(now.getDate())} ` +
      `${this.padZero(now.getHours())}:${this.padZero(now.getMinutes())}:${this.padZero(now.getSeconds())}.${this.padZero(now.getMilliseconds())}`;

    this.logBuffer.push({
      timestamp,
      level,
      domain: APP_DOMAIN.toString(16),
      tag,
      message,
    });

    // 超过缓冲区大小,移除最早的
    if (this.logBuffer.length > this.maxBufferSize) {
      this.logBuffer.shift();
    }
  }

  // 导出日志到文件
  async exportToFile(context: Context, filename: string = 'app_log.txt'): Promise<string> {
    try {
      const cacheDir = context.cacheDir;
      const filePath = `${cacheDir}/${filename}`;
      
      const logContent = this.logBuffer.map(entry => 
        `[${entry.timestamp}] [${entry.level}] [${entry.domain}] [${entry.tag}] ${entry.message}`
      ).join('\n');

      const file = fs.openSync(filePath, fs.OpenMode.CREATE | fs.OpenMode.TRUNC | fs.OpenMode.WRITE);
      fs.writeSync(file.fd, logContent);
      fs.closeSync(file);

      hilog.info(APP_DOMAIN, this.tag, `日志已导出: ${filePath}`);
      return filePath;
    } catch (err) {
      const error = err as BusinessError;
      hilog.error(APP_DOMAIN, this.tag, `日志导出失败: ${error.message}`);
      return '';
    }
  }

  // 按级别过滤日志
  filterByLevel(level: string): LogEntry[] {
    return this.logBuffer.filter(entry => entry.level === level);
  }

  // 按标签过滤日志
  filterByTag(tag: string): LogEntry[] {
    return this.logBuffer.filter(entry => entry.tag.includes(tag));
  }

  // 按关键词搜索日志
  search(keyword: string): LogEntry[] {
    return this.logBuffer.filter(entry => entry.message.includes(keyword));
  }

  // 获取错误统计
  getErrorStats(): Map<string, number> {
    const stats = new Map<string, number>();
    this.logBuffer
      .filter(entry => entry.level === 'ERROR' || entry.level === 'FATAL')
      .forEach(entry => {
        const key = entry.tag;
        stats.set(key, (stats.get(key) || 0) + 1);
      });
    return stats;
  }

  private padZero(num: number): string {
    return num < 10 ? `0${num}` : `${num}`;
  }
}

export default new LogCollector();

完整示例:日志诊断面板

// pages/LogDiagnosisPage.ets - 日志诊断面板
import LogCollector from '../common/LogCollector';
import { createLogger } from '../common/Logger';
import fs from '@ohos.file.fs';
import promptAction from '@ohos.promptAction';

const logger = createLogger('LogDiagnosisPage');

@Entry
@Component
struct LogDiagnosisPage {
  @State isCollecting: boolean = false;
  @State logCount: number = 0;
  @State errorCount: number = 0;
  @State warnCount: number = 0;
  @State searchKeyword: string = '';
  @State filteredLogs: string[] = [];
  @State exportPath: string = '';

  aboutToAppear() {
    this.updateStats();
  }

  // 更新统计信息
  updateStats() {
    const allLogs = LogCollector.stopCollect();
    if (this.isCollecting) {
      LogCollector.startCollect();
    }
    this.logCount = allLogs.length;
    this.errorCount = allLogs.filter(l => l.level === 'ERROR' || l.level === 'Fatal').length;
    this.warnCount = allLogs.filter(l => l.level === 'WARN').length;
  }

  // 开始/停止采集
  toggleCollect() {
    if (this.isCollecting) {
      LogCollector.stopCollect();
      this.isCollecting = false;
      logger.info('日志采集已停止');
    } else {
      LogCollector.startCollect();
      this.isCollecting = true;
      logger.info('日志采集已开始');
    }
    this.updateStats();
  }

  // 模拟日志输出
  simulateLogs() {
    logger.debug('这是调试日志 %d', 1);
    logger.info('用户点击了按钮');
    logger.warn('配置项缺失,使用默认值');
    logger.error('网络请求失败,状态码: %d', 500);
    logger.info('数据加载完成,共 %d 条', 42);
    logger.warn('内存使用率较高: %d%%', 85);
    this.updateStats();
    promptAction.showToast({ message: '已输出模拟日志' });
  }

  // 搜索日志
  searchLogs() {
    if (!this.searchKeyword) {
      this.filteredLogs = [];
      return;
    }
    const results = LogCollector.search(this.searchKeyword);
    this.filteredLogs = results.map(l => `[${l.level}] [${l.tag}] ${l.message}`);
  }

  // 导出日志
  async exportLogs(context: Context) {
    const path = await LogCollector.exportToFile(context, `diagnosis_${Date.now()}.txt`);
    if (path) {
      this.exportPath = path;
      promptAction.showToast({ message: '日志已导出' });
    } else {
      promptAction.showToast({ message: '导出失败' });
    }
  }

  build() {
    Scroll() {
      Column() {
        // 采集控制
        Row() {
          Column() {
            Text('日志诊断')
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
            Text(this.isCollecting ? '采集中...' : '未采集')
              .fontSize(12)
              .fontColor(this.isCollecting ? '#4caf50' : '#999999')
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)

          Button(this.isCollecting ? '停止采集' : '开始采集')
            .height(36)
            .fontSize(13)
            .backgroundColor(this.isCollecting ? '#f44336' : '#4caf50')
            .borderRadius(18)
            .onClick(() => this.toggleCollect())
        }
        .width('100%')
        .padding(16)
        .backgroundColor('#ffffff')
        .borderRadius(12)
        .margin({ bottom: 12 })

        // 统计卡片
        Row() {
          Column() {
            Text(`${this.logCount}`)
              .fontSize(24)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1976d2')
            Text('总日志')
              .fontSize(11)
              .fontColor('#999999')
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)

          Column() {
            Text(`${this.errorCount}`)
              .fontSize(24)
              .fontWeight(FontWeight.Bold)
              .fontColor('#f44336')
            Text('错误')
              .fontSize(11)
              .fontColor('#999999')
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)

          Column() {
            Text(`${this.warnCount}`)
              .fontSize(24)
              .fontWeight(FontWeight.Bold)
              .fontColor('#ff9800')
            Text('警告')
              .fontSize(11)
              .fontColor('#999999')
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
        }
        .width('100%')
        .padding(16)
        .backgroundColor('#ffffff')
        .borderRadius(12)
        .margin({ bottom: 12 })

        // 操作按钮
        Row() {
          Button('模拟日志')
            .height(40)
            .fontSize(13)
            .backgroundColor('#616161')
            .borderRadius(8)
            .layoutWeight(1)
            .onClick(() => this.simulateLogs())

          Button('导出日志')
            .height(40)
            .fontSize(13)
            .backgroundColor('#1976d2')
            .borderRadius(8)
            .layoutWeight(1)
            .margin({ left: 8 })
            .onClick(() => this.exportLogs(getContext(this)))
        }
        .width('100%')
        .margin({ bottom: 12 })

        // 搜索框
        Row() {
          TextInput({ placeholder: '搜索日志关键词...' })
            .height(40)
            .layoutWeight(1)
            .backgroundColor('#f5f5f5')
            .borderRadius(8)
            .onChange((value: string) => {
              this.searchKeyword = value;
            })
          
          Button('搜索')
            .height(40)
            .fontSize(13)
            .backgroundColor('#ff9800')
            .borderRadius(8)
            .margin({ left: 8 })
            .onClick(() => this.searchLogs())
        }
        .width('100%')
        .margin({ bottom: 12 })

        // 搜索结果
        if (this.filteredLogs.length > 0) {
          Column() {
            Text(`搜索结果 (${this.filteredLogs.length}条)`)
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .margin({ bottom: 8 })

            List() {
              ForEach(this.filteredLogs, (log: string, index: number) => {
                ListItem() {
                  Text(log)
                    .fontSize(12)
                    .fontColor('#333333')
                    .width('100%')
                    .padding(8)
                    .backgroundColor(index % 2 === 0 ? '#fafafa' : '#ffffff')
                }
              }, (log: string, index: number) => `${index}`)
            }
            .width('100%')
            .height(200)
            .divider({ strokeWidth: 0.5, color: '#f0f0f0' })
          }
          .width('100%')
          .padding(16)
          .backgroundColor('#ffffff')
          .borderRadius(12)
        }

        // 导出路径
        if (this.exportPath) {
          Row() {
            Text('导出路径:')
              .fontSize(12)
              .fontColor('#999999')
            Text(this.exportPath)
              .fontSize(12)
              .fontColor('#1976d2')
              .layoutWeight(1)
          }
          .width('100%')
          .padding(12)
          .backgroundColor('#e3f2fd')
          .borderRadius(8)
          .margin({ top: 12 })
        }

        // hdc常用命令参考
        Column() {
          Text('hdc常用命令')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .margin({ bottom: 12 })

          this.CommandRow('实时日志', 'hdc hilog')
          this.CommandRow('按域过滤', 'hdc hilog -D 0xD001')
          this.CommandRow('按标签过滤', 'hdc hilog -T WiFiManager')
          this.CommandRow('只看错误', 'hdc hilog -L ERROR')
          this.CommandRow('清除日志', 'hdc hilog -r')
          this.CommandRow('推送文件', 'hdc file send local remote')
          this.CommandRow('拉取文件', 'hdc file recv remote local')
          this.CommandRow('端口转发', 'hdc fport tcp:8080 tcp:8080')
          this.CommandRow('查看进程', 'hdc shell ps -ef')
          this.CommandRow('查看崩溃日志', 'hdc shell ls /data/log/faultlog/')
        }
        .width('100%')
        .padding(16)
        .backgroundColor('#ffffff')
        .borderRadius(12)
        .margin({ top: 12 })
      }
      .padding(16)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f5f5f5')
  }

  @Builder
  CommandRow(desc: string, cmd: string) {
    Row() {
      Text(desc)
        .fontSize(13)
        .fontColor('#666666')
        .width(100)
      Text(cmd)
        .fontSize(12)
        .fontColor('#333333')
        .fontFamily('monospace')
        .layoutWeight(1)
    }
    .width('100%')
    .margin({ bottom: 6 })
  }
}

踩坑与注意事项

坑一:HiLog的format参数不是完全兼容printf

HiLog的格式化参数支持%d%s%f,但不支持%lld%zu等C语言特有格式。而且参数数量有限制——最多4个参数。

// 错误:参数太多
hilog.info(0xD001, 'Tag', 'a=%d, b=%d, c=%d, d=%d, e=%d', 1, 2, 3, 4, 5);

// 正确:最多4个参数
hilog.info(0xD001, 'Tag', 'a=%d, b=%d, c=%d, d=%d', 1, 2, 3, 4);
// 或者拼接字符串
hilog.info(0xD001, 'Tag', `a=1, b=2, c=3, d=4, e=5`);

坑二:hilog缓冲区满了旧日志被覆盖

HiLog的内存缓冲区大小有限(默认约2MB),日志量大了旧日志会被覆盖。如果你发现日志"丢失"了,很可能就是被覆盖了。

解决办法:

  1. hdc hilog -r清除旧日志,腾出空间
  2. hdc hilog > log.txt实时保存到文件
  3. 调整缓冲区大小:hdc shell hilog -G 10M

坑三:hdc连接不稳定

USB连接的hdc偶尔会断开,尤其是在传输大文件或长时间运行时。

解决办法:

  1. 用TCP连接替代USB:hdc tconn <device_ip>:5555
  2. 增加超时时间:hdc -t <timeout_ms> <command>
  3. 重启hdc服务:hdc kill; hdc start

坑四:Release包的Debug日志不输出

HiLog的Debug级别日志在Release构建中默认不输出。如果你在Release包里用Debug日志追踪问题,白费力气。

开发阶段用Debug日志没问题,但关键流程一定要用Info级别以上。这样Release包里也能看到关键节点的日志。

坑五:hdc shell中文乱码

hdc shell里执行命令,中文输出乱码。原因是Windows的终端编码跟设备端不一致。

解决办法:

  1. PowerShell设置UTF-8:[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
  2. hdc shell hilog导出日志到文件,用VS Code打开查看

HarmonyOS 6适配说明

HarmonyOS 6在调试与日志方面有几个变化:

  1. 结构化日志:HiLog新增结构化日志接口,支持输出JSON格式的日志,方便机器解析。
// HarmonyOS 6 结构化日志
hilog.info(0xD001, 'NetworkService', {
  event: 'request_completed',
  url: 'https://api.example.com/data',
  statusCode: 200,
  duration: 150,
  requestId: 'abc123'
});
  1. hdc远程调试增强:新增hdc debug命令,支持远程断点调试,不需要USB连接。

  2. 日志关联追踪:新增TraceId概念,同一次请求的所有日志自动关联,方便追踪跨模块调用链。

// 设置追踪ID
hilog.setTraceId('request_12345');
// 后续所有日志自动携带这个traceId
hilog.info(0xD001, 'API', '请求开始');
hilog.info(0xD001, 'DB', '数据库查询完成');
hilog.info(0xD001, 'API', '请求结束');
hilog.clearTraceId();
  1. 性能分析集成:hdc新增hdc perf命令,可以直接采集CPU、内存、网络等性能数据,不需要额外工具。

适配建议:关键业务流程使用结构化日志,配合TraceId做全链路追踪。Release包保留Info级别以上的日志,确保线上问题可追踪。

总结

HiLog和hdc是系统开发的"眼睛"和"手"。没有日志你啥也看不见,没有hdc你啥也摸不着。用好它们,调试效率翻倍。

核心要点回顾:

  • HiLog封装成工具类,统一Domain和Tag,别到处写原生调用
  • 日志级别要选对:Debug开发用、Info关键流程、Error不可恢复错误
  • hdc不只是装应用,日志采集、文件推送、端口转发都是常用功能
  • 日志缓冲区有限,重要日志及时导出
  • Release包的Debug日志不输出,关键流程用Info级别以上
维度 评价
学习难度 ⭐⭐⭐ API简单,但日志分析和hdc进阶需要经验
使用频率 ⭐⭐⭐⭐⭐ 开发调试天天用,没有比这更常用的了
重要程度 ⭐⭐⭐⭐⭐ 没有日志和调试工具,开发寸步难行
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。