在这里插入图片描述

HarmonyKit | 鸿蒙开发:hvigor 缓存机制与问题排查手册

引言:缓存是性能的源泉,也是问题的温床

HarmonyKit 开发过程中,约有 20% 的"诡异 bug"最终定位到了 hvigor 缓存问题。这类 bug 的共同特征是:代码逻辑完全正确、语法没有问题、昨天还能正常构建、今天突然就报错了。当你遇到这种"变量型" bug(issue only reproducible sometimes),第一个怀疑对象就应该是缓存。

这篇文章基于 HarmonyKit 项目中实际遇到并解决的 7 个缓存相关案例,系统梳理 hvigor 的缓存体系、缓存失效的触发条件、排查步骤和清理策略。

项目仓库:https://atomgit.com/VON-/harmony-kit

hvigor 缓存体系全景

hvigor 的缓存分布在三个位置,各自缓存不同类型的数据:

第一层:.hvigor/cache/(构建引擎缓存)

在这里插入图片描述

.hvigor/cache/
├── file-cache.json         # 源文件哈希缓存
├── last-build-info.json    # 上次构建的快照信息
├── project-config.json     # 项目配置的解析结果缓存
├── task-cache.json         # 构建任务的执行结果缓存
└── meta.json               # 缓存版本和兼容性元数据

这层缓存的职责是"避免重复工作"。如果文件 A 在上次构建后没有变化,hvigor 从 file-cache.json 中检测到哈希值一致,直接跳过对该文件的编译——即使构建模式从 debug 切换到 release(只要文件本身没变)。

last-build-info.json 记录了上次构建的环境快照:SDK 版本、模块列表、构建模式、时间戳。每次新构建开始时,hvigor 对比当前环境与快照,判断是否需要进行某些全局性的重新处理(如重新解析依赖图、重新处理所有资源)。

第二层:.hvigor/dependencyMap/(依赖图缓存)

在这里插入图片描述

.hvigor/dependencyMap/
├── oh-package.json5            # 项目级依赖的解析结果
├── dependencyMap.json5         # 模块间依赖关系图
└── entry/
    └── oh-package.json5        # entry 模块的依赖解析结果

依赖图缓存的职责是"快速确定受影响范围"。当文件 A 被标记为"脏"(因为内容变化),hvigor 查询依赖图找出所有直接或间接 import 文件 A 的其他文件,将这些文件也标记为"脏"。

依赖图缓存的重要性:HarmonyKit 的 ToolItem.ets 被 Index.ets 和 ToolCard.ets 引用。没有依赖图缓存,hvigor 需要通过 AST 分析每个文件的 import 关系来判断影响范围——这在 20+ 个文件的项目中可能很快,但在 200+ 个文件的项目中就变得昂贵了。

第三层:.hvigor/outputs/records/(构建记录缓存)

在这里插入图片描述

.hvigor/outputs/records/
└── performance-recorder.json   # 构建性能追踪记录

这个文件记录了每次构建的各阶段耗时,用于性能分析和 regressions 检测。它不直接影响构建正确性,但对性能优化很有价值。

第四层:.hvigor/report/(构建报告)

在这里插入图片描述

.hvigor/report/
├── report-202607051507059470.json
├── report-202607051502171150.json
└── ...

每次构建生成一个报告文件,包含构建过程的详细信息。主要用于 CI 环境的构建结果分析和历史对比。本地开发时一般不需要关注。

缓存失效的五种触发场景

场景一:修改 hvigor-config.json5

任何对 hvigor/hvigor-config.json5 的修改都应该触发缓存重置。但实际行为取决于修改的内容:

  • 修改 execution.parallelexecution.typeCheck:不影响文件缓存和依赖图,但影响编译过程的执行方式。hvigor 通常能正确处理增量。
  • 修改 modelVersion:标记大版本变更,hvigor 应自动执行全量重建。
  • 修改 dependencies:影响插件加载,需要重启 daemon 才能生效。

最佳实践:修改 hvigor-config.json5 后,手动执行一次 hvigorw clean && hvigorw assembleHap。不要依赖 hvigor 的自动缓存失效判断。

场景二:切换 SDK 版本

从 API 18 切换到 API 22 时,所有缓存的类型信息和编译配置都过时了。hvigor 理论上应该检测到 targetSdkVersion 的变化并触发全量重建,但在实际中不总是可靠。

HarmonyKit 的迁经验:切换到 API 22 时,第一次增量编译通过了,但运行时出现了 API 行为变化导致的崩溃。执行全量清理重建后问题消失。

rm -rf .hvigor entry/build
hvigorw --no-daemon assembleHap

场景三:重命名或移动源文件

如果你重命名了 RegexTester.etsRegexTool.ets,hvigor 的缓存中仍然记录着旧文件 RegexTester.ets 的哈希值和编译状态。新的 RegexTool.ets 被当作"新文件"编译,但旧的缓存记录可能会导致:

  1. 编译器报错"找不到 pages/tools/RegexTester"——因为 main_pages.json 仍然引用旧路径
  2. 依赖图缓存过时——引用 RegexTester 的 import 语句没有被更新

最佳实践:文件重命名后,在 IDE 中使用"Refactor > Rename"功能(支持的话),或者手动执行一次 hvigorw clean

场景四:Git 分支切换后构建异常

当你在 feature 分支上修改了多个文件,切换到 develop 分支时文件内容变了,但 .hvigor/cache/ 中的哈希值还是 feature 分支的。hvigor 通过文件修改时间戳辅助判断——切换分支会更新文件时间戳,通常能正确触发重编译。

但偶发情况下,Git 操作可能保留文件时间戳(取决于 git checkout 的行为和文件系统实现),导致 hvigor 误判文件未变化。

最佳实践

git checkout develop
hvigorw clean
hvigorw assembleHap

场景五:异常中断的构建

如果在构建过程中按下 Ctrl+C 强制中断,或者在 DevEco Studio 中点了 Stop,部分中间文件可能处于不一致状态。典型的例子:

  • entry/build/intermediates/ 中的中间产物只写了一半
  • .hvigor/cache/task-cache.json 记录了某个任务"执行中"但实际已中断
  • daemon 进程仍然运行但内部状态不一致

这种情况的症状是"上次能构建,Ctrl+C 后就不行了"。

修复方案

# 第一级修复
hvigorw clean
hvigorw assembleHap

# 如果仍然失败,第二级修复
rm -rf .hvigor entry/build
hvigorw --stop  # 停止 daemon
hvigorw --no-daemon assembleHap

最致命的缓存问题:.hvigor 递归生成

这是 HarmonyKit 开发中遇到的最诡异也最具破坏性的缓存问题。

现象

entry/src/main/ets/pages/tools/ 目录下,出现了一个 .hvigor 子目录,其中又包含 cache/dependencyMap/ 等结构。更糟糕的情况:.hvigor 目录递归嵌套——.hvigor 里面还有个 .hvigor,导致目录树无限扩展,最终触发文件系统的递归限制,编译报错 Too many open files

根因

hvigor daemon 在扫描源码目录构建依赖图时,需要确定"哪些目录是模块源码目录"和"哪些目录是缓存目录"。正常情况下,daemon 从项目根目录的 build-profile.json5 中读取 modules[].srcPath 来确定源码目录。

但当 daemon 进程异常重启,或者两个 DevEco Studio 实例同时运行时,daemon 的工作目录可能被错误设置。如果 daemon 的当前工作目录指向了 entry/src/main/ets/ 而不是项目根目录,它将源码目录识别为 ./(即当前目录),将缓存输出到 ./.hvigor/(即 entry/src/main/ets/.hvigor/)。

这之后,下一次构建时 daemon 扫描源码目录发现 .hvigor 子目录,将其中的文件也纳入编译范围——包括缓存文件(JSON)和可能被递归包含的更多文件。

排查与修复

# 1. 定位所有不在项目根目录的 .hvigor 目录
find entry/ -name ".hvigor" -type d

# 2. 删除所有错误的 .hvigor 目录
find entry/ -name ".hvigor" -type d -exec rm -rf {} +

# 3. 检查是否有递归嵌套导致的超大深层目录
find entry/ -maxdepth 50 -type d | wc -l
# 如果行数异常大(超过几百),说明可能仍有残留

# 4. 彻底清理并重建
rm -rf .hvigor entry/build
hvigorw --stop
killall node  # 极端情况,杀掉所有 Node 进程
hvigorw --no-daemon clean
hvigorw assembleHap

预防

  1. 及时清理过期 daemon:开发完成后用 hvigorw --stop 停止 daemon,不要让它一直在后台运行
  2. 避免多实例:不要同时打开两个 DevEco Studio 窗口指向同一个项目
  3. 定期全量重建:每周执行一次 rm -rf .hvigor entry/build && hvigorw assembleHap 可以避免缓存问题的积累

缓存问题排查的通用流程

遇到"代码明明对但构建失败"的问题,按以下步骤排查:

Step 1: 确认不是代码问题

# 检查语法是否完全正确
# 在 DevEco Studio 中查看 Problems 面板
# 确认不是 ArkTS 严格模式导致的类型错误

Step 2: 清理应用级缓存(尝试保留 daemon)

hvigorw clean
hvigorw assembleHap

Step 3: 清理所有缓存(包括 daemon)

rm -rf .hvigor entry/build
hvigorw --stop
hvigorw --no-daemon assembleHap

Step 4: 终极清理(完全从零开始)

rm -rf .hvigor entry/build oh_modules/.hvigor .idea
hvigorw --stop
killall node
ohpm install
hvigorw --no-daemon assembleHap

Step 5: 检查环境问题

# 验证 SDK 安装是否正确
hdc list targets

# 验证 Node.js 版本
node --version

# 验证 ohpm 版本
ohpm --version

# 检查磁盘空间
df -h .

大多数缓存问题在 Step 2 或 Step 3 就能解决。如果问题持续到 Step 4 仍然存在,那很可能不是缓存问题,需要回到代码或环境检查。

build 目录:什么时候该删

entry/build/ 目录包含的是构建产物,而非缓存。删除它是安全且常见的操作:

rm -rf entry/build

应该删除 build/ 的场景

  • 切换 buildMode(debug → release 或反过来)
  • 修改了 module.json5 中的配置(如 abilities、permissions)
  • 修改了资源文件(resources/ 目录下的内容)
  • 修改了签名配置
  • 构建后的 HAP 安装到设备上行为异常

不需要删除 build/ 的场景

  • 仅修改了 .ets 源文件(增量编译会正确处理)
  • 仅修改了 main_pages.json 中的页面列表

删除 build/ vs 删除 .hvigor/ 的区别

  • 删除 build/:清理构建产物,但保留编译缓存(下次构建回退到增量编译的"缓存有效但产物缺失"状态,重新生成所有中间和最终产物,但不需要重新编译未变化的源文件)
  • 删除 .hvigor/:清理编译缓存(下次构建是全量编译,所有文件重新编译)
  • 删除两者:完全的"从零开始"构建

对于日常开发,"先试 hvigorw clean,不行再删 .hvigor/"是最有效率的排查路径。

缓存与 CI/CD 的交互

在 CI/CD 环境中,缓存的策略不同:

应该跨构建保留的缓存

  • oh_modules/:如果 oh-package.json5 没有变化,复用已安装的依赖可以节省安装时间
  • ~/.ohpm/cache/:ohpm 的全局缓存目录(位于用户目录下)

应该每次构建清理的缓存

  • .hvigor/cache/:CI 环境中的文件路径可能每次都不同(如临时目录)
  • entry/build/:CI 的每次构建都应该是干净的

CI 配置示例:

- name: Build
  run: |
    # 清理构建产物(保留依赖缓存)
    rm -rf entry/build .hvigor/cache
    
    # 如果 oh-package.json5 变化了才重装依赖
    ohpm install
    
    # 全量干净构建
    hvigorw --no-daemon assembleHap -p buildMode=release

CI 中使用 --no-daemon 是因为每次 CI 运行结束后环境被销毁,daemon 的常驻没有意义,反而可能因为残留进程导致下一次 CI 运行出现问题。

缓存规模与项目规模的关系

HarmonyKit 是一个小型项目(约 25 个 .ets 文件,~2000 行代码),缓存体积不到 5MB。但大型鸿蒙项目的缓存体积可能达到数百 MB。

缓存规模和项目规模的对应关系(经验值):

项目规模 .ets 文件数 .hvigor/ 体积 构建时间(全量) 构建时间(增量)
小型(如 HarmonyKit) < 50 < 10 MB ~15s ~4s
中型 50-200 10-50 MB ~30s ~8s
大型 200-1000 50-200 MB ~90s ~20s
超大型 > 1000 > 200 MB 2-5 min ~40s

对于中型及以上项目,缓存的重要性显著提升——全量构建可能需要 90 秒甚至更久,增量编译是日常开发体验的生命线。因此,正确的缓存管理在大型项目中不是"可选优化",而是"开发可行性"的前提。

结语

缓存问题是所有构建系统都面临的经典难题——你既要缓存来提高性能,又因为缓存引入了一致性问题。hvigor 作为相对年轻的构建系统,在缓存策略上仍在迭代优化。

对开发者来说,最重要的不是记住每个缓存文件的作用,而是建立一套排查习惯:遇到构建问题先问"是不是缓存的问题?",按清理层级从"最轻"到"最重"逐步排查。大多数时候,hvigorw clean 就是你需要的全部修复。

项目仓库:https://atomgit.com/VON-/harmony-kit

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