【maaath】Flutter for OpenHarmony 跨平台工程日志能力实战:分级日志输出与本地文件持久化
Flutter for OpenHarmony 跨平台工程日志能力实战:分级日志输出与本地文件持久化
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
一、前言
在移动应用开发中,日志系统是排查问题的第一道防线。尤其在跨平台场景下,Flutter for OpenHarmony(以下简称 FHO)工程涉及 ArkTS 原生层与 Dart 上层的协同调用,传统的 print 语句难以满足复杂场景下的日志管理需求——我们需要分级控制、日志持久化、以及在真机/模拟器上统一可用的日志输出能力。
本文将围绕一个实际落地的日志模块 Logger.ets 展开,完整呈现从需求分析、架构设计、编码实现,到模拟器运行验证的全流程。该模块具备以下核心能力:
- 分级日志输出:支持 DEBUG / INFO / WARN / ERROR / FATAL 五个级别,运行时可动态调整
- 双重输出通道:同时向系统 HiLog(
hilog)和本地日志文件写入,互不干扰 - 日志文件滚动:单文件超过阈值后自动切换新文件,并自动清理超出数量限制的旧文件
- 异步安全写入:内部实现文件锁机制,避免多线程并发写入导致日志错乱
- 灵活配置项:支持自定义日志级别、输出开关、文件路径、文件大小上限、文件数量上限、时间戳开关等
AtomGit 仓库:https://atomgit.com
本文完整源码可于 AtomGit 平台搜索对应仓库获取。
二、需求分析与方案设计
2.1 为什么需要自定义日志模块
OpenHarmony 系统提供了原生的 hilog 接口用于日志输出,但其能力有如下局限:
- 不提供持久化:
hilog输出到系统日志缓冲区,应用重启后日志丢失,无法满足事后排查需求 - 缺乏滚动机制:没有自动分文件和清理旧日志的能力
- 分级控制粒度粗:只能通过
isLoggable做全局开关,无法针对不同业务模块精细控制 - 跨层日志对齐困难:Flutter Dart 层与 ArkTS 原生层各有各的日志格式,排查跨层问题时日志分散
因此,我们需要封装一个统一的日志工具,在 ArkTS 层(原生 Ability 代码)提供上述全部能力,同时保持与 hilog 的良好集成——即日志既写入系统日志供实时查看,又保存到本地文件供事后分析。
2.2 架构设计
整体采用单例模式,对外暴露统一的 logger 实例。模块内部划分为以下层次:
┌─────────────────────────────────────────┐
│ Logger (对外暴露层) │
│ - d() / i() / w() / e() / f() │
│ - setLevel() / getLogFiles() 等 │
├─────────────────────────────────────────┤
│ 日志格式化层 (formatLogEntry) │
│ - 组装时间戳、级别、Tag、消息 │
├─────────────────────────────────────────┤
│ 双通道输出层 │
│ - HiLog 输出 (enableHiLog) │
│ - 文件输出 (enableFile) │
├─────────────────────────────────────────┤
│ 文件管理层 (滚动/清理) │
│ - rollLogFile() │
│ - writeToFile() + 文件锁 │
└─────────────────────────────────────────┘
核心设计决策:
- 单例模式:整个应用生命周期共享一个 Logger 实例,避免多实例竞争文件句柄
- 异步写入:文件 I/O 操作不阻塞主线程,通过
Promise实现 - 文件锁机制:用布尔标志
fileLock模拟轻量级锁,防止并发写入冲突 - Pending 队列:文件忙碌时将待写入日志入队,待锁释放后自动 flush
三、核心实现
3.1 类型定义与配置
首先定义日志级别枚举和配置接口,这是整个模块的契约层:
export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
FATAL = 4,
NONE = 5
}
export interface LoggerConfig {
level: LogLevel; // 最低输出级别,低于此级别的日志将被过滤
enableHiLog: boolean; // 是否输出到系统日志
enableFile: boolean; // 是否写入本地文件
logDir: string; // 日志目录路径
logFileName: string; // 日志文件名前缀
maxFileSize: number; // 单个文件最大字节数
maxFileCount: number; // 最多保留的日志文件数量
showTimestamp: boolean; // 是否显示时间戳
showLevel: boolean; // 是否显示日志级别
}
DefaultConfig 提供开箱即用的默认值:DEBUG 级别、双通道开启、5MB 文件上限、最多 5 个文件、显示时间戳和级别。
3.2 单例实例获取
通过 getInstance 工厂方法获取 Logger 实例,首次调用时初始化配置和日志目录:
static getInstance(config?: LoggerConfig): Logger {
if (Logger.instance === null) {
const defaultCfg = new DefaultConfig();
if (config) {
defaultCfg.level = config.level ?? defaultCfg.level;
defaultCfg.enableHiLog = config.enableHiLog ?? defaultCfg.enableHiLog;
defaultCfg.enableFile = config.enableFile ?? defaultCfg.enableFile;
defaultCfg.logDir = config.logDir ?? defaultCfg.logDir;
defaultCfg.logFileName = config.logFileName ?? defaultCfg.logFileName;
defaultCfg.maxFileSize = config.maxFileSize ?? defaultCfg.maxFileSize;
defaultCfg.maxFileCount = config.maxFileCount ?? defaultCfg.maxFileCount;
defaultCfg.showTimestamp = config.showTimestamp ?? defaultCfg.showTimestamp;
defaultCfg.showLevel = config.showLevel ?? defaultCfg.showLevel;
}
Logger.instance = new Logger(defaultCfg);
}
return Logger.instance;
}
使用空值合并运算符 ?? 进行安全赋值,保证配置项可选——调用方只需关心需要自定义的部分,其余使用默认值。
3.3 日志目录初始化
日志目录的初始化逻辑会根据是否已设置 context(Ability 上下文)选择不同路径:
private initLogDirectory(): void {
try {
if (this.config.logDir === "") {
this.config.logDir = this.getDefaultLogDir();
}
if (!fs.accessSync(this.config.logDir)) {
fs.mkdirSync(this.config.logDir, true);
}
this.currentLogFile = this.getLogFilePath(0);
} catch (e) {
hilog.error(LOG_DOMAIN, LOG_TAG, "Failed to init log directory: %{public}s",
JSON.stringify(e));
}
}
private getDefaultLogDir(): string {
if (this.context) {
return this.context.filesDir + "/logs";
}
return "/data/storage/el2/base/logs/logger";
}
- 有
context时,使用context.filesDir(应用私有目录),确保日志文件不会被其他应用访问 - 无
context时,回退到系统标准日志路径
3.4 双重输出:HiLog + 本地文件
log 方法是核心输出逻辑,先做级别过滤,再分别写入两个通道:
private async log(level: LogLevel, tag: string, message: string, args: Object[]): Promise<void> {
// 1. 级别过滤
if (level < this.config.level) {
return;
}
// 2. 格式化日志行
const formatMessage = args.length > 0 ? this.formatMessage(message, args) : message;
const logLine = this.formatLogEntry(this.getLevelString(level), tag, formatMessage);
// 3. HiLog 输出(使用 %{public}s 避免隐私告警)
if (this.config.enableHiLog) {
const hiLogLevel = this.toHiLogLevel(level);
if (hilog.isLoggable(LOG_DOMAIN, tag, hiLogLevel)) {
if (level === LogLevel.DEBUG) {
hilog.debug(LOG_DOMAIN, tag, "%{public}s", formatMessage);
} else if (level === LogLevel.INFO) {
hilog.info(LOG_DOMAIN, tag, "%{public}s", formatMessage);
} else if (level === LogLevel.WARN) {
hilog.warn(LOG_DOMAIN, tag, "%{public}s", formatMessage);
} else if (level === LogLevel.ERROR) {
hilog.error(LOG_DOMAIN, tag, "%{public}s", formatMessage);
} else if (level === LogLevel.FATAL) {
hilog.fatal(LOG_DOMAIN, tag, "%{public}s", formatMessage);
}
}
}
// 4. 文件输出(异步,不阻塞)
this.writeToFile(logLine).catch((e: Error) => {
hilog.error(LOG_DOMAIN, LOG_TAG, "Async write failed: %{public}s", JSON.stringify(e));
});
}
格式化时使用 ISO 8601 时间戳格式(如 2026-04-20T10:30:45.123Z),便于后续日志分析和时间线重建。
3.5 文件滚动机制
当写入日志后文件大小超过 maxFileSize 时,触发滚动逻辑:删除最老的文件,其余文件序号依次递增,最后创建新的 app_log_0.txt:
private rollLogFile(): void {
try {
const files = this.getLogFileList();
// 超出数量限制时,删除最老的
if (files.length >= this.config.maxFileCount) {
fs.unlinkSync(files[0]);
}
// 旧文件序号递增:app_log_1 -> app_log_2, app_log_0 -> app_log_1
for (let i = files.length - 1; i > 0; i--) {
const oldPath = files[i];
const newPath = this.getLogFilePath(i);
if (fs.accessSync(newPath)) {
fs.unlinkSync(newPath);
}
fs.renameSync(oldPath, newPath);
}
this.currentFileSize = 0;
this.currentLogFile = this.getLogFilePath(0);
} catch (e) {
hilog.error(LOG_DOMAIN, LOG_TAG, "Failed to roll log file: %{public}s",
JSON.stringify(e));
}
}
3.6 异步安全写入
文件写入操作需要处理并发和阻塞问题。我们使用一个布尔锁加待处理队列的组合方案:
private async writeToFile(logLine: string): Promise<void> {
if (!this.config.enableFile) return;
// 文件忙碌时,将日志加入待处理队列
if (this.fileLock) {
this.pendingLogs.push(logLine);
return;
}
this.fileLock = true;
const logLineBytes: number = this.getByteLength(logLine);
// 超出单文件大小限制则滚动
if (this.currentFileSize + logLineBytes > this.config.maxFileSize) {
this.rollLogFile();
}
try {
const openMode = fs.OpenMode.CREATE | fs.OpenMode.APPEND | fs.OpenMode.READ_WRITE;
const result = await fs.open(this.currentLogFile, openMode);
const fd = this.getFdFromOpenResult(result);
if (fd !== -1) {
fs.writeSync(fd, logLine + "\n");
fs.close(fd);
this.currentFileSize += logLineBytes;
}
} catch (writeErr) {
hilog.error(LOG_DOMAIN, LOG_TAG, "Failed to write log: %{public}s",
JSON.stringify(writeErr));
} finally {
this.fileLock = false;
// 释放锁后,尝试写入 pending 队列中的日志
if (this.pendingLogs.length > 0) {
const pending = this.pendingLogs.shift();
if (pending) {
this.writeToFile(pending);
}
}
}
}
关于 fs.open 返回值的处理:OpenHarmony 不同版本中 fs.open 可能返回数字(文件描述符 fd)也可能返回包含 fd 字段的对象,因此 getFdFromOpenResult 方法对两种情况做了兼容处理:
private getFdFromOpenResult(result: ESObject): number {
if (typeof result === 'number') {
return result;
}
try {
const obj = result as FileDescriptor;
return obj.fd;
} catch {
return -1;
}
}
3.7 便捷调用 API
对外暴露的五个核心方法分别对应五个日志级别,与 Android 的 Log.d/i/w/e 风格一致:
d(tag: string, message: string, ...args: Object[]): void {
this.log(LogLevel.DEBUG, tag, message, args).catch((_e: Error) => {});
}
i(tag: string, message: string, ...args: Object[]): void {
this.log(LogLevel.INFO, tag, message, args).catch((_e: Error) => {});
}
w(tag: string, message: string, ...args: Object[]): void {
this.log(LogLevel.WARN, tag, message, args).catch((_e: Error) => {});
}
e(tag: string, message: string, ...args: Object[]): void {
this.log(LogLevel.ERROR, tag, message, args).catch((_e: Error) => {});
}
f(tag: string, message: string, ...args: Object[]): void {
this.log(LogLevel.FATAL, tag, message, args).catch((_e: Error) => {});
}
额外提供的工具方法包括:setLevel() 动态调整日志级别、readLogFile() 读取日志文件内容、readLastLines() 读取最新 N 行日志、getLogStats() 获取日志统计信息、clearLogs() 清空所有日志文件。
3.8 模块导出
在文件末尾实例化并导出默认 logger 供全工程使用:
export const logger = Logger.getInstance({
level: LogLevel.DEBUG,
enableHiLog: true,
enableFile: true,
logFileName: "app_log",
maxFileSize: 5 * 1024 * 1024,
maxFileCount: 5,
logDir: "",
showTimestamp: true,
showLevel: true
});
export default logger;
四、集成使用
在实际业务代码中使用极为简洁:
import logger from '../util/Logger';
// 初始化时绑定 Ability 上下文
logger.setContext(context);
// 日常日志记录
logger.d("Network", "Request sent: %{public}s", url);
logger.i("Network", "Response received: %{public}s", JSON.stringify(data));
logger.w("Cache", "Cache miss for key: %{public}s", key);
logger.e("Database", "Query failed: %{public}s", error.message);
logger.f("Crash", "Fatal error occurred: %{public}s", error.stack);
// 查看日志统计
const stats = logger.getLogStats(); // "Log files: 3, Total size: 2.35 MB"
// 读取最近日志
const recentLogs = logger.readLastLines(50);
// 动态调整级别(生产环境可关闭 DEBUG)
logger.setLevel(LogLevel.INFO);
日志文件默认保存在应用私有目录下的 /logs/app_log_N.txt 中,通过设备文件管理器或 DevEco Studio 的 File Explorer 即可查看。
五、运行验证
5.1 编译验证
在 DevEco Studio 中打开项目,执行 Build > Build Hap(s) / App(s) > Build Debug Hap 或使用 hvigor 命令行构建:
hvigor.bat build --mode module -p product=phone -p target=arkJsRelease
预期输出:BUILD SUCCESS,无 ArkTS 编译错误(ERROR:0)。
5.2 模拟器运行验证
将应用部署到 OpenHarmony 模拟器上运行,通过 DevEco Studio 的 Log窗口 或 hilog 命令行工具可以实时查看日志输出:
hilog | grep Logger
同时验证本地日志文件是否正确生成。日志内容将包含时间戳、级别、Tag 和消息四个部分,例如:
[2026-04-20T10:30:45.123Z] [I] [Network] Response received: {"code":200,"data":[...]}
[2026-04-20T10:30:46.456Z] [E] [Database] Query failed: SQLITE_ERROR: no such table
运行截图:

5.3 功能验证清单
| 验证项 | 预期结果 | 验证方法 |
|---|---|---|
| DEBUG 日志输出 | HiLog 中可见 DEBUG 级别日志 | 设置 level=DEBUG,观察 HiLog |
| INFO 日志输出 | HiLog 中可见 INFO 级别日志 | 调用 logger.i() |
| ERROR 日志输出 | HiLog 中可见 ERROR 级别日志,红色高亮 | 调用 logger.e() |
| 日志文件生成 | /filesDir/logs/ 下出现 app_log_0.txt |
DevEco File Explorer 查看 |
| 日志滚动 | 写入大量日志后出现 app_log_1.txt 等 |
写入超过 5MB 数据后检查 |
| 旧文件清理 | 超过 5 个文件后,最老的文件被删除 | 写入大量数据后检查 |
| 级别过滤 | 设置 level=INFO 后 DEBUG 日志不再输出 | 调用 logger.setLevel(LogLevel.INFO) |
| 文件读取 | readLastLines() 返回正确的日志内容 |
调用方法检查返回值 |
六、扩展建议
6.1 接入 Flutter Dart 层
当前模块运行在 ArkTS 原生层。在 Flutter for OpenHarmony 工程中,可以通过 OH_LOG 方法通道将日志能力桥接到 Dart 层,使 Dart 代码也能使用同一套日志体系,实现跨层日志统一管理。
6.2 日志上传与远程分析
可进一步对接网络请求模块,将日志文件压缩后定期上传到服务器,支持远程实时分析和异常告警。
6.3 日志加密
敏感应用场景下,可对日志文件内容进行 AES 加密,仅在特定解密工具中查看,防止日志内容泄露。
6.4 结构化日志格式
可将日志格式从纯文本升级为 JSON 结构化格式,便于日志解析工具(如 ELK、Graylog)直接入库分析。
七、总结
本文详细介绍了在 Flutter for OpenHarmony 跨平台工程中,如何从零设计并实现一个功能完备的日志模块。通过合理运用 ArkTS 的 hilog 系统日志接口和 fs 文件管理能力,我们实现了分级日志输出、本地文件持久化、自动滚动清理等核心功能。
该方案具备以下优势:
- 零依赖:纯使用 OpenHarmony 原生 API,无第三方库依赖
- 轻量级:单例模式 + 异步写入,对主线程性能影响极小
- 高可用:文件锁 + pending 队列确保写入安全可靠
- 易集成:导出默认实例,业务代码一行导入即可使用
- 可扩展:接口设计清晰,便于后续扩展加密、上传等能力
希望本文能为正在进行 OpenHarmony 跨平台开发的工程师们提供有价值的参考,也欢迎大家基于 AtomGit 平台共同完善这一日志模块。
感谢各位阅读!
参考链接
- OpenHarmony 应用日志开发指南:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides
- ArkTS 接口参考 - hilog:https://docs.openharmony.cn
- AtomGit 开源平台:https://atomgit.com
更多推荐

所有评论(0)