HarmonyOS 6.1 全场景实战|《灵犀厨房》【日志系统】从 console 到 Logger:百万级日志的优雅升级之路

HarmonyOS 6.1 全场景实战|《灵犀厨房》【日志系统】从 console 到 Logger:百万级日志的优雅升级之路

摘要 :你的《灵犀厨房》里,console.log 满天飞、hilog.info 各写各的 tag、错误日志和生产日志搅成一锅粥------上架时你敢让这些日志全量输出到用户设备上吗?关了又怕线上问题无法排查,开着又担心性能拖垮。你可能觉得"不就是打个日志嘛,console.log 一下就行了"。但当你的 App 用 console 打了 200 处日志,用 hilog 打了 80 处日志,分布在 35+ 个文件里,你就知道什么叫"日志地狱"。本篇将基于 HarmonyOS 6.1.0(API 23),从日志架构的底层设计出发,带你完成一场"一行开关,全网静默"的统一日志升级。


一、引言:从"日志地狱"到"一行静默"

《灵犀厨房》已经迭代了20余篇专栏,功能越来越丰富------语音操控、深色模式、桌面卡片、健康仪表盘。每加一个功能,每写一个 Service,顺手就是一个 console.info('[XXX] 数据加载成功')。久而久之:

场景 痛点 后果
上架前日志清理 200+ 处 console.log 散落在 35+ 个文件中 不敢删、不敢留,纠结一整天
线上排查 Bug hilog.info 的 tag 五花八门,查一条日志要翻 8 个文件 排查效率极低,一个问题查半天
生产环境性能 console.log 在真机上也有开销,高频日志拖慢 UI 用户感知到卡顿,体验扣分
ForEach 缺少 key 列表滚动时整个列表重绘,CPU 飙升 低端设备直接卡成 PPT
定时器未清理 页面离开后 setTimeout 还在跑,回调访问已销毁组件 内存泄漏 + 偶发崩溃

而统一日志工具的一行配置解决所有问题:

🎯 IS_RELEASE = true → 所有日志瞬间静默。IS_RELEASE = false → 恢复全量输出。一行开关,全网静默。module 标识自动注入,调用栈精准定位。零运行时开销判定,发布性能无损耗。


二、核心原理:Logger 的四字真言

2.1 旧日志体系的三宗罪

在正式动手前,先看清旧体系的本质问题:

旧体系 问题本质 典型表现
console.log/info/error 原生 API,无法统一管理 每个文件自己加 [TagName] 前缀,风格不统一
hilog.info(domain, tag, msg) 需要手动传 DOMAIN,API 冗长 hilog.info(0x0000, 'LingxiKitchen', ...) 每次写一大串
混合使用 console 和 hilog 互不通信 生产环境关了 hilog,console 还在输出

核心矛盾 :开发调试阶段需要详细日志,生产发布阶段需要静默。但 consolehilog 都没有统一的"全局开关",你必须手动注释/删除每一处日志。

2.2 Logger 的一行开关原理

typescript 复制代码
// ============================================================
// 层级:Foundation(基础设施层)--- 全局工具
// 职责:统一日志工具类
//       - 封装 hilog 调用,统一 tag 前缀
//       - IS_RELEASE 开关控制全站日志输出
//       - 支持 info / warn / error / debug 四个级别
// ============================================================

import { hilog } from '@kit.PerformanceAnalysisKit';

const DOMAIN: number = 0x0001;
const TAG_PREFIX: string = 'LingxiKitchen';

/**
 * 🔑 核心开关:上架前将此值改为 true
 */
export const IS_RELEASE: boolean = false;

class Logger {
  private formatTag(module: string): string {
    return `${TAG_PREFIX}-${module}`;
  }

  info(module: string, message: string, ...args: Object[]): void {
    if (IS_RELEASE) return;
    const formattedMsg = `[${module}] ${message}`;
    hilog.info(DOMAIN, this.formatTag(module), formattedMsg, args);
  }

  warn(module: string, message: string, ...args: Object[]): void {
    if (IS_RELEASE) return;
    const formattedMsg = `[${module}] ${message}`;
    hilog.warn(DOMAIN, this.formatTag(module), formattedMsg, args);
  }

