开源鸿蒙平台 KMP 三方库「Volcano」适配全流程
本文记录
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 / scaleMax | SignedMetricColorScale.legendEntries() 的原样输出。 |
stats | layoutMs、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 Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) | 社区已审仓库(qrose / multiplot / rollingnumbers)同款钉法;volcano 零外部依赖,不存在元数据版本冲突 |
| JDK | 21(Temurin) | jvmToolchain(21),与 Kotlin/Native 工具链一致 |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm("desktop") 保留作交叉验证) | 真机 arm64 + 真机 x86_64 |
| Gradle | 8.14.1(wrapper 替换) | 2.2.21 鸿蒙线配套版本,上游 9.8.0 与该线不兼容 |
| DevEco product | 5.1.0(18) 兼容 | 与已审仓库一致 |
| DevEco Studio | 5.1.0 Release | IDE 版本,负责工程同步、HAP 构建与签名 |
| ABI | arm64-v8a + x86_64 | HAP 双 ABI,缺一装不上 |
| Bundle | org.terminator.ohos.volcano | 与 oh-tpc 其他适配仓库同前缀 |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 模块 | commonMain 文件 | 依赖形态 | 处置 |
|---|---|---|---|
volcano | 8 | 纯 Kotlin,零 UI、零 AGP、零第三方依赖 | 整体编译(jvm + 双 ohos) |
volcano-compose | 7 | Compose Multiplatform 1.12.1 + HeatmapState | 留盘不进构建 |
volcano-compose-coil | 1 | Coil 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 项全部通过(上游原测试文件,零改动):
| 测试类 | 用例数 | 钉住什么 |
|---|---|---|
HeatmapNodeTest | 8 | layoutValue 汇总规则、effectiveMetric 加权、兄弟 id 唯一性、拷贝语义 |
HeatmapTransformTest | 5 | 排序、叶子过滤、Others 聚合、toDisplayTree 路径映射 |
SquarifiedMeasurerTest | 2 | 面积守恒与不重叠 |
第 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 请求字段
| 字段 | 类型 | 默认 | 作用 |
|---|---|---|---|
op | string | layout | 目前只有 layout,未知 op 走 ok:false |
width / height | int | 1080 / 1440 | 画布尺寸,单位 vp |
density | float | 3 | 单位换算系数;ArkUI 侧固定传 1 |
dataset | string | market | market / services / budget |
path | string | "" | 下钻路径,如 technology/samsung(只能是分组) |
sort | string | desc | desc / label / none |
maxChildren | int | 0 | 大于 0 时触发 HeatmapAggregation.maximumChildren |
minFraction | double | 0 | 小于该比例的子项并入 Others |
tick | int | 0 | 行情心跳,按上游 withDemoMetrics 的公式扰动 metric |
stress | bool | false | 5000 叶压测(对应上游 expandForHeatmapStressTest) |
selected | string | "" | 选中叶子 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 方法 | 覆盖内核 |
|---|---|---|
VolcanoLayout | volcanoLayout(req) | 显示树构建 + 递归拍平 + 颜色 + 文本阈值 |
VolcanoVersion | volcanoVersion() | 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)
}
}
三个必须遵守的约束:
- 每个分组一个全新的
SquarifiedMeasurer。上游注释写得很清楚:这个类在测量时会改写自己的工作区,跨层复用会把父层坐标覆盖掉,表现为「只剩表头」。 - 深度上限 4(根=1)。超过之后把分组当成一个格子画出来,点击仍可下钻——避免为了几像素递归出上百个 0 面积单元。
- 最小可嵌套尺寸 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 | 结构 | 用途 |
|---|---|---|
market | Market → 3 板块 → 9 个标的 | 与 examples/react-demo/src/data.ts 逐字一致,三端同图 |
services | Service health → 3 区域 → 3 服务 → 2~3 实例(26 叶) | 三层嵌套 + 聚合,验证递归与 Others |
budget | Budget → 4 部门 → 3 团队 → 2~3 条目(25 叶) | 验证小尺寸下的分组降级 |
stress | market 扩到 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 += 1 | metric 扰动,颜色与图例同步变化 |
| 压测 | 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.toml | Kotlin 2.2.21-1.0.0,只留 kotlin-multiplatform 插件 |
gradle.properties | JDK 21、版本号、Kotlin/Native 开关 |
volcano/build.gradle.kts | 纯 KMP:jvm("desktop") + ohosArm64 + ohosX64 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib(baseName=ohosvolcano)+ copyKnSo* 回填 |
Bridge.kt | 2 个 @CName C ABI |
HeatmapBridge.kt | 递归拍平、文本阈值、颜色、JSON 响应、错误通道 |
Datasets.kt | market(与上游一致)/ services / budget / 5000 叶压测 |
MiniJson.kt | 扁平 JSON 读取 + 转义/数字格式化 |
napi_init.cpp | N-API 导出、参数校验、DisposeString |
CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、三件套拷贝 |
Index.ets | ArkUI 画布验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(19 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | HeatmapNode.layoutValue | 面积权重:子节点有正值时父值让位 |
| 引擎 | SquarifiedMeasurer.measureNodes | squarified treemap,返回矩形列表 |
| 引擎 | HeatmapAggregation.aggregateSmallChildren | 低于阈值合并成 Others |
| 引擎 | toDisplayTree | 排序 + 聚合 + 路径映射 |
| 引擎 | SignedMetricColorScale.colorOf | 负红 / 中灰 / 正绿 |
| Native | VolcanoLayout / VolcanoVersion | C ABI JSON 导出 |
| N-API | volcanoLayout(req) / volcanoVersion() | 向 ArkTS 暴露能力 |
| ArkTS | requestLayout() / 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 项用例全部通过(上游原测试文件,零改动):
| 测试类 | 用例数 | 钉住什么 |
|---|---|---|
HeatmapNodeTest | 8 | layoutValue 汇总规则、effectiveMetric 加权、兄弟 id 唯一性、拷贝语义 |
HeatmapTransformTest | 5 | 排序、叶子过滤、Others 聚合、toDisplayTree 路径映射 |
SquarifiedMeasurerTest | 2 | 面积守恒与不重叠 |
工具链从 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 / OhosX64 | BUILD SUCCESSFUL,源码零改动 |
:volcano:desktopTest | 15 项全绿 |
linkDebugSharedOhosArm64 | libohosvolcano.so 4,118,704 B + libc++_shared.so 1,850,184 B |
linkDebugSharedOhosX64 | libohosvolcano.so 3,812,960 B + libc++_shared.so 1,731,432 B |
llvm-nm -D | VolcanoLayout / VolcanoVersion 为全局 T |
BuildNativeWithCmake | 双 ABI 均产出 libvolcano_napi.so(arm64 64,992 B / x86_64 67,128 B) |
PackageHap | entry-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 侧的桥接冒烟(非真机)记录:
| 场景 | cellCount | layoutMs |
|---|---|---|
market 根层(3 分组 9 叶) | 12 | 1~13 |
services + maxChildren=2(生成 5 个 Others) | 19 | 6.6 |
market 压测 5000 叶 | 5,003 | 49.4 |
真机数值以页面统计条为准(同一份 Kotlin/Native 代码,arm64 上通常更快)。
6.5 功能用例
- 启动后标题右侧显示
v2.0.2-ohos.1(来自真实 Native 调用,非静态文字); - Market 数据集渲染出 3 个板块表头 + 9 个标的格子,面积比例与数值一致(K8 Financial
-2.6、Samsung+2.3、SK Hynix+1.4等); - 点按
Electronic Technology表头下钻,面包屑变为Market / Electronic Technology,格子重排; - 点按叶子(如 Samsung)出现深色选中框,详情卡显示值 48,000、metric +2.3;
- 面包屑点
Market或「上级」可返回; - 切换「前 2」聚合,出现
Others格子且面积等于被合并项之和; - 切到 Service health,三层嵌套正常,hilog 出现
volcanoLayout request bytes=...; - 「刷新行情」后颜色变化而面积不变(metric 扰动,value 不动);
- 「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 踩坑复盘
- 只改 ArkTS 不算适配:必须让
:volcano真的编译出ohosArm64KLIB,否则SquarifiedMeasurer根本没上设备; - eazytec 双仓必须置前:顺序错了配置期就
Unresolved reference: ohosArm64,与依赖是否齐全无关; - Gradle 版本要跟 Kotlin 线匹配:上游 9.8.0 与 2.2.21 鸿蒙线不兼容,必须降到 8.14.1;反过来 2.4.20-0.1.0-07 线才允许 9.x;
SquarifiedMeasurer不能跨层复用:它会在measureNodes里改写left/top/widthLeft/heightLeft,复用会让父层坐标被覆盖,现象是「只剩表头」;- 根层不要画表头:Compose 版根节点
showLabel=false,桥上对depth == 1跳过表头,否则首屏会少掉一条 18vp 的可视高度; - 表头高度必须从可用高度里扣:先扣
18vp再调measureNodes,否则子层会画到表头底下; - 深度不设上限会退化成碎片:1000+ 叶时递归到 6 层以上,单格不足 1vp,既看不见也点不中;上限 4 并用
group格子承载下钻; density与画布单位必须一致:ArkUI Canvas 默认 vp,桥却按 px 传density=3时,字号会被放大 3 倍、阈值全部错位,固定density=1;RepositoriesMode要跟着换:上游FAIL_ON_PROJECT_REPOS会让示例模块的仓库声明直接失败,改成PREFER_PROJECT;- NAPI 的
.d.ts必须与导出名一致:types/libvolcano_napi/index.d.ts少一个函数,ArkTS 侧就是undefined is not a function; - ArkTS 的
arkts-no-any-unknown:const raw = volcanoLayout(...)会被判为隐式 any,必须写const raw: string = ...; - KN 字符串必须
DisposeString:N-API 层free()会破坏 Kotlin 运行时堆,表现为随机崩溃; - HAP 缺当前 ABI 的 so 装不上(
9568347):abiFilters双 ABI,每个 ABI 三件套齐全; - 签名配置不要进仓库:
signingConfigs留空数组,由 DevEco 自动生成回填;提交前用skip-worktree保护; - 根锚定的
.gitignore盖不住嵌套目录:build/只匹配根层,example/harmonyApp/entry/build/、entry/libs/、.hvigor/、entry/.cxx/必须单独追加; 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(ArkUIImage+ 网络能力),补齐 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 三条经验
- 先分清「算法」与「UI」:volcano 的核心是纯函数式的几何计算,零
expect、零第三方依赖,所以它的适配答案是「真编译核心 + 换一层宿主」;Compose 层没有 ohos 制品是生态问题,不是代码问题,留盘比移植更诚实; - 桥的粒度决定宿主的复杂度:把递归、排序、聚合、颜色全部留在 Kotlin 侧,响应只给「画什么」的矩形列表,ArkTS 侧就只剩
fillRect/fillText,升级与排错都在一处; - 单位先定死,再谈复用:
density=1让桥里的数值就是 vp,上游16/40/48dp阈值、字号钳位、56vp 最小嵌套尺寸全部原样复用,少一层换算就少一类错位。
9.4 适配成果
当前适配已完成:
- volcano
2.0.2核心引擎的双 ABI Kotlin/Native 编译(上游commonMain文件零重写、零新增 actual); ohosArm64/ohosX64KLIB + 双 ABIlibohosvolcano.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。
参考文档
更多推荐



所有评论(0)