本文记录 volcano 接入 OpenHarmony 的完整过程,覆盖层级热力图算法盘点、ohosArm64 / ohosX64 双目标、Compose 层隔离、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI Canvas 宿主、HAP 构建和真机验收。

volcano 是 Kotlin Multiplatform 的层级热力图 SDK:给定一棵带 value(面积)与 metric(颜色)的 HeatmapNode 树,它用 squarified treemap 算法算出每个单元格的位置与尺寸,并提供排序、Others 聚合、下钻导航与带符号的颜色标尺。上游把能力拆成三个制品:volcano(平台无关核心)、volcano-compose(Compose Multiplatform UI)、volcano-compose-coil(可选的 Coil 图片集成)。

本次适配的核心判断是:能跨平台的是算法,不是 UI。volcano 是零依赖纯 Kotlin,SquarifiedMeasurer、layoutValue 聚合、toDisplayTree、SignedMetricColorScale 全部在 commonMain 里,可以原样编译成 OpenHarmony KLIB,一行源码都不用改;而 volcano-compose 依赖 Compose Multiplatform 1.12.1,鸿蒙 CMP 线(CPF 社区 1.9.x)与 Coil 3 都没有 ohos 制品,这一层在鸿蒙上无法参与构建。因此边界划在:引擎真编译 + ArkUI Canvas 绘制,而不是用 ArkTS 把算法重写一遍。

项目地址: AtomGit/oh-tpc/volcano.git

上游仓库: taewooyo/volcano

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配

volcano 解决的是「把密集的层级数据画成可读的矩形」这一件事:HeatmapNode(value, metric, children) 描述数据,layoutValue 把子节点的面积逐级汇总,SquarifiedMeasurer.measureNodes 返回每个子项的 width/height/offsetX/offsetY,toDisplayTree 负责排序与聚合,SignedMetricColorScale 把 metric 映射成颜色。Compose 层在此之上提供 Heatmap / DefaultHeatmapCell 与 HeatmapState 的下钻状态机。

鸿蒙应用同样需要这套能力:行情板块、服务健康度、预算执行、容量水位都是天然的层级结构。但上游构建(Kotlin 2.4.20 / Gradle 9.8.0 / AGP 9.4.0 / Compose 1.12.1)不会产出任何 OpenHarmony 变体,ohosArm64 目标根本不存在。

如果只把示例页面改成 ArkTS,页面可以画出矩形,却无法证明 SquarifiedMeasurer 在 ARM64 设备上真的跑过、Others 聚合真的按权重合并、颜色真的来自 SignedMetricColorScale。适配需要同时验证布局引擎、聚合与排序、颜色标尺、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失上游没有 ohosArm64() / ohosX64(),必须补双目标才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.4.20 + Gradle 9.8.0 与鸿蒙工具链不在一条线;旧钉法 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1 是社区已审路线。
UI 依赖无制品volcano-compose 依赖 CMP 1.12.1,volcano-compose-coil 依赖 Coil 3.6.3,两者都没有 ohos 变体。
语言边界不同ArkTS 不能持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
布局语义跨不了桥递归显示树、密度相关单位、Compose 的测量与绘制分离,C ABI 只能同步一来一回。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:上游核心的真编译、根构建替换(cmp_qrose 模式)、引擎侧递归拍平、C ABI/N-API 桥接层。布局语义完全由上游原版实现负责,ArkTS 只负责调用 Native、解析 JSON 和用 Canvas 画矩形。

1.2 库提供的能力

Demo 把桥接入口收敛成一个 layout 请求,覆盖引擎的全部关键路径:

  • 布局(layout):SquarifiedMeasurer().measureNodes(...) 算出当前层每个子项的面积与偏移,逐层递归,最终拍平成绘制列表;
  • 聚合(Others):HeatmapAggregation.maximumChildren / minFraction 触发 aggregateSmallChildren,低于阈值的子项合并成一个 Others 叶子,面积与加权 metric 按权重汇总;
  • 排序(sort):HeatmapSort.AreaDescending / LabelAscending / None,逐层生效;
  • 配色(color):NodeColor 优先,否则走 SignedMetricColorScale(maximumAbsoluteMetric = 5.0),负红、中灰、正绿;
  • 下钻(path):请求带 / 拼接的路径,引擎按路径重建显示树,回传 breadcrumbs 与当前 visible;
  • 版本握手:volcanoVersion() 返回 fork 版本号,来自真实 Native 调用而非静态文字。

响应包含:

字段作用
ok / error操作是否成功,失败时 error 带原文。
breadcrumbs当前路径的节点链(id + label),驱动面包屑。
visible当前可见分组:layoutValue、加权 metric、可见子项数。
cells拍平后的绘制列表:矩形、颜色、文本、字号、深度、kind。
legend / scaleMaxSignedMetricColorScale.legendEntries() 的原样输出。
statslayoutMs、cellCount、leafCount、aggregatedCount、treeDepth、maxRenderedDepth。

引擎侧完整能力(squarify 边界条件、layoutValue 汇总、Others 加权、toDisplayTree 路径映射)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 15 项 desktopTest 交叉验证用例,证明 ohos .so 里执行的布局逻辑没有漂移。

1.3 实现适配