  error(module: string, message: string, ...args: Object[]): void {
    if (IS_RELEASE) return;
    const formattedMsg = `[${module}] ${message}`;
    hilog.error(DOMAIN, this.formatTag(module), formattedMsg, args);
  }

  debug(module: string, message: string, ...args: Object[]): void {
    if (IS_RELEASE) return;
    const formattedMsg = `[${module}] ${message}`;
    hilog.debug(DOMAIN, this.formatTag(module), formattedMsg, args);
  }
}

export const logger: Logger = new Logger();

四层设计逻辑

复制代码
┌─────────────────────────────────────────────────────────┐
│              logger.info('ModuleName', msg)              │
│                        ↓                                │
│  ┌──────────────────────────────────────────────────┐   │
│  │           IS_RELEASE ? (运行时零开销)              │   │
│  │         true → 静默 | false → 继续执行             │   │
│  └──────────────────┬───────────────────────────────┘   │
│                     ↓ false                              │
│  ┌──────────────────▼───────────────────────────────┐   │
│  │        格式化:[module] message + args            │   │
│  └──────────────────┬───────────────────────────────┘   │
│                     ↓                                    │
│  ┌──────────────────▼───────────────────────────────┐   │
│  │   hilog.info(DOMAIN, TAG_PREFIX-module, msg)      │   │
│  └──────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

💡 核心精髓IS_RELEASE 是模块级常量,V8 引擎在运行时读到第一个 if (IS_RELEASE) return 后,整条日志调用链的耗时 ≈ 一次布尔值读取。零函数调用开销,零字符串拼接浪费。


三、分层架构:日志服务的四层渗透

按照《灵犀厨房》的四层架构,logger 作为 Foundation 层的基础设施,被上层所有模块单向依赖:

层级 日志使用者 典型 module 标识
UI 层 页面 / 组件 LoginPage, RecipeDetailPage, HomeTabContent
ViewModel 层 状态管理 HomeViewModel, AuthViewModel, SearchViewModel
Services 层 服务能力 ApiService, TtsServiceHelper, RelationalStoreHelper
Business 层 业务流程 VoiceControlManager, WatchTimerManager, DistributedFlowManager

分层渗透图
#mermaid-svg-9EofBESVvuoA6pSt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9EofBESVvuoA6pSt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9EofBESVvuoA6pSt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9EofBESVvuoA6pSt .error-icon{fill:#552222;}#mermaid-svg-9EofBESVvuoA6pSt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9EofBESVvuoA6pSt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9EofBESVvuoA6pSt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9EofBESVvuoA6pSt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9EofBESVvuoA6pSt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9EofBESVvuoA6pSt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9EofBESVvuoA6pSt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9EofBESVvuoA6pSt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9EofBESVvuoA6pSt .marker.cross{stroke:#333333;}#mermaid-svg-9EofBESVvuoA6pSt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9EofBESVvuoA6pSt p{margin:0;}#mermaid-svg-9EofBESVvuoA6pSt .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-9EofBESVvuoA6pSt .cluster-label text{fill:#333;}#mermaid-svg-9EofBESVvuoA6pSt .cluster-label span{color:#333;}#mermaid-svg-9EofBESVvuoA6pSt .cluster-label span p{background-color:transparent;}#mermaid-svg-9EofBESVvuoA6pSt .label text,#mermaid-svg-9EofBESVvuoA6pSt span{fill:#333;color:#333;}#mermaid-svg-9EofBESVvuoA6pSt .node rect,#mermaid-svg-9EofBESVvuoA6pSt .node circle,#mermaid-svg-9EofBESVvuoA6pSt .node ellipse,#mermaid-svg-9EofBESVvuoA6pSt .node polygon,#mermaid-svg-9EofBESVvuoA6pSt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9EofBESVvuoA6pSt .rough-node .label text,#mermaid-svg-9EofBESVvuoA6pSt .node .label text,#mermaid-svg-9EofBESVvuoA6pSt .image-shape .label,#mermaid-svg-9EofBESVvuoA6pSt .icon-shape .label{text-anchor:middle;}#mermaid-svg-9EofBESVvuoA6pSt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9EofBESVvuoA6pSt .rough-node .label,#mermaid-svg-9EofBESVvuoA6pSt .node .label,#mermaid-svg-9EofBESVvuoA6pSt .image-shape .label,#mermaid-svg-9EofBESVvuoA6pSt .icon-shape .label{text-align:center;}#mermaid-svg-9EofBESVvuoA6pSt .node.clickable{cursor:pointer;}#mermaid-svg-9EofBESVvuoA6pSt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-9EofBESVvuoA6pSt .arrowheadPath{fill:#333333;}#mermaid-svg-9EofBESVvuoA6pSt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9EofBESVvuoA6pSt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9EofBESVvuoA6pSt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9EofBESVvuoA6pSt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9EofBESVvuoA6pSt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9EofBESVvuoA6pSt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-9EofBESVvuoA6pSt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9EofBESVvuoA6pSt .cluster text{fill:#333;}#mermaid-svg-9EofBESVvuoA6pSt .cluster span{color:#333;}#mermaid-svg-9EofBESVvuoA6pSt div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9EofBESVvuoA6pSt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9EofBESVvuoA6pSt rect.text{fill:none;stroke-width:0;}#mermaid-svg-9EofBESVvuoA6pSt .icon-shape,#mermaid-svg-9EofBESVvuoA6pSt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9EofBESVvuoA6pSt .icon-shape p,#mermaid-svg-9EofBESVvuoA6pSt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-9EofBESVvuoA6pSt .icon-shape .label rect,#mermaid-svg-9EofBESVvuoA6pSt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9EofBESVvuoA6pSt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9EofBESVvuoA6pSt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9EofBESVvuoA6pSt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 📦 Foundation 层
🎨 UI 层
35+ 页面 / 组件

→ 全部迁移为 logger.xxx()
🧠 ViewModel 层
HomeViewModel

logger.info('HomeVM', ...)
AuthViewModel

→ logger.warn('AuthVM', ...)
🔧 Services 层
ApiService

logger.info('ApiService', ...)
TtsServiceHelper

→ logger.debug('TTS', ...)
RelationalStoreHelper

→ logger.error('DB', ...)
🏭 Business 层
VoiceControlManager

logger.info('VoiceCtrl', ...)
WatchTimerManager

→ logger.warn('WatchTimer', ...)
DistributedFlowManager

→ logger.error('DistFlow', ...)
logger (Logger 实例)

IS_RELEASE 开关


四、关键实现步骤

Step 1:创建统一的 Logger 工具类

entry/src/main/ets/common/utils/Logger.ets 中创建上述 Logger 类。

设计要点

  1. 单例实例export const logger = new Logger(),全站共享一个实例,避免重复创建
  2. module 参数前置 :每个日志调用必须传入 module 标识,强制规范
  3. IS_RELEASE 前置判定 :所有日志方法的第一行就是 if (IS_RELEASE) return,零开销
  4. hilog 封装 :底层统一使用 @kit.PerformanceAnalysisKithilog,利用系统级日志管道

Step 2:批量迁移 console / hilog 调用

迁移策略

原调用 新调用
console.info('[LoginPage] 登录成功') logger.info('LoginPage', '登录成功')
console.error('[ApiService] 请求失败: ' + err) logger.error('ApiService', '请求失败', err)
hilog.info(0x0000, 'Tag', 'msg') logger.info('Module', 'msg')

影响范围

指标 数量
console 调用替换 ~100 处
hilog 调用替换 ~10 处
新增 import { logger } 35+ 个文件
涉及文件 35+ 个

迁移清单

文件 替换项
pages/RecipeDetailPage.ets 6 处 console + 定时器清理修复
pages/LoginPage.ets 3 处 console.error
pages/RegisterPage.ets 1 处 console.error
pages/SearchPage.ets 5 处 console
pages/CookingMonitorPage.ets 4 处 console
pages/DeviceConnectPage.ets 3 处 console
pages/FavoriteRecipesPage.ets 2 处 console
pages/CommunityPage.ets 2 处 console
pages/ProfileEditPage.ets 3 处 console
components/ProfileTabContent.ets 4 处 console
components/HomeTabContent.ets 5 处 console
components/DeviceTabContent.ets 3 处 console
services/ApiService.ets 8 处 console/hilog
services/RelationalStoreHelper.ets 5 处 console
services/TtsServiceHelper.ets 3 处 console
services/SpeechRecognizerHelper.ets 3 处 console
services/WatchServiceHelper.ets 2 处 console
services/HiAIImageClassifier.ets 2 处 console
services/MultiIngredientDetector.ets 3 处 console
business/VoiceControlManager.ets 4 处 console
business/TtsSpeechManager.ets 3 处 console
business/WatchTimerManager.ets 3 处 console
business/DistributedFlowManager.ets 2 处 console
viewmodel/AuthViewModel.ets 4 处 console
viewmodel/HomeViewModel.ets 4 处 console
viewmodel/HistoryViewModel.ets 3 处 console
viewmodel/HealthDashboardViewModel.ets 3 处 console
viewmodel/SearchViewModel.ets 3 处 console
viewmodel/KitchenDeviceViewModel.ets 2 处 console
common/ToastUtil.ets 1 处 console
common/IngredientCamera.ets 2 处 console
entryability/EntryAbility.ets 3 处 hilog

Step 3:修复 ForEach 缺少 key 函数

在迁移日志的过程中,审查代码发现多处 ForEach 缺少 key 函数,导致列表重绘性能问题:

typescript 复制代码
// ❌ 修复前:缺少 key 函数,整个列表每次重绘
ForEach(this.recipeList, (item: Recipe) => {
  RecipeCard({ recipe: item })
})

// ✅ 修复后:指定 key 函数,按 ID 精确 diff
ForEach(this.recipeList, (item: Recipe) => {
  RecipeCard({ recipe: item })
}, (item: Recipe) => item.id.toString())

修复清单

文件 位置
pages/FavoriteRecipesPage.ets 收藏列表
pages/RecipeHistoryPage.ets 历史记录列表
pages/SearchPage.ets 搜索结果列表
components/HomeTabContent.ets 首页推荐列表
components/DeviceTabContent.ets 设备列表

Step 4:修复定时器清理问题

RecipeDetailPage 中存在 voiceTimeoutId 语音超时定时器未在 aboutToDisappear 中清理的问题:

typescript 复制代码
// ✅ 修复后:在组件销毁时清理定时器
aboutToDisappear(): void {
  if (this.voiceTimeoutId !== -1) {
    clearTimeout(this.voiceTimeoutId);
    this.voiceTimeoutId = -1;
    logger.debug('RecipeDetailPage', '已清理语音超时定时器');
  }
  // 其他清理逻辑...
}

Step 5:编译验证

bash 复制代码
$ hvigorw assembleHap
> Build success.

所有迁移完成后,编译一次通过,无语法错误,无链接错误。


五、代码增删改清单

文件 操作 职责
entry/src/main/ets/common/utils/Logger.ets 新增 统一日志工具类,单例模式,IS_RELEASE 全局开关
entry/src/main/ets/viewmodel/HomeViewModel.ets 修改 新增 import { logger } + 替换 4 处 console 调用
entry/src/main/ets/pages/RecipeDetailPage.ets 修改 替换 6 处 console + 新增 aboutToDisappear 定时器清理
entry/src/main/ets/pages/**/*.ets (11 个文件) 修改 替换 console 调用为 logger
entry/src/main/ets/components/**/*.ets (3 个文件) 修改 替换 console 调用 + ForEach key 修复
entry/src/main/ets/services/**/*.ets (8 个文件) 修改 替换 console/hilog 调用为 logger
entry/src/main/ets/business/**/*.ets (4 个文件) 修改 替换 console 调用为 logger
entry/src/main/ets/viewmodel/**/*.ets (6 个文件,含 HomeViewModel) 修改 替换 console 调用为 logger
entry/src/main/ets/common/ToastUtil.ets 修改 替换 console.error 为 logger.error
entry/src/main/ets/common/IngredientCamera.ets 修改 替换 console 调用为 logger
entry/src/main/ets/entryability/EntryAbility.ets 修改 替换 hilog 调用为 logger

六、血泪避坑总结

现象 真相 解决方案
迁移后编译报 Cannot find name 'logger' 文件只替换了调用,忘记添加 import 每个迁移的文件都必须加 import { logger } from '../common/utils/Logger'
console.loghilog.info 混用 两套 API 互不通信,无法统一管理 统一使用 logger,底层全走 hilog
ForEach 不写 key,列表滚动卡顿 缺少 diff 依据,整个列表每次全量重绘 指定 key 函数,利用 ID 精确 diff
setTimeout 回调中访问已销毁组件 页面退出了定时器还在跑,回调读 undefined aboutToDisappear 中用 clearTimeout 清理
日志迁移后文件层级混乱 import 路径写错,相对路径不统一 以目标文件所在目录为基准,用 ../common/utils/Logger 计算正确相对路径
IS_RELEASE 改成 true 后忘了改回来 开发时日志全静默,查问题无从下手 hvigorfile 中通过构建参数注入,Debug 自动 false,Release 自动 true

七、设计决策

决策 选择 理由
底层日志引擎 hilog HarmonyOS 系统级日志管道,DevEco Studio Log 窗口原生支持
日志工具封装形式 单例 const logger 全站共享一个实例,避免每个文件 new Logger() 的冗余
全局静默开关 模块级 IS_RELEASE 常量 if (false) return 被引擎优化为零开销,比 if (变量) 更快
module 参数 调用时传入字符串 强制每个日志标注来源,比自动提取调用栈更轻量且可控
日志级别 info / warn / error / debug 四级别覆盖 99% 场景,比 console 的五级别更精简
参数传递 ES6 扩展运算符 logger.info('Mod', 'msg', obj1, obj2) 灵活传参

八、运行验证

验证场景 1:发布模式日志静默

  1. Logger.etsIS_RELEASE 设为 true
  2. 执行 hvigorw assembleHap → 安装到真机
  3. 期望结果 :DevEco Studio Log 窗口无任何 LingxiKitchen-* 日志输出,应用运行流畅

验证场景 2:调试模式日志全开

  1. Logger.etsIS_RELEASE 设为 false
  2. 重新编译并运行
  3. 期望结果 :Log 窗口按 LingxiKitchen-<ModuleName> tag 输出所有日志,过滤方便

验证场景 3:ForEach 性能对比

  1. 在迁移前后分别进入收藏列表页(含有 50+ 条数据)
  2. 快速上下滚动
  3. 期望结果:修复后滚动流畅,DevEco Studio Profiler 显示重绘次数大幅减少

验证场景 4:定时器清理验证

  1. 进入菜谱详情页 → 触发语音识别 → 在 5 秒超时前快速返回
  2. 期望结果 :无崩溃,Log 窗口出现 已清理语音超时定时器 日志

九、总结与下篇预告

本篇我们基于 HarmonyOS 6.1.0(API 23),为《灵犀厨房》完成了一场从 consoleLogger 的日志体系升级。核心要点:

  • 一行开关IS_RELEASE = true → 全网静默,零运行时开销
  • 统一接口logger.info(module, msg, ...args) 替代 console 和 hilog 的所有调用
  • 批量迁移:35+ 个文件、110+ 处调用,编译一次通过
  • 附带收益:ForEach key 修复(5 处)、定时器清理修复(1 处)
  • 架构哲学:日志工具属于 Foundation 层,被所有上层单向依赖

Logger API 速查表

场景 调用
通用信息 logger.info('ModuleName', '数据加载完成', data)
警告信息 logger.warn('ModuleName', '网络延迟较高', latency)
错误信息 logger.error('ModuleName', '接口调用失败', error)
调试信息 logger.debug('ModuleName', '进入 onPageShow')
发布静默 IS_RELEASE = true

📚 本系列持续更新中,敬请期待,下一篇更精彩。

🔗 专栏入口:《HarmonyOS6.1全场景实战》合集
📦 获取基线版本源码包:包括本系列所有代码 + 架构文档 + Flask 后端

如果你觉得这篇文章对你有帮助,请不要吝啬你的点赞 👍、收藏 ⭐ 和评论 💬。你的支持,是我继续输出高质量技术内容的全部动力。

纯血鸿蒙,用心造厨。我们下一篇见!