HarmonyOS开发:系统调试与日志——HiLog与hdc进阶
HarmonyOS开发:系统调试与日志——HiLog与hdc进阶
📌 核心要点:HiLog不只是
console.log的替代品,hdc也不只是装应用的工具——掌握高级日志采集、hdc远程调试和系统诊断,你才能在复杂问题面前不抓瞎。
背景与动机
你写了个系统应用,跑起来出了bug。怎么看日志?console.log?那玩意儿在Release包里直接没了,而且没有分级、没有过滤、没有持久化,稍微复杂点的场景就歇菜。
鸿蒙的HiLog才是正经的日志方案。它有分级、有标签、有域、能过滤、能持久化、能远程采集。但很多开发者只会hilog.info(0, 'tag', 'message')这种最基础的用法,根本没发挥出HiLog的真正能力。
再说hdc(Hardware Device Connector)。大多数人只知道hdc install和hdc shell,但hdc能做的事远不止这些——文件推送、端口转发、日志采集、性能分析、远程调试,它是一个全能的设备调试工具。
这篇就来讲HiLog的高级用法、hdc的进阶命令、系统日志采集与分析、远程调试与诊断。
核心原理
HiLog的日志体系
HiLog的日志分四个维度管理:
| 维度 | 说明 | 示例 |
|---|---|---|
| Domain | 日志域,标识模块 | 0x0001(系统UI)、0x9001(自定义) |
| Tag | 日志标签,标识子模块 | WiFiManager、BluetoothService |
| 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),日志量大了旧日志会被覆盖。如果你发现日志"丢失"了,很可能就是被覆盖了。
解决办法:
- 用
hdc hilog -r清除旧日志,腾出空间 - 用
hdc hilog > log.txt实时保存到文件 - 调整缓冲区大小:
hdc shell hilog -G 10M
坑三:hdc连接不稳定
USB连接的hdc偶尔会断开,尤其是在传输大文件或长时间运行时。
解决办法:
- 用TCP连接替代USB:
hdc tconn <device_ip>:5555 - 增加超时时间:
hdc -t <timeout_ms> <command> - 重启hdc服务:
hdc kill; hdc start
坑四:Release包的Debug日志不输出
HiLog的Debug级别日志在Release构建中默认不输出。如果你在Release包里用Debug日志追踪问题,白费力气。
开发阶段用Debug日志没问题,但关键流程一定要用Info级别以上。这样Release包里也能看到关键节点的日志。
坑五:hdc shell中文乱码
hdc shell里执行命令,中文输出乱码。原因是Windows的终端编码跟设备端不一致。
解决办法:
- PowerShell设置UTF-8:
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - 用
hdc shell hilog导出日志到文件,用VS Code打开查看
HarmonyOS 6适配说明
HarmonyOS 6在调试与日志方面有几个变化:
- 结构化日志:HiLog新增结构化日志接口,支持输出JSON格式的日志,方便机器解析。
// HarmonyOS 6 结构化日志
hilog.info(0xD001, 'NetworkService', {
event: 'request_completed',
url: 'https://api.example.com/data',
statusCode: 200,
duration: 150,
requestId: 'abc123'
});
-
hdc远程调试增强:新增
hdc debug命令,支持远程断点调试,不需要USB连接。 -
日志关联追踪:新增
TraceId概念,同一次请求的所有日志自动关联,方便追踪跨模块调用链。
// 设置追踪ID
hilog.setTraceId('request_12345');
// 后续所有日志自动携带这个traceId
hilog.info(0xD001, 'API', '请求开始');
hilog.info(0xD001, 'DB', '数据库查询完成');
hilog.info(0xD001, 'API', '请求结束');
hilog.clearTraceId();
- 性能分析集成:hdc新增
hdc perf命令,可以直接采集CPU、内存、网络等性能数据,不需要额外工具。
适配建议:关键业务流程使用结构化日志,配合TraceId做全链路追踪。Release包保留Info级别以上的日志,确保线上问题可追踪。
总结
HiLog和hdc是系统开发的"眼睛"和"手"。没有日志你啥也看不见,没有hdc你啥也摸不着。用好它们,调试效率翻倍。
核心要点回顾:
- HiLog封装成工具类,统一Domain和Tag,别到处写原生调用
- 日志级别要选对:Debug开发用、Info关键流程、Error不可恢复错误
- hdc不只是装应用,日志采集、文件推送、端口转发都是常用功能
- 日志缓冲区有限,重要日志及时导出
- Release包的Debug日志不输出,关键流程用Info级别以上
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐⭐ API简单,但日志分析和hdc进阶需要经验 |
| 使用频率 | ⭐⭐⭐⭐⭐ 开发调试天天用,没有比这更常用的了 |
| 重要程度 | ⭐⭐⭐⭐⭐ 没有日志和调试工具,开发寸步难行 |
- 点赞
- 收藏
- 关注作者
评论(0)