维度要求
代码复用volcano/src/commonMain 全部原样编译,不提取、不重写;全仓新增 Kotlin 源码只有桥接层四个文件。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm("desktop") 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);volcano-compose、volcano-compose-coil、src/jsMain 留盘不进构建。
桥接稳定使用 2 个 C ABI 和 UTF-8 扁平 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律版本主体与上游一致,只换工具链线;上游测试零改动全绿。
UI 完整单页覆盖全部桥接入口:数据集切换、面包屑、排序、聚合、行情刷新、5000 叶压测、选中详情。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。volcano 的 Compose 层在鸿蒙上没有制品,ArkTS Demo 承担的是「画布 + 手势」这一层,布局与配色仍由 Kotlin/Native 里的上游引擎计算。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认「核心真编译 / Compose 留盘」、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec 双仓置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 8.14.1
第 3 阶段:桥协议建模     ── 扁平 JSON 载荷、下钻路径字符串、vp 单位、错误通道
第 4 阶段:原生桥接       ── @CName C ABI、递归拍平、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── Canvas 绘制、命中测试、面包屑、排序聚合控件、详情卡
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
volcano/                              上游核心引擎(进 ohos 构建,jvm("desktop") + ohosArm64 + ohosX64)
volcano/src/commonMain/               100% 原样编译,零改动
volcano/src/commonTest/               100% 原样跑(desktopTest)
volcano/src/jsMain/                   Kotlin/JS 专用,留盘不进构建
volcano-compose/                      Compose UI(CMP 1.12.1 无 ohos 制品),留盘不进 settings
volcano-compose-coil/                 Coil 3 集成(无 ohos 制品),留盘不进 settings
example/nativeApp/                    Kotlin/Native 动态库(Bridge / HeatmapBridge / Datasets / MiniJson)
example/harmonyApp/                   DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md        鸿蒙适配说明(中英双份)
README.upstream.md                    上游原文保存
doc/                                  本文与效果图
documentation/ examples/ packages/    上游文档站、React Demo 与 npm 包,留盘不进构建

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :volcano 与 :example:nativeApp,rootProject.name = "ohos_volcano"。上游的 kover / dokka / vanniktech 发布插件、AGP classpath、spotless、binary-compatibility-validator 全部随根构建替换剥离——本 fork 不发 Maven Central,也不需要文档站。

与 cmp_qrose 把 Compose painter 挪到 src/composeMain 不同,volcano 的 UI 本来就在独立模块里,直接不 include 更干净,也更不侵入上游源码。

1.2 工具链矩阵
项目配置选择理由
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)社区已审仓库(qrose / multiplot / rollingnumbers)同款钉法;volcano 零外部依赖,不存在元数据版本冲突
JDK21(Temurin)jvmToolchain(21),与 Kotlin/Native 工具链一致
OpenHarmony 目标ohosArm64 + ohosX64(jvm("desktop") 保留作交叉验证)真机 arm64 + 真机 x86_64
Gradle8.14.1(wrapper 替换)2.2.21 鸿蒙线配套版本,上游 9.8.0 与该线不兼容
DevEco product5.1.0(18) 兼容与已审仓库一致
DevEco Studio5.1.0 ReleaseIDE 版本,负责工程同步、HAP 构建与签名
ABIarm64-v8a + x86_64HAP 双 ABI,缺一装不上
Bundleorg.terminator.ohos.volcano与 oh-tpc 其他适配仓库同前缀
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块commonMain 文件依赖形态处置
volcano8纯 Kotlin,零 UI、零 AGP、零第三方依赖整体编译(jvm + 双 ohos)
volcano-compose7Compose Multiplatform 1.12.1 + HeatmapState留盘不进构建
volcano-compose-coil1Coil 3.6.3 图片加载留盘不进构建

volcano 核心没有任何 expect 声明,也没有平台源码集依赖——它连 Dispatchers 都不碰,全部是纯函数式的几何计算。这意味着 ohos 侧不需要补任何 actual,适配面是一批库里最小的。

值得单独说明的是 TreemapNode 用了 @ExposedCopyVisibility(Kotlin 2.2 起稳定)。如果工具链钉在更老的 2.0/2.1 线会直接编译失败;2.2.21-1.0.0 与上游 2.4.20 在语言特性上同代兼容,实测无需任何 @Suppress。

1.4 页面边界
标题区          VOLCANO · OPENHARMONY / v2.0.2-ohos.1(版本握手行)
统计条          叶 / 格 / 聚合 / 布局耗时(均来自 stats)
数据集          Market / Service health / Budget
面包屑          Market / Electronic Technology + 上级按钮
画布            420vp 高,Canvas 逐格 fillRect + 白描边 + 文本
图例            -5.0 / 0 / +5.0 与 metric ±5
控件            排序(面积/名称/原始)、聚合(不聚合/前 4/前 2)、刷新行情、5000 叶压测
详情            选中叶子的值、metric、kind、depth、尺寸

页面启动时先调 volcanoVersion() 做握手,再把画布宽高(vp)作为请求参数发起第一次 layout。

第 2 阶段:目标与依赖打通

2.1 settings:eazytec 双仓置前
pluginManagement {
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
        google()
        mavenCentral()
    }
}

rootProject.name = "ohos_volcano"
include(":volcano")
include(":example:nativeApp")

两个细节:其一,鸿蒙 Kotlin 插件只发布在 eazytec 仓库,两个仓库必须排在 gradlePluginPortal() 之前,顺序写错配置期会直接报 Unresolved reference: ohosArm64;其二,RepositoriesMode.PREFER_PROJECT 替代上游的 FAIL_ON_PROJECT_REPOS,示例模块自行声明仓库时不被拦截。

2.2 根 build:版本与测试策略
plugins {
    alias(libs.plugins.kotlin.multiplatform) apply false
}

allprojects {
    group = "io.github.taewooyo"
    version = providers.gradleProperty("version").orElse("2.0.2-ohos.1").get()
}

subprojects {
    tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
        .configureEach { enabled = false }
}

Ohos*Test* 全局禁用:CPF 工具链没有 Kotlin/Native 测试运行器,测试统一在 JVM 上跑。这样上游 commonTest 文件零改动保留,ohos 侧只承担编译与链接。

版本目录从上游 24 个版本、17 个插件收敛到一行:

[versions]
kotlin = "2.2.21-1.0.0"

[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
2.3 核心模块收敛为纯 KMP
plugins {
    alias(libs.plugins.kotlin.multiplatform)
}

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)

    jvm("desktop")   // 交叉验证:同一份 commonMain 在 JVM 上跑上游测试
    ohosArm64()
    ohosX64()

    sourceSets {
        commonTest.dependencies {
            implementation(kotlin("test"))
        }
    }
}

上游这段脚本原本挂着 AGP KMP 插件、vanniktech 发布、dokka、Kotlin/JS(nodejs + TypeScript 定义生成),现在收敛成纯 KMP。gradle.properties 同时删掉上游的 POM_* / SONATYPE_* / android.*,显式钉 org.gradle.java.home 指向 JDK 21:

kotlin.code.style=official
org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.java.home=C:\\Program Files\\Eclipse Adoptium\\jdk-21.0.12.101-hotspot

version=2.0.2-ohos.1

kotlin.native.ignoreDisabledTargets=true
kotlin.native.distribution.downloadFromMaven=true
2.4 编译真实引擎(零改源码)
./gradlew :volcano:compileKotlinOhosArm64 :volcano:compileKotlinOhosX64
# BUILD SUCCESSFUL

volcano/src/** 一个字节都没有改。交叉验证:

./gradlew :volcano:desktopTest

15 项全部通过(上游原测试文件,零改动):

测试类用例数钉住什么
HeatmapNodeTest8layoutValue 汇总规则、effectiveMetric 加权、兄弟 id 唯一性、拷贝语义
HeatmapTransformTest5排序、叶子过滤、Others 聚合、toDisplayTree 路径映射
SquarifiedMeasurerTest2面积守恒与不重叠

第 3 阶段:桥协议建模

3.1 JSON 契约
com.taewooyo.volcano.**(上游引擎原样编译)
        ↓ HeatmapNode / layoutValue / SquarifiedMeasurer / toDisplayTree / SignedMetricColorScale
HeatmapBridge.kt(递归拍平 + 文本阈值 + JSON)
        ↓ VolcanoLayout / VolcanoVersion
libohosvolcano.so(Kotlin/Native)
        ↓ volcanoLayout / volcanoVersion
libvolcano_napi.so(C++ N-API)
        ↓ JSON.parse + Canvas 绘制
Index.ets(ArkUI 单页)

请求与响应:

请求 : {"op":"layout","width":420,"height":420,"density":1,"dataset":"market",
        "path":"","sort":"desc","maxChildren":0,"minFraction":0,"tick":0,
        "stress":false,"selected":""}
响应 : {"ok":true,"breadcrumbs":[...],"visible":{...},"cells":[...],
        "legend":[...],"scaleMax":5.0,"stats":{...}}
       {"ok":false,"error":"..."}

请求是扁平 JSON:下钻路径用 / 拼接的字符串,避免在 Kotlin/Native 里引入完整 JSON 解析器(MiniJson 只做一层 key-value 解析)。响应里的 cells 也是一个平铺数组,ArkTS 拿到就是「按绘制顺序排好的矩形列表」。

3.2 请求字段
字段类型默认作用
opstringlayout目前只有 layout,未知 op 走 ok:false
width / heightint1080 / 1440画布尺寸,单位 vp
densityfloat3单位换算系数;ArkUI 侧固定传 1
datasetstringmarketmarket / services / budget
pathstring""下钻路径,如 technology/samsung(只能是分组)
sortstringdescdesc / label / none
maxChildrenint0大于 0 时触发 HeatmapAggregation.maximumChildren
minFractiondouble0小于该比例的子项并入 Others
tickint0行情心跳,按上游 withDemoMetrics 的公式扰动 metric
stressboolfalse5000 叶压测(对应上游 expandForHeatmapStressTest)
selectedstring""选中叶子 id,引擎回填 selected 标记
3.3 单位约定:一切以 vp 为准

ArkUI 的 Canvas 默认坐标系是 vp,ctx.font 的 px 在默认单位下也按 vp 解释。因此桥里固定传 density: 1,于是:

  • 坐标即 vp,ArkTS 的点击坐标可以直接与 cells 的 x/y/w/h 比较;
  • 上游 HeatmapDisplayPolicy 的 16.dp / 40.dp / 48.dp 阈值在桥里就是 16 / 40 / 48,语义与 Compose 版完全一致;
  • 表头高度 18vp、最小可嵌套尺寸 56vp、内边距上限 6vp 都是同一套 vp 数值。

density 参数保留,是为了将来换成 px 画布(LengthMetricsUnit.PX)时不用改协议。

3.4 单元格(cell)
{"id":"samsung","label":"Samsung","kind":"leaf","x":0,"y":54,"w":555,"h":403,
 "color":"#478672","text":"#FFFFFF","value":48000,"metric":2.3,
 "metricText":"+2.3","valueText":"48,000",
 "showLabel":true,"showMetric":true,"labelSize":56.4,"metricSize":48.4,
 "depth":3,"childCount":0,"selected":false}
  • kind:leaf(叶子)、group(因尺寸/深度不足以继续嵌套的分组,可点击下钻)、header(分组标题条,白底深字,与 Compose 版 HeatmapStyle.groupHeaderColor 一致);
  • showLabel / showMetric 由引擎按阈值 + 估算字宽决定,ArkTS 不做二次判断;
  • labelSize / metricSize 已经换算成 vp,直接喂给 ctx.font。

第 4 阶段:原生桥接(技术难点)

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libvolcano_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohosvolcano.so)
  │ VolcanoLayout / VolcanoVersion
  ▼
UTF-8 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制布局语义,所以选择 C ABI + JSON。

4.2 Kotlin/Native 导出函数
@CName("VolcanoLayout")
fun volcanoLayout(request: String): String = HeatmapBridge.dispatch(request)

@CName("VolcanoVersion")
fun volcanoVersion(): String = HeatmapBridge.VERSION      // "2.0.2-ohos.1"
C ABI 符号ArkTS 方法覆盖内核
VolcanoLayoutvolcanoLayout(req)显示树构建 + 递归拍平 + 颜色 + 文本阈值
VolcanoVersionvolcanoVersion()fork 版本握手(真机版本行的数据来源)

llvm-nm -D 验证:

00000000001bbea8 T VolcanoLayout
00000000001bc054 T VolcanoVersion
00000000001d749c T libohosvolcano_symbols
4.3 递归拍平:把 Compose 的 Layout 换成列表

Compose 版是递归 Layout + 每层一个 SquarifiedMeasurer;桥上做同样的事,只是把结果写进一个列表:

private fun walk(node: HeatmapNode, x: Int, y: Int, width: Int, height: Int, depth: Int) {
    val children = node.children.filter { it.layoutValue > 0.0 }
    val nestable = children.isNotEmpty() &&
        depth < MAX_NESTING_DEPTH &&
        width >= minGroupWidth && height >= minGroupHeight
    if (!nestable) { cells += cellOf(node, ...); return }

    var bodyTop = y
    if (depth > ROOT_DEPTH) {                 // 根层不画表头,与 Compose 版 showLabel=false 对齐
        cells += headerCell(node, x, y, width, depth)
        bodyTop = y + headerHeight            // 18vp
    }
    val measured = SquarifiedMeasurer().measureNodes(
        values = children.map(HeatmapNode::layoutValue),
        width = width,
        height = height - (bodyTop - y),
    )
    children.forEachIndexed { index, child ->
        val r = measured[index]
        if (child.isLeaf) cells += cellOf(child, x + r.offsetX, bodyTop + r.offsetY, r.width, r.height, depth + 1)
        else walk(child, x + r.offsetX, bodyTop + r.offsetY, r.width, r.height, depth + 1)
    }
}

三个必须遵守的约束:

  1. 每个分组一个全新的 SquarifiedMeasurer。上游注释写得很清楚:这个类在测量时会改写自己的工作区,跨层复用会把父层坐标覆盖掉,表现为「只剩表头」。
  2. 深度上限 4(根=1)。超过之后把分组当成一个格子画出来,点击仍可下钻——避免为了几像素递归出上百个 0 面积单元。
  3. 最小可嵌套尺寸 56vp。小于这个尺寸时表头就吃掉全部高度,子层没有意义。
4.4 文本可见性、配色与数据集
val minDimension = min(width, height).toFloat()
val labelSize = (minDimension * 0.14f).coerceIn(8f * density, 26f * density)
val metricSize = (minDimension * 0.12f).coerceIn(8f * density, 22f * density)
val showMetric = metric != null && minDimension >= 40f * density
val showLabel = minDimension >= 48f * density &&
    estimatedWidth(node.label, labelSize) <= width - padding * 2f

字号公式、8/26/22 的钳位、40/48 阈值都来自 DefaultHeatmapCell。唯一的替代是「估算字宽」(字符数 × 字号 × 0.56)——Compose 版用 TextMeasurer 精确测量,Kotlin/Native 侧没有字体引擎。差异只体现在「极窄格子是否显示标签」这一条边界上,且最终绘制时 ArkUI 会用 fillText 的 maxWidth 再做一次裁切。

配色:

val colorScale = SignedMetricColorScale(maximumAbsoluteMetric = scaleMax)  // 默认 5.0
val color = node.color?.let(Cell::hex) ?: Cell.hex(colorScale.colorOf(node))

node.color 优先于颜色标尺,与上游 DefaultHeatmapCell 的 color: Long? = node.color 一致;colorOf 内部用 effectiveMetric,所以分组表头/分组格子也有加权颜色。

数据集:

dataset结构用途
marketMarket → 3 板块 → 9 个标的与 examples/react-demo/src/data.ts 逐字一致,三端同图
servicesService health → 3 区域 → 3 服务 → 2~3 实例(26 叶)三层嵌套 + 聚合,验证递归与 Others
budgetBudget → 4 部门 → 3 团队 → 2~3 条目(25 叶)验证小尺寸下的分组降级
stressmarket 扩到 5000 叶对应上游 expandForHeatmapStressTest,验证布局规模

tick 参数复刻上游 withDemoMetrics:metric + sin(tick * 0.8 + index * 1.7) * 5,只扰动「本身有 metric」的节点,所以「刷新行情」按钮改变的是颜色而不是面积。

4.5 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"volcanoLayout", nullptr, NapiVolcanoLayout, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"volcanoVersion", nullptr, NapiVolcanoVersion, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosvolcano_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free():

static void DisposeKnString(const char *raw) {
    libohosvolcano_ExportedSymbols *symbols = libohosvolcano_symbols();
    if (symbols != nullptr && symbols->DisposeString != nullptr) symbols->DisposeString(raw);
}

libohosvolcano_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成的 5000 行头文件:

typedef struct {
  void (*DisposeStablePointer)(void *ptr);
  void (*DisposeString)(const char *string);
} libohosvolcano_ExportedSymbols;

extern libohosvolcano_ExportedSymbols *libohosvolcano_symbols(void);
extern const char *VolcanoLayout(const char *request);
extern const char *VolcanoVersion(void);
4.6 CMake 和类型声明
add_library(ohosvolcano SHARED IMPORTED)
set_target_properties(ohosvolcano PROPERTIES
    IMPORTED_LOCATION ${KN_SO}
    IMPORTED_NO_SONAME TRUE)

add_library(volcano_napi SHARED napi_init.cpp)
target_link_options(volcano_napi PRIVATE "-Wl,--no-undefined" "-Wl,-rpath,\$ORIGIN")
target_link_libraries(volcano_napi PUBLIC libace_napi.z.so libhilog_ndk.z.so ohosvolcano)

add_custom_command(TARGET volcano_napi POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_if_different ${KN_SO} $<TARGET_FILE_DIR:volcano_napi>
    COMMAND ${CMAKE_COMMAND} -E copy_if_different ${KN_SO_DIR}/libc++_shared.so $<TARGET_FILE_DIR:volcano_napi>)

OHOS_ARCH 为空或 libohosvolcano.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath + POST_BUILD 拷贝保证 NAPI、KN、libc++ 三件套同目录,dlopen 时互相可见。

产物依赖关系实测:

NEEDED  libace_napi.z.so / libhilog_ndk.z.so / libohosvolcano.so / libc++_shared.so / libc.so
RUNPATH $ORIGIN:$ORIGIN
export const volcanoLayout: (request: string) => string;
export const volcanoVersion: () => string;

第 5 阶段:ArkUI 宿主

5.1 请求与绘制
const raw: string = volcanoLayout(JSON.stringify(request));
const reply: LayoutReply = JSON.parse(raw) as LayoutReply;
this.cells = reply.cells;
this.draw();

draw() 只做四件事:填白底 → 逐格 fillRect(color) → strokeRect 白色 0.5 描边(对应 Compose 版 0.5dp 白边)→ 按 showLabel / showMetric 用 fillText 写字(带 maxWidth 裁切)。表头左对齐、正文居中,选中格子额外描 2.5vp 深色边框。

画布尺寸变化时用 onAreaChange 重新发起布局,onReady 只做首次绘制——避免「先画后量」导致的错位。

5.2 页面交互
手势目标行为
点按 leaf选中回填 selected,刷新详情卡与高亮
点按 header / group下钻path 追加 id 后重新布局
点按面包屑跳级path 截断到该层
上级按钮返回path 去掉最后一段
排序/聚合/数据集参数改请求字段后重布局(数据集切换会重置 path)
刷新行情tick += 1metric 扰动,颜色与图例同步变化
压测stress 开关5000 叶布局,统计条显示真实 layoutMs 与 cellCount

命中测试从后往前遍历 cells(子格总在父格之后入列),命中即停,保证重叠时取最上层。页面里没有任何 treemap 逻辑——volcano 升级时 ArkTS 一行不用改。

5.3 对比度和错误边界

表头白底深字、正文按 metric 取白色或深色(引擎算好放在 text 字段),保证在红/绿/灰三色上都可读;ok:false 时错误原文进页面提示条。Native 调用本身抛错(理论不该发生)同样被 try/catch 收住,不让页面白屏。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohosvolcano)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/
│       ├── Bridge.kt                         @CName × 2(VolcanoLayout / VolcanoVersion)
│       ├── HeatmapBridge.kt                  递归拍平、文本阈值、颜色、JSON 响应、错误通道
│       ├── Datasets.kt                       market / services / budget / 5000 叶压测
│       └── MiniJson.kt                       扁平 JSON 读取 + 转义/数字格式化
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.volcano、versionName 2.0.2-ohos.1
    └── entry/
        ├── build-profile.json5               externalNativeOptions + abiFilters [arm64-v8a, x86_64]
        ├── libs/{arm64-v8a,x86_64}/          linkDebugShared* 经 copyKnSo* 回填的三件套(gitignore)
        ├── src/main/cpp/CMakeLists.txt       IMPORTED 引入 KN so、$ORIGIN rpath
        ├── src/main/cpp/napi_init.cpp        N-API 导出、参数检查、DisposeString
        ├── src/main/cpp/include/libohosvolcano_api.h
        ├── src/main/cpp/types/libvolcano_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 画布验收页面
6.2 构建、签名和安装
./gradlew :volcano:compileKotlinOhosArm64 :volcano:compileKotlinOhosX64   # KLIB
./gradlew :volcano:desktopTest                                           # 15 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon

签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填带 material 的完整配置)。

hdc list targets -v
hdc -t <设备序列号> install -r \
  entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
  -a EntryAbility -b org.terminator.ohos.volcano -m entry
hdc -t <设备序列号> shell hilog | grep -E "Volcano|VolcanoNapi"

四、完整代码对照

4.1 整体架构

com.taewooyo.volcano.**(上游引擎原样编译)
    │ HeatmapNode / layoutValue / SquarifiedMeasurer / toDisplayTree
    │ SignedMetricColorScale / HeatmapAggregation / HeatmapSort
HeatmapBridge.kt + MiniJson
    │ 递归拍平 → cells[] / breadcrumbs / legend / stats
VolcanoLayout / VolcanoVersion(C ABI)
    │
libvolcano_napi.so
    │ volcanoLayout / volcanoVersion
Index.ets
    │ JSON.parse + Canvas fillRect / fillText
    ▼
ArkUI 真机页面(单页滚动)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建,eazytec 双仓置前,只 include :volcano / :example:nativeApp
build.gradle.kts(根)版本 2.0.2-ohos.1,全局禁用 Ohos*Test*
gradle/libs.versions.tomlKotlin 2.2.21-1.0.0,只留 kotlin-multiplatform 插件
gradle.propertiesJDK 21、版本号、Kotlin/Native 开关
volcano/build.gradle.kts纯 KMP:jvm("desktop") + ohosArm64 + ohosX64
example/nativeApp/build.gradle.kts双 ABI sharedLib(baseName=ohosvolcano)+ copyKnSo* 回填
Bridge.kt2 个 @CName C ABI
HeatmapBridge.kt递归拍平、文本阈值、颜色、JSON 响应、错误通道
Datasets.ktmarket(与上游一致)/ services / budget / 5000 叶压测
MiniJson.kt扁平 JSON 读取 + 转义/数字格式化
napi_init.cppN-API 导出、参数校验、DisposeString
CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、三件套拷贝
Index.etsArkUI 画布验收页面
README.OpenHarmony_CN.md鸿蒙适配主文档(19 节)

4.3 关键 API 对照

层次API作用
引擎HeatmapNode.layoutValue面积权重:子节点有正值时父值让位
引擎SquarifiedMeasurer.measureNodessquarified treemap,返回矩形列表
引擎HeatmapAggregation.aggregateSmallChildren低于阈值合并成 Others
引擎toDisplayTree排序 + 聚合 + 路径映射
引擎SignedMetricColorScale.colorOf负红 / 中灰 / 正绿
NativeVolcanoLayout / VolcanoVersionC ABI JSON 导出
N-APIvolcanoLayout(req) / volcanoVersion()向 ArkTS 暴露能力
ArkTSrequestLayout() / draw()触发布局并刷新 Canvas

4.4 ArkTS 与 Kotlin 的边界

const reply: LayoutReply = JSON.parse(volcanoLayout(JSON.stringify({
  op: 'layout', width: 420, height: 420, density: 1,
  dataset: 'market', path: 'technology', sort: 'desc', maxChildren: 2
}))) as LayoutReply;
// reply.cells 已是绘制列表;reply.breadcrumbs 驱动面包屑
val display = dataset.root.toDisplayTree(        // 上游原版
    path = request.path, sort = request.sort,
    aggregation = aggregationOf(request)
)
val root = display.root
walk(root, 0, 0, request.width, request.height, depth = 1)

两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。

五、关键决策说明

决策 1:真编译核心,而不是 ArkTS 重写

squarify 的边界条件(worst 比值、layoutOrientation 翻转、scaleArea 归一化)非常容易在移植时引入偏移;Others 聚合还涉及 id 去重与加权 metric。让上游原件在设备上跑,desktopTest 15 项就是「没漂」的闸门。而且 volcano 零外部依赖、零 expect,真编译的代价只是补两个目标。

决策 2:Compose 模块整块留盘,不搬进 commonMain

cmp_qrose 把 Compose painter 挪到 src/composeMain 是因为它和编码器在同一个模块;volcano 的 UI 本来就在独立模块,直接不 include 即可,零源码移动。文档里明确写清这一层在鸿蒙上不参与构建,避免审核误判为「漏编」。

决策 3:拍平成绘制列表,而不是让 ArkTS 递归

递归在 Kotlin 侧一次完成,响应里就是「按绘制顺序排好的矩形列表」。好处有三:ArkTS 不需要理解 HeatmapNode 树;命中测试退化成线性扫描;上游升级时 ArkTS 一行不用改。

决策 4:单位统一为 vp,density 只做预留

桥里所有几何与字号都是 vp。ArkUI Canvas 默认 vp,于是点击坐标可以直接与 cell 矩形比较,上游 16/40/48dp 阈值不用换算就能复用。density 参数保留给未来的 px 画布。

决策 5:Kotlin 2.2.21-1.0.0 + Gradle 8.14.1

eazytec 线上有 2.4.20-0.1.0-07 等更新钉法,但 volcano 零外部依赖,没有「跟着依赖升」的压力;选择社区已审仓库最普遍的 2.2.21-1.0.0 + 8.14.1 组合,兼容面最大。@ExposedCopyVisibility 在 2.2 线可用,实测无告警。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x86_64)、JDK 21(Temurin 21.0.12.101-hotspot)、DevEco Studio 5.1.0 Release(SDK 5.1.0(18))、Hvigor / hdc 命令行工具与鸿蒙真机(arm64)。

6.2 JVM 交叉验证

./gradlew :volcano:desktopTest

15 项用例全部通过(上游原测试文件,零改动):

测试类用例数钉住什么
HeatmapNodeTest8layoutValue 汇总规则、effectiveMetric 加权、兄弟 id 唯一性、拷贝语义
HeatmapTransformTest5排序、叶子过滤、Others 聚合、toDisplayTree 路径映射
SquarifiedMeasurerTest2面积守恒与不重叠

工具链从 Kotlin 2.4.20 降到 2.2.21、Gradle 从 9.8.0 降到 8.14.1,测试照跑全绿——这是「换工具链没换语义」的直接证据。

6.3 HAP 验证

:volcano:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / OhosX64: BUILD SUCCESSFUL
llvm-nm -D: VolcanoLayout / VolcanoVersion 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libvolcano_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
检查项结果
:volcano:compileKotlinOhosArm64 / OhosX64BUILD SUCCESSFUL,源码零改动
:volcano:desktopTest15 项全绿
linkDebugSharedOhosArm64libohosvolcano.so 4,118,704 B + libc++_shared.so 1,850,184 B
linkDebugSharedOhosX64libohosvolcano.so 3,812,960 B + libc++_shared.so 1,731,432 B
llvm-nm -DVolcanoLayout / VolcanoVersion 为全局 T
BuildNativeWithCmake双 ABI 均产出 libvolcano_napi.so(arm64 64,992 B / x86_64 67,128 B)
PackageHapentry-default-unsigned.hap 7,204,602 B
HAP 内 so每 ABI 三件套,打包时 strip(arm64 libohosvolcano.so 2,153,744 B)
符号解析NAPI so 的 NEEDED 含 libohosvolcano.so、libc++_shared.so,RUNPATH=$ORIGIN

6.4 布局规模

引擎在每次响应里回传 stats.layoutMs(TimeSource.Monotonic 包裹「显示树 + 拍平」全程)。JVM 侧的桥接冒烟(非真机)记录:

场景cellCountlayoutMs
market 根层(3 分组 9 叶)121~13
services + maxChildren=2(生成 5 个 Others)196.6
market 压测 5000 叶5,00349.4

真机数值以页面统计条为准(同一份 Kotlin/Native 代码,arm64 上通常更快)。

6.5 功能用例

  1. 启动后标题右侧显示 v2.0.2-ohos.1(来自真实 Native 调用,非静态文字);
  2. Market 数据集渲染出 3 个板块表头 + 9 个标的格子,面积比例与数值一致(K8 Financial -2.6、Samsung +2.3、SK Hynix +1.4 等);
  3. 点按 Electronic Technology 表头下钻,面包屑变为 Market / Electronic Technology,格子重排;
  4. 点按叶子(如 Samsung)出现深色选中框,详情卡显示值 48,000、metric +2.3;
  5. 面包屑点 Market 或「上级」可返回;
  6. 切换「前 2」聚合,出现 Others 格子且面积等于被合并项之和;
  7. 切到 Service health,三层嵌套正常,hilog 出现 volcanoLayout request bytes=...;
  8. 「刷新行情」后颜色变化而面积不变(metric 扰动,value 不动);
  9. 「5000 叶压测」后统计条出现 5003 格 与真实 layoutMs,画布呈细密马赛克且不卡死。

6.6 验证结论

布局引擎、聚合与排序、颜色标尺、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的布局会话:版本握手、Market 三层热力图、图例与排序聚合控件(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/volcano.git
cd volcano
export JAVA_HOME="/path/to/jdk-21"
./gradlew :volcano:compileKotlinOhosArm64 :volcano:compileKotlinOhosX64        # klib
./gradlew :volcano:desktopTest                                                 # 15 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.volcano -m entry
hdc shell hilog | grep Volcano

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 不算适配:必须让 :volcano 真的编译出 ohosArm64 KLIB,否则 SquarifiedMeasurer 根本没上设备;
  2. eazytec 双仓必须置前:顺序错了配置期就 Unresolved reference: ohosArm64,与依赖是否齐全无关;
  3. Gradle 版本要跟 Kotlin 线匹配:上游 9.8.0 与 2.2.21 鸿蒙线不兼容,必须降到 8.14.1;反过来 2.4.20-0.1.0-07 线才允许 9.x;
  4. SquarifiedMeasurer 不能跨层复用:它会在 measureNodes 里改写 left/top/widthLeft/heightLeft,复用会让父层坐标被覆盖,现象是「只剩表头」;
  5. 根层不要画表头:Compose 版根节点 showLabel=false,桥上对 depth == 1 跳过表头,否则首屏会少掉一条 18vp 的可视高度;
  6. 表头高度必须从可用高度里扣:先扣 18vp 再调 measureNodes,否则子层会画到表头底下;
  7. 深度不设上限会退化成碎片:1000+ 叶时递归到 6 层以上,单格不足 1vp,既看不见也点不中;上限 4 并用 group 格子承载下钻;
  8. density 与画布单位必须一致:ArkUI Canvas 默认 vp,桥却按 px 传 density=3 时,字号会被放大 3 倍、阈值全部错位,固定 density=1;
  9. RepositoriesMode 要跟着换:上游 FAIL_ON_PROJECT_REPOS 会让示例模块的仓库声明直接失败,改成 PREFER_PROJECT;
  10. NAPI 的 .d.ts 必须与导出名一致:types/libvolcano_napi/index.d.ts 少一个函数,ArkTS 侧就是 undefined is not a function;
  11. ArkTS 的 arkts-no-any-unknown:const raw = volcanoLayout(...) 会被判为隐式 any,必须写 const raw: string = ...;
  12. KN 字符串必须 DisposeString:N-API 层 free() 会破坏 Kotlin 运行时堆,表现为随机崩溃;
  13. HAP 缺当前 ABI 的 so 装不上(9568347):abiFilters 双 ABI,每个 ABI 三件套齐全;
  14. 签名配置不要进仓库:signingConfigs 留空数组,由 DevEco 自动生成回填;提交前用 skip-worktree 保护;
  15. 根锚定的 .gitignore 盖不住嵌套目录:build/ 只匹配根层,example/harmonyApp/entry/build/、entry/libs/、.hvigor/、entry/.cxx/ 必须单独追加;
  16. buildSrc 仍会被编译:它不会进入任何鸿蒙模块,但 Gradle 依旧构建,日志里那行 No valid plugin descriptors 属正常噪声。

8.2 已知问题

  • volcano-compose / volcano-compose-coil 在鸿蒙上不可用:CMP 1.12.1 与 Coil 3 都没有 ohos 制品。需要 Compose 界面的鸿蒙工程应等待 CMP 鸿蒙线升级,或继续用本 Demo 的 Canvas 方案;
  • HeatmapState 的下钻/面包屑语义在桥上是重算一遍(path → 显示树解析),不是持有可变状态;行为与上游一致,但不共享同一个对象;
  • 文本不做精确测量:showLabel 用估算字宽 + 绘制期 maxWidth 裁切,极窄格子的「是否显示标签」可能与 Compose 版有一格之差;
  • 图片(imageUrl / volcano-compose-coil)未接入:Demo 不加载 logo,imageUrl 字段保留在模型里;
  • 悬停 tooltip、长按详情、按压动画属于 Compose 交互,鸿蒙侧以「点击选中 + 详情卡」替代;
  • ArkTS 页面未做横屏/平板适配,画布固定 420vp 高;
  • volcano/src/jsMain 与 androidApp / desktopApp / iosApp / benchmark 目标剥离(源文件留在树里),发布、dokka、kover 一并剥离,本 fork 不发 Maven Central;
  • 上游 .github/workflows/ 保持原样未适配:build.yml / publish.yml / release-verification.yml 仍指向已移出构建的 :volcano-compose、:androidApp、:desktopApp 与 Maven Central 发布,在本 fork 上不可用。

8.3 未来优化方向

  • 增加 .har 形态,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 把 HeatmapPath 的选中/返回栈下沉到桥里,支持「返回键」语义与路径持久化;
  • 用 ArkUI 的 Text 组件做精确测量,替换估算字宽;
  • 接入 imageUrl(ArkUI Image + 网络能力),补齐 logo 渲染;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充真机(x86_64)截图与多设备验收矩阵。

九、总结

9.1 核心难点回顾

上游 volcano(全部 commonMain,零依赖)
    → 根构建替换(cmp_qrose 模式,eazytec 双仓置前)
    → Kotlin 2.2.21-1.0.0 + Gradle 8.14.1
    → ohosArm64 / ohosX64 KLIB(源码零改动)
    → 15 项 desktopTest 交叉验证
    → 递归拍平 + vp 单位 + 文本阈值(HeatmapBridge)
    → C ABI(@CName × 2)
    → C++ N-API(DisposeString 纪律)
    → ArkTS JSON 解析 + Canvas 绘制
    → ArkUI 真机单页验收(下钻 / 聚合 / 压测)

9.2 封装层次

  • 引擎层:上游原版 HeatmapNode、SquarifiedMeasurer、toDisplayTree、SignedMetricColorScale;
  • Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理请求参数、JSON 解析、Canvas 绘制、命中测试与详情卡;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 先分清「算法」与「UI」:volcano 的核心是纯函数式的几何计算,零 expect、零第三方依赖,所以它的适配答案是「真编译核心 + 换一层宿主」;Compose 层没有 ohos 制品是生态问题,不是代码问题,留盘比移植更诚实;
  2. 桥的粒度决定宿主的复杂度:把递归、排序、聚合、颜色全部留在 Kotlin 侧,响应只给「画什么」的矩形列表,ArkTS 侧就只剩 fillRect / fillText,升级与排错都在一处;
  3. 单位先定死,再谈复用:density=1 让桥里的数值就是 vp,上游 16/40/48dp 阈值、字号钳位、56vp 最小嵌套尺寸全部原样复用,少一层换算就少一类错位。

9.4 适配成果

当前适配已完成:

  • volcano 2.0.2 核心引擎的双 ABI Kotlin/Native 编译(上游 commonMain 文件零重写、零新增 actual);
  • ohosArm64 / ohosX64 KLIB + 双 ABI libohosvolcano.so(arm64 4.1MB / x86_64 3.8MB);
  • 2 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • DevEco Stage ArkUI 单页验收页面(数据集切换、面包屑下钻、排序、聚合、行情刷新、5000 叶压测、选中详情);
  • 15 项 JVM 交叉验证全绿(上游原测试文件)+ HAP 构建(双 ABI 六件套 so,约 7.2MB)、签名、设备安装和真机运行;
  • AtomGit 项目文档、OpenHarmony README(中英双份)与本文效果图。

适配过程 FAQ

以下问题来自 ohosArm64 目标接入、NAPI 桥接、HAP 构建与真机验收的实际流程,按出现频率排序。

Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?

  • 确认 DevEco Studio 版本与工程 build-profile.json5 的 compatibleSdkVersion 一致;
  • 在 File > Settings > Build, Execution, Deployment > Ohpm 配置可用的 ohpm 仓库镜像或代理;
  • 删除 oh_modules、oh-package-lock.json5 与 .hvigor 缓存后重新 Sync;
  • 公司网络需要代理时,同时给 ohpm 与 hvigor 配置 http.proxy。

Q2:CMake 报 Missing .../libxxx.so 或找不到 Kotlin/Native 动态库?

  • 先在 example 目录执行 ./gradlew :nativeApp:linkDebugSharedOhosArm64(模拟器用 linkDebugSharedOhosX64);
  • 确认 libxxx.so 与 libc++_shared.so 已复制到 ohos/entry/libs/<abi>/;
  • 检查 entry/build-profile.json5 的 abiFilters 与设备 ABI 是否一致(真机 arm64-v8a,模拟器 x86_64);
  • 清理 entry/.cxx 与 entry/build 后重试。

Q3:ArkTS 报 Cannot find module 'libxxx_napi.so' 或调用返回 undefined?

  • 确认 CMake 的 add_library(<target> SHARED ...) 名称与 import ... from 'libxxx_napi.so' 完全一致;
  • 解包 HAP,确认 libs/<abi>/libxxx_napi.so 已打包;
  • 删除 entry/build、entry/.cxx 与 oh_modules 后重新构建;
  • 检查 module.json5 的 srcEntry 与页面路径是否正确。

Q4:真机安装报 signature verification failed 或 install failed?

  • 在 DevEco Studio 的 Signing Configs 配置证书、Profile 与密码;
  • bundleName 必须与签名 profile 中的 bundle name 一致;
  • 设备需加入 profile 的 deviceIds,调试证书有效期需覆盖当前时间;
  • 用 hdc list targets 确认设备在线,再执行 hdc install -r xxx-signed.hap。

Q5:应用启动白屏、闪退或 NAPI 调用崩溃?

  • 用 hdc hilog | grep -i "napi\|kotlin" 查看原生日志;
  • 确认 libc++_shared.so 与 Kotlin/Native 工具链版本匹配,且 ABI 与设备一致;
  • 检查 C ABI 返回的字符串是否由 Native 侧分配、ArkTS 侧复制后是否调用 XxxFree 释放;
  • 在 DevEco 的 Native 调试配置中附加 lldb,定位空指针与越界访问。

Q6:Gradle 报 ohosArm64 目标不存在或 Kotlin/Native 编译失败?

  • 使用鸿蒙发行版 Kotlin(如 2.2.21-1.0.0),并在 settings.gradle.kts 配置 eazytec 仓库;
  • Gradle 建议 8.x、JDK 21,版本不匹配时先统一工具链;
  • 确认 kotlin { ohosArm64() } 已应用,且 local.properties 的 SDK 路径有效;
  • 清理 ~/.gradle/caches 中对应版本的 Kotlin/Native 缓存后重试。

Q7:HAP 构建报 SDK 版本不匹配或 compatibleSdkVersion 错误?

  • 对齐 DevEco Studio、HarmonyOS SDK 与 build-profile.json5 中的 compatibleSdkVersion;
  • 升级 DevEco Studio 后同步升级 hvigor 与 SDK,避免 modelVersion 与 IDE 不匹配;
  • 命令行构建时设置 DEVECO_SDK_HOME 指向 DevEco 的 sdk 目录。

Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?

  • 使用 DevEco 自带的 node 与 hvigor:
    "$DEVECO/Contents/tools/node/bin/node" "$DEVECO/Contents/tools/hvigor/bin/hvigorw.js" ...;
  • 设置 JAVA_HOME、NODE_HOME、DEVECO_SDK_HOME;
  • 加上 --no-daemon 排除守护进程缓存影响,并清理 entry/build 与 .hvigor。

参考文档

Logo

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

更多推荐