开源鸿蒙平台 CMP 三方库「Kuiver」适配全流程
本文记录
Kuiver接入 OpenHarmony 的完整过程,覆盖图可视化库盘点、Compose ohos 制品线选型、真实引擎编译、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
Kuiver是基于 Compose Multiplatform 的有向图可视化库:给定一张节点/边图,它提供层级布局(Sugiyama 风格:去环、分层、交叉最小化、坐标分配)与力导向布局(Fruchterman-Reingold),自动完成边分类(FORWARD / BACK / CROSS / SELF_LOOP),支持环、自环、平行边与节点拖拽重排。本次适配与社区早期路线(qrose / haze / PreCompose / voyager 的"零依赖内核提取")乃至 KStore 的"kotlinx 制品线真编译"都有本质区别:社区 Maven 上已经出现 Compose 本体的 ohos 制品线(org.jetbrains.compose.*1.12.0-0.1.0-09,全部带ohosArm64/ohosX64Gradle 变体),因此不需要剥离 Compose——整个库连同 Compose runtime / foundation / ui 依赖被原样编译成 OpenHarmony KLIB,一个包含图模型、双布局算法、边分类与 Compose 渲染器的完整图可视化引擎运行在真机上,ArkTS 页面作为验收宿主,通过 2 个 C ABI 符号驱动真实布局引擎出图。
项目地址: AtomGit/oh-tpc/kuiver.git
上游仓库: justdeko/kuiver
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
Kuiver 为 Compose Multiplatform 应用提供"图数据 → 可视化布局"这一件事的完整答案:buildKuiver { } 一行建图,withNode / withEdge / withoutNode 不可变派生,classifyAllEdges 一次 O(V+E) 完成边分类,hasCycles / wouldCreateCycle 回答环语义;布局侧 hierarchical 处理 DAG 与树(含 BACK 边回撤与交叉最小化),forceDirected 处理一般图(弹簧-斥力模拟,确定性初始化)。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套图可视化能力,但上游构建(Kotlin 2.4.20 / Gradle 9.7.1 / AGP 9.4)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以显示几个方块,却无法证明公共布局引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证图模型、布局引擎、边分类、Compose 依赖矩阵、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.4.20 / Gradle 9.7.1 / AGP 9.4 与鸿蒙工具链无法共存于一个 Gradle 构建。 |
| 依赖矩阵最深 | 库本体直接依赖 org.jetbrains.compose.runtime / foundation / ui——Compose 本体必须有 ohos 变体才能解析;这比 KStore 的 kotlinx 四件套深一层。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 交互语义跨不了桥 | 缩放/平移/悬停、LayoutTransition 插值动画是 Compose 状态驱动的 Kotlin 侧概念,C ABI 只能同步一来一回。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:整个库(含 Compose 依赖)的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。布局语义完全由上游原版实现负责,ArkTS 只负责调用 Native、解析 JSON 和绘制结果。
1.2 库提供的能力
Demo 提供一个真实的图布局会话,对应桥的全部入口:
- 版本握手:
kuiverVersion()返回 fork / 上游 / Kotlin / Compose 版本号,来自真实 Native 调用而非静态文字; - 四种预设图:链式(5 节点链)、树形(7 节点三分支)、环形依赖(CI/CD 管道,
Deploy → Test回边成环)、网状(10 节点 + 自环 + 交叉边); - 双算法:层级布局(Sugiyama)与力导向布局(Fruchterman-Reingold)一键切换;
- 方向切换:层级布局的横向 / 纵向流向旋转;
- 节点拖拽:手指按住节点跟手移动,边实时重画;
- 统计与日志:节点/边数量、引擎耗时与含桥接耗时的对照、按时间戳滚动的操作日志。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
algorithm / cycles | 本次使用的算法与图是否含环。 |
nodes | 每个节点的 id / x / y / width / height,坐标即布局引擎输出。 |
edges | 每条边的 id / from / to / type(FORWARD / BACK / CROSS / SELF_LOOP)。 |
stats | nodes / edges / durationMs,引擎侧纯计算耗时。 |
引擎侧完整能力(不可变图结构、wouldCreateCycle、rebuild DSL、平行边、LayoutTransition 动画、渲染器全套)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 140 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的布局逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | core 的全部 commonMain 文件原样编译,不提取、不重写;仅新增 2 个源文件(8 行 ohos actual + 约 15 行布局引擎公开入口)。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 sample/(Compose 示例 App)留盘不进构建。 |
| 桥接稳定 | 使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 版本主体与上游完全一致(Compose 1.12.0 → ohos 线 1.12.0-0.1.0-09,换线不换版),上游测试零改动全绿。 |
| UI 完整 | 单页覆盖全部桥接入口:预设图、双算法、方向、拖拽、统计、日志和版本握手。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。Compose 渲染器已编进
.so但不经桥调用;ArkTS Canvas 承担绘制——这正是当前阶段可验收的边界。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认 Compose ohos 制品线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版、internal 制品放行
第 3 阶段:引擎公开入口 ── 8 行 ohos actual + KuiverLayoutEngine 公开包装(全仓唯二新增源文件)
第 4 阶段:桥协议建模 ── 扁平 JSON 请求/响应、边分类透传、悬空边拦截、错误通道
第 5 阶段:原生桥接 ── @CName C ABI × 2、C++ N-API、参数校验和内存释放
第 6 阶段:ArkUI 宿主 ── 预设图/算法/方向切换、节点拖拽、统计与日志、版本握手
第 7 阶段:HAP 交付 ── 双 ABI 三件套、CMake、签名(DevEco 自动生成)、真机安装
第 8 阶段:验证 ── KLIB 编译 × 2、上游测试 140 项、协议测试 15 项、符号表、真机验收
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
core/ 图可视化库(进 ohos 构建,jvm + ohosArm64 + ohosX64)
core/src/commonMain/ 上游全部模型/布局/渲染/ui/util 源码(零改动)
core/src/commonMain/.../KuiverLayoutEngine.kt 新增:公开入口,转调模块 internal 的 layout()
core/src/ohosMain/.../PlatformDefaults.ohos.kt 新增:expect actual(平移/缩放速度默认值,8 行)
example/nativeApp/ Kotlin/Native 动态库(Bridge + KuiverProtocol + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
*.upstream.gradle.kts 上游原构建留盘备份(不进编译)
docs/ sample/ 上游文档站与 Compose 示例,留盘不进构建
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :core 与 :example:nativeApp,rootProject.name = "ohos_kuiver"。上游的 AGP classpath、Compose 示例 App、热重载插件、vanniktech 发布段全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven) |
| JDK | 21(Homebrew openjdk@21,JAVA_HOME 指向即可) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 9.7.1(沿用上游 wrapper;2.4.20 线支持 Gradle 9.x) |
| Compose(ohos 线) | 1.12.0-0.1.0-09;JVM 侧保持上游 1.12.0(换线不换版) |
| kotlinx | coroutines / coroutines-test 1.11.0-0.1.0-07 |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.kuiver |
export JAVA_HOME="/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home"
java -version
1.3 源码依赖度盘点
| 模块 | commonMain 文件 | 依赖形态 | 处置 |
|---|---|---|---|
core | 39 | Compose runtime / foundation / ui(Dp、DpOffset、@Immutable、Canvas 渲染器) | 整体编译(jvm + 双 ohos,连 Compose 一起) |
example/nativeApp | 3 | :core + kotlin-test | 桥协议与 @CName 导出 |
example/harmonyApp | ArkTS + C++ | N-API + KN so | 验收宿主 |
expect 声明全库只有 1 处:internal expect object PlatformDefaults(平移/缩放速度默认值)。上游给了 5 个平台 actual,ohos 补第 6 个,这就是整个 fork 在库模块里唯一的新平台源文件:
// core/src/ohosMain/kotlin/com/dk/kuiver/renderer/PlatformDefaults.ohos.kt
internal actual object PlatformDefaults {
actual val defaultPanVelocity: Float = 15f
actual val defaultZoomVelocity: Float = 0.05f
}
布局引擎的入口 layout() 是模块 internal(上游由 KuiverViewer 在 Compose 内部驱动),第二个新增源文件把它转成公开 API,供桥调用、也供 JVM 测试钉住:
// core/src/commonMain/kotlin/com/dk/kuiver/KuiverLayoutEngine.kt
fun layoutKuiver(graph: Kuiver, config: LayoutConfig): Kuiver = layout(graph, config)
值得强调的是:两个算法都是确定性纯计算(力导向也是圆形确定性初始化,无随机源),同一输入永远同一输出——这是布局结果能被 JVM 测试精确钉住、再由真机复现的前提。
1.4 页面边界
标题区 KUIVER · OPENHARMONY / 版本握手行(真实 Native 调用返回的四段版本号)
预设区 链式 Chain / 树形 Tree / 环形依赖 Cyclic / 网状 Mesh 四选一胶囊
算法区 层级布局 Hierarchical / 力导向 Force;层级布局下再出 横向/纵向 方向切换
画布区 ArkUI Canvas:节点圆角矩形 + id 标签,FORWARD/CROSS 边实线箭头,
BACK 边红色虚线二次曲线,SELF_LOOP 自环弧;节点按住拖拽跟手移动
状态/统计行 最近一次操作 · 算法 · 含环与否;节点/边数 · 引擎耗时 · 含桥接耗时
日志区 带时间戳的操作日志(最新在前,上限 100 条)
架构脚注 ArkTS → NAPI → C ABI → Kotlin 引擎 分层说明 + AtomGit 仓库/社区链接
页面启动时先调 kuiverVersion() 做握手,再以默认预设(环形依赖)发起首次布局。
第 2 阶段:目标与依赖打通
2.1 制品线验证(本适配的成败手)
Kuiver 是 Compose 库——它的 ohos 适配成败不在 Kotlin 侧,而在 Compose 本体有没有 ohos 变体。对 https://maven.eazytec-cloud.com 逐坐标查 .module 元数据:
| 制品 | 版本 | ohos 变体 | 结论 |
|---|---|---|---|
org.jetbrains.compose.ui:ui(-ohosarm64) | 1.12.0-0.1.0-09 | ✓ ohosArm64 / ohosX64 | Compose 主体线完整 |
org.jetbrains.compose.foundation:foundation(-ohosarm64) | 1.12.0-0.1.0-09 | ✓ | 连带 animation / ui-text / ui-util / foundation-layout |
org.jetbrains.compose.runtime:runtime(-ohosarm64) | 1.12.0-0.1.0-09 | ✓ | 引擎只用其 Dp / 状态原语 |
org.jetbrains.androidx.lifecycle:* / savedstate / navigationevent | 2.11.0 / 1.5.0 / 1.1.1 的 -0.1.0-09 线 | ✓ | ui 的传递依赖齐全 |
org.jetbrains.skiko:skiko | 0.150.1-0.1.0-06 | ✓(编译期依赖) | 不初始化渲染器即可链接 |
org.jetbrains.compose.annotation-internal:annotation | 1.9.1-0.1.0-09(-09 线自引用版本) | ✓ | 纯注解 jar,版本节奏独立 |
org.jetbrains.compose.collection-internal:collection | 1.5.0-0.1.0-09(同上) | ✓ | 同上 |
org.jetbrains.compose.runtime:runtime-annotation | 声明 1.12.0-0.1.0-09,从未发布 → 钉 1.9.2-20260525000011 | ✓ | 唯一真缺口,注解 jar 向后兼容 |
org.jetbrains.kotlinx:kotlinx-coroutines-core | 1.11.0-0.1.0-07 | ✓ | 与 KStore 同线 |
三个关键结论:其一,Compose ohos 制品线存在且完整,"剥离 Compose"的历史理由消失了——这是本次适配与前几批 CMP 库的分水岭;其二,*-internal 注解/集合 jar 有自己的版本节奏,仓库发布的是 -09 线自引用的那些版本,依赖改写规则必须放行它们声明的原始版本;其三,runtime-annotation 是制品线上真正的破洞,需要单独钉到其独立版本线(详见 8.1 踩坑复盘)。
2.2 工具链钉版(为什么是 2.4.20-0.1.0-07)
社区早期钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 用的)。但 Compose ohos 线 1.12.0-0.1.0-09 基于 Kotlin 2.4.20 构建,coroutines 1.11.0-0.1.0-07 的 stdlib 元数据同样要 2.4 系编译器才能读。工具链版本跟着依赖走:Kotlin 钉 2.4.20-0.1.0-07。上游源码本来就是 Kotlin 2.4.0 编的,2.4.20 与之同代,源码级兼容成本为零;Gradle 9.7.1 上游 wrapper 原样沿用(2.4.20 线支持 Gradle 9.x,不需要像 2.2.21 旧线那样降到 8.14.1)。Compose 编译器插件必须同线:org.jetbrains.kotlin.plugin.compose 同样钉 2.4.20-0.1.0-07。
settings.gradle.kts 把 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()
}
}
rootProject.name = "ohos_kuiver"
include(":core")
include(":example:nativeApp")
2.3 编译真实引擎(连 Compose 一起)
core/build.gradle.kts 收敛为纯 KMP 模块,ohos 配置的依赖经解析规则改写到社区线,JVM 保持上游原版:
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {
api("org.jetbrains.compose.runtime:runtime:1.12.0")
api("org.jetbrains.compose.foundation:foundation:1.12.0")
api("org.jetbrains.compose.ui:ui:1.12.0")
}
// commonTest / jvmTest 同上游
}
}
// OHOS 变体走社区制品线,JVM 保持上游同版本制品——换线不换版。
// *-internal jar 版本节奏独立且仓库确实发布其声明版本,放行原始版本;
// runtime-annotation 被 -09 线引用却从未发布,钉到其独立 1.9.x 线。
configurations.configureEach {
if (name.contains("ohos", ignoreCase = true)) {
resolutionStrategy.eachDependency {
val internal = requested.group.endsWith("-internal")
if (!internal && requested.group.startsWith("org.jetbrains.compose")) {
useVersion("1.12.0-0.1.0-09")
}
if (requested.group == "org.jetbrains.compose.runtime" &&
requested.name.startsWith("runtime-annotation")) {
useVersion("1.9.2-20260525000011")
}
if (requested.group == "org.jetbrains.kotlinx" &&
requested.name.startsWith("kotlinx-coroutines")) {
useVersion("1.11.0-0.1.0-07")
}
}
}
}
实测:
./gradlew :core:compileKotlinOhosArm64 :core:compileKotlinOhosX64 # BUILD SUCCESSFUL
图模型、双布局算法、边分类、渲染器全套 39 个 commonMain 文件一个没动,连同 Compose 依赖一起进了 ohos KLIB。
2.4 测试依赖的处置:coroutines-test 没有 ohos 制品
commonTest 的依赖表对 ohos 测试编译同样可见,kotlinx-coroutines-test 解析不到 ohos 变体就会卡死测试编译。处置是两条纪律的组合:测试统一在 JVM 跑,根构建全局禁用 ohos 测试任务:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
这样上游测试文件零改动保留,ohos 侧只承担编译与链接。
第 3 阶段:桥协议建模
3.1 JSON 契约
io.github.justdeko/kuiver(上游引擎 + Compose,原样编译)
↓ buildKuiver / layoutKuiver / classifyAllEdges / hasCycles
KuiverProtocol.kt(扁平 JSON 进出,JVM 测试与设备执行同一份)
↓ UTF-8 JSON
N-API kuiverDispatch() / kuiverVersion()
↓
ArkUI Index.ets(画布绘制 + 交互)
请求与响应:
请求 : {"op":"layout","algorithm":"hierarchical"|"force",
"direction":"horizontal"|"vertical",
"levelSpacing":140,"nodeSpacing":90, // 层级布局参数(dp)
"iterations":200,"repulsion":500,
"attraction":0.02,"damping":0.85, // 力导向参数
"width":1000,"height":760, // 画布(dp)
"nodes":[{"id":"Build","width":72,"height":40}],
"edges":[{"from":"Build","to":"Test"}]} // 可选 "id" 支持平行边
响应 : {"ok":true,"op":"layout","algorithm":"hierarchical","cycles":true,
"nodes":[{"id":"Build","x":43.0,"y":360.0,"width":72,"height":40}],
"edges":[{"id":"Build->Test","from":"Build","to":"Test","type":"FORWARD"}],
"stats":{"nodes":5,"edges":5,"durationMs":1}}
失败 : {"ok":false,"error":"Edge A -> GHOST references unknown node GHOST"}
3.2 布局结果为什么能直接透传
桥内布局这样跑:
val graph = buildKuiver { /* 节点/边逐条加入 */ }
val laidOut = layoutKuiver(graph, config) // 真实引擎:hierarchical 或 forceDirected
val classified = laidOut.classifyAllEdges() // 统一补一次 O(V+E) 边分类
val cycles = laidOut.hasCycles()
两个设计取舍:其一,层级布局在引擎内部已做边分类、力导向不做,桥对两种算法统一跑一次 classifyAllEdges(),保证 type 字段永远有值——分类本身就是引擎公开 API;其二,悬空边前置拦截——引擎的构建器容忍引用不存在节点的边(布局会静默跳过),桥在构建时校验 from / to 必须在节点表内,调用方把节点 id 写错会立刻得到带节点名的明确报错,而不是少一条边。桥全程无会话状态,每次 dispatch 独立建图、独立布局。
3.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:
fun dispatch(request: String): String = try {
// op 分发
} catch (t: Throwable) {
"""{"ok":false,"error":${MiniJson.string(t.message ?: t.toString())}}"""
}
非法 JSON、缺 op、未知算法、非正画布、空节点表、悬空边,全部走同一个 ok:false 通道,ArkTS 侧统一渲染进状态行与日志。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libkuiver_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohoskuiver.so)
│ KuiverDispatch / KuiverVersion
▼
UTF-8 JSON + DisposeString
4.2 Kotlin/Native 导出函数
@CName("KuiverDispatch")
fun kuiverDispatch(request: String): String = KuiverProtocol.dispatch(request)
@CName("KuiverVersion")
fun kuiverVersion(): String = KuiverProtocol.version()
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
KuiverDispatch | kuiverDispatch(req) | 建图 + layoutKuiver(层级/力导向)+ 边分类 + 环检测 |
KuiverVersion | kuiverVersion() | fork 版本握手(真机版本行的数据来源) |
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"kuiverDispatch", nullptr, NapiKuiverDispatch, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"kuiverVersion", nullptr, NapiKuiverVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohoskuiver_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohoskuiver_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。
4.4 CMake 和类型声明
add_library(ohoskuiver SHARED IMPORTED)
set_target_properties(ohoskuiver PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(kuiver_napi SHARED napi_init.cpp)
target_link_libraries(kuiver_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohoskuiver)
OHOS_ARCH 为空或 libohoskuiver.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const kuiverDispatch: (request: string) => string;
export const kuiverVersion: () => string;
第 5 阶段:ArkUI 宿主
5.1 握手和触发布局
aboutToAppear(): void {
const v = JSON.parse(kuiverVersion()) as VersionResponse;
this.versionLine = `Kuiver ${v.version} · upstream ${v.upstream} · Kotlin ${v.kotlin} · Compose ${v.compose}`;
this.relayout('启动初始布局');
}
private relayout(reason: string): void {
const started = Date.now();
const res = JSON.parse(kuiverDispatch(JSON.stringify(request))) as LayoutResponse;
const wallMs = Date.now() - started; // 含桥接墙钟,与 res.stats.durationMs 引擎耗时对照
this.renderNodes = res.nodes;
this.renderEdges = res.edges;
this.draw();
}
状态行的双耗时对照(引擎 1ms / 含桥接 9ms)让"JSON 序列化 + FFI 往返"的成本每次都可见——这是桥接方案自身开销的持续度量。
5.2 页面交互
画布绘制顺序:先边后节点。节点为圆角矩形 + id 标签;边从节点中心连线出发、在两端矩形边界裁剪,箭头沿边方向绘制;BACK 边为红色虚线二次贝塞尔曲线(控制点垂直边方向偏移),SELF_LOOP 为节点顶边半圆弧。坐标变换是"图空间 → 画布像素"的自适应缩放:按图包围盒算 scale 与偏移,绘制与触控共用同一套变换(逆变换把手指坐标换回图空间做节点命中)。四种预设、双算法、横纵方向都是一次 relayout 派发;节点拖拽在 Move 事件里改图空间坐标并重画。
5.3 对比度和错误边界
BACK 边红色、FORWARD 蓝色、CROSS 灰色的三色区分让边分类一眼可读;ok:false 时错误原文进状态行与日志。Native 调用本身抛错(理论不该发生)同样被 try/catch 收进日志区。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohoskuiver)+ copyKnSo*
│ └── src/
│ ├── commonMain/kotlin/kuiver/ohos/ KuiverProtocol + MiniJson(JVM 测试共享)
│ ├── commonTest/kotlin/kuiver/ohos/ 协议测试 15 项
│ └── ohosMain/kotlin/kuiver/ohos/ Bridge.kt(@CName × 2)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.kuiver、versionName 0.4.2-ohos.1
└── entry/
├── build-profile.json5 externalNativeOptions + abiFilters [arm64-v8a, x86_64]
├── libs/{arm64-v8a,x86_64}/ link 任务经 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/libohoskuiver_api.h
├── src/main/cpp/types/libkuiver_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 验收页面
6.2 构建、签名和安装
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# copyKnSo* 自动回填 example/harmonyApp/entry/libs/{arm64-v8a,x86_64}
cd example/harmonyApp
export DEVECO_SDK_HOME=~/ohos/command-line-tools/sdk
~/ohos/command-line-tools/bin/hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组)。
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.kuiver -m entry
四、完整代码对照
4.1 整体架构
com.dk.kuiver/**(上游引擎 + Compose 渲染器,原样编译)
│ Kuiver 图模型 / hierarchical / forceDirected / classifyAllEdges
KuiverLayoutEngine.kt(公开入口,全库唯二新增源文件之一)
│ layoutKuiver(graph, config)
KuiverProtocol.kt + MiniJson.kt
│ KuiverDispatch / KuiverVersion
libkuiver_napi.so
│ kuiverDispatch / kuiverVersion
Index.ets
│ JSON parse + Canvas 绘制 + 触控逆变换
▼
ArkUI 真机页面(单页滚动)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_kuiver、sample 移出 |
gradle/libs.versions.toml | Kotlin / Compose 编译器插件钉 2.4.20-0.1.0-07 |
build.gradle.kts(根) | 版本 0.4.2-ohos.1、全局禁用 Ohos*Test* 任务 |
core/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,ohos 换线解析规则 |
core/src/ohosMain/.../PlatformDefaults.ohos.kt | 8 行平台 actual |
core/src/commonMain/.../KuiverLayoutEngine.kt | 布局引擎公开入口(约 15 行) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、copyKnSo* 回填三件套 |
nativeApp/.../KuiverProtocol.kt | 桥协议:建图、布局、分类、统计、错误通道 |
nativeApp/.../MiniJson.kt | 零依赖 JSON 解析/生成(跨 JVM 与 ohos) |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(13 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | buildKuiver { } | DSL 建图,节点/边逐条加入 |
| 引擎 | layoutKuiver(graph, config) | 公开入口 → internal layout() 分发 |
| 引擎 | hierarchical | 去环 → 分层 → 交叉最小化 → 坐标分配(Sugiyama) |
| 引擎 | forceDirected | 弹簧-斥力模拟,确定性圆形初始化 |
| 引擎 | classifyAllEdges | O(V+E) 边分类(FORWARD/BACK/CROSS/SELF_LOOP) |
| 引擎 | hasCycles | 环检测(响应 cycles 字段) |
| Native | KuiverDispatch / KuiverVersion | C ABI JSON 导出 |
| N-API | kuiverDispatch(req) / kuiverVersion() | 向 ArkTS 暴露能力 |
| ArkTS | relayout(reason) | 触发布局并刷新画布/统计/日志 |
4.4 ArkTS 与 Kotlin 的边界
const res = JSON.parse(kuiverDispatch(JSON.stringify({
op: 'layout', algorithm: 'hierarchical', direction: 'horizontal',
width: 1000, height: 760,
nodes: [{ id: 'Build', width: 72, height: 40 }, /* … */],
edges: [{ from: 'Build', to: 'Test' }, /* … */]
}))) as LayoutResponse;
// res.nodes[].x/y 即引擎输出坐标;res.edges[].type 驱动红虚线/自环样式
val laidOut = layoutKuiver(graph, config)
val classified = laidOut.classifyAllEdges()
// 节点坐标、边类型、环标志、耗时统计一次序列化回 ArkTS
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:真编译整库连 Compose 一起,而不是抽纯 Kotlin 内核
早期仓库抽内核,是因为 ohos 上没有它们依赖的制品——qrose 把 Compose 画笔搬进不编译的 composeMain,KStore 验证了 kotlinx 制品线后实现"引擎真编译"。本次先验证 Compose 本体制品线(runtime / foundation / ui 及传递依赖全部有 ohos 变体),障碍再次消失——再剥离 Compose 等于把渲染器代码搬出编译面,未来鸿蒙端 Compose 渲染管线就绪时还得搬回来。真编译让图模型、双布局算法、边分类、渲染器全部是上游原件,.so 里躺着的 KuiverGraph 可组合项就是未来升级的余地。
决策 2:换线不换版 + internal 放行 + runtime-annotation 单独钉
JVM 侧依赖保持上游 1.12.0 原版制品,ohos 侧经解析规则改写为社区 1.12.0-0.1.0-09——版本主体一致,上游测试照跑。规则内部再分两档:*-internal 注解/集合 jar 版本节奏独立且仓库确实发布其声明版本,放行原始版本(一条 endsWith("-internal") 判断);runtime-annotation 被 -09 线引用却从未发布,钉到其独立 1.9.x 线的 1.9.2-20260525000011(纯注解 jar,语义兼容),且匹配要覆盖带平台后缀的名称。
决策 3:根构建替换(cmp_qrose 模式),sample/docs 留盘
上游根构建(AGP classpath、Compose 示例 App、热重载、vanniktech 发布)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。sample/ 是完整 Compose 示例 App,鸿蒙侧由 example/harmonyApp 承担验收职责,上游 sample 与 docs/ 留盘不进 settings.gradle.kts。
决策 4:布局引擎为桥出口,渲染归 ArkTS
制品线能编译不等于有可用的设备端渲染管线(skiko-ohos 尚在演进)。桥出口选在布局引擎:坐标与边分类是纯数据、可 JSON 化、可被任意 UI 复用;ArkTS Canvas 承担绘制与触控。这个切面同时是库的核心价值所在——图可视化的难点从来是"算出每个节点放哪",而不是"画一个圆角矩形"。
决策 5:双测试闸门(上游 140 项 + 协议 15 项)
引擎逻辑在 commonMain,JVM 与 ohos 跑同一份代码::core:jvmTest 的 140 项跑的是上游原测试文件,钉住"引擎没漂";协议实现放在 nativeApp 的 commonMain,:example:nativeApp:jvmTest 的 15 项跑的是与设备 .so 内同一个 KuiverProtocol 对象,钉住"桥没漂"。两个闸门加起来,真机上每个坐标都有 JVM 侧的对应断言。
六、测试与验证
6.1 测试环境
本次验证使用 macOS(arm64)、JDK 21(Homebrew openjdk@21)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
./gradlew :core:jvmTest :example:nativeApp:jvmTest
155 项用例全部通过:
| 测试类 | 用例数 | 钉住什么 |
|---|---|---|
上游 core commonTest + jvmTest | 140 | 图模型语义、层级/力导向布局行为、边分类、交互计算(上游原测试文件,零改动) |
KuiverProtocolTest | 15 | 桥协议契约:版本握手、层级链式坐标与层距(150dp 精确断言)、纵向方向、环检测与 BACK 分类、自环分类、平行边、力导向有限性、五类错误通道、统计字段 |
依赖换线没有 API 漂移:Compose 1.12.0 换成 1.12.0-0.1.0-09 线后版本主体不变,上游 140 项照跑全绿——这正是"换线不换版"的直接证据。协议测试里"链式 A→B→C 的 B.x − A.x == 150"这类精确断言之所以可能,是因为两个算法都是确定性纯计算。
6.3 HAP 验证
:core:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
:example:nativeApp:jvmTest: 15 项全绿
linkDebugSharedOhosArm64 / OhosX64: BUILD SUCCESSFUL(8m 7s)
llvm-nm -D: KuiverDispatch / KuiverVersion 均为全局 T 符号(双 ABI)
CompileArkTS: finished
BuildNativeWithCmake: finished(双 ABI 编出 libkuiver_napi.so)
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
entry-default-unsigned.hap 约 53 MB;debugShared 未 strip 的 libohoskuiver.so arm64 ≈63 MB、x86_64 ≈57 MB(内含完整 Compose 运行时,这是"整库连 Compose 一起编译"的直接代价与证明——抽内核路线的 .so 只有 4~5 MB),hvigor 打包时自动 strip,入包后 arm64 ≈27.6 MB、x86_64 ≈22.6 MB。HAP 内双 ABI 三件套(libkuiver_napi.so / libohoskuiver.so / libc++_shared.so)齐全。
6.4 功能用例
- 启动后标题下出现版本握手行
Kuiver 0.4.2-ohos.1 · upstream 0.4.2 · Kotlin 2.4.20-0.1.0-07 · Compose 1.12.0-0.1.0-09(来自真实 Native 调用,非静态文字); - 默认「环形依赖」预设出图:Build → Test → Package → Deploy 横向层级流,
Deploy → Test回边呈红色虚线曲线,状态行显示「含环」; - 切换「链式 / 树形 / 环形依赖 / 网状」四个预设均重新布局,网状预设含 SELF_LOOP 自环弧与灰色 CROSS 边;
- 切换「力导向 Force」:同一图换算法重排,统计行的引擎耗时随之变化;
- 层级布局下切换「横向 / 纵向」,流向旋转 90°;
- 手指按住节点拖动,节点跟手移动、相邻边实时重画;
- 统计行显示
节点 5 · 边 5 · 引擎 1ms · 含桥接 9ms——引擎纯计算与含 FFI 往返的墙钟对照; - 操作日志按时间戳滚动留痕(如
[17:02:34] ✓ 启动初始布局: hierarchical, 5 节点 / 5 边, 引擎 1ms)。
6.5 验证结论
图模型、双布局引擎、边分类、Compose 依赖矩阵、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的图布局会话:版本握手、层级/力导向双算法、横纵方向、环检测与 BACK 边样式、自环、节点拖拽与双耗时统计(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/kuiver.git
cd kuiver
export JAVA_HOME="/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home"
./gradlew :core:compileKotlinOhosArm64 :core:compileKotlinOhosX64 # klib
./gradlew :core:jvmTest :example:nativeApp:jvmTest # 155 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
export DEVECO_SDK_HOME=~/ohos/command-line-tools/sdk
~/ohos/command-line-tools/bin/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.kuiver -m entry
hdc shell hilog | grep Kuiver
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:core真正编译成ohosArm64/ohosX64;对 Compose 库还要更进一步——Compose 本体必须整体过线,抽内核等于把库的一半留在编译面外; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;*-internal制品不能被通用改写规则误伤:annotation-internal/collection-internal被 -09 线以自有版本(1.9.1 / 1.5.0-0.1.0-09)引用且仓库确实发布了这些版本——"所有org.jetbrains.compose.*一律改写到主版本"会让它们解析到不存在的坐标,规则要加endsWith("-internal")放行;runtime-annotation是制品线的真破洞:被 -09 线声明为1.12.0-0.1.0-09但从未发布;钉到独立线1.9.2-20260525000011,且匹配必须用startsWith("runtime-annotation")覆盖runtime-annotation-ohosarm64等带后缀子制品;- Compose 编译器插件版本必须与 Kotlin 同线(同钉
2.4.20-0.1.0-07),错线的症状是编译期静默失败或测试期才炸; String.format在 Kotlin/Native 不存在,通用代码要用手写十六进制格式化;字符字面量里的控制字符要显式转义;- 上游构建器容忍悬空边、布局静默跳过——桥协议层必须前置校验,否则调用方一个拼写错误就"少一条边"而无任何报错;
kotlinx-coroutines-test没有 ohos 制品:测试统一 JVM 跑 + 根构建禁用Ohos*Test*,上游测试文件零改动;- Gradle 9.x 守护进程可能因工作目录失效报
could not setcwd(),./gradlew --stop后重跑即可; - ArkTS 严格模式拒绝未类型化对象字面量与字面量返回类型——请求结构要声明接口,
.map()回调里也要给字面量落类型;List的直接子组件必须是ListItem; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - 签名材料由 DevEco 自动写入
build-profile.json5,仓库内保持signingConfigs: []空数组,提交前防止证书进入 AtomGit; - debug
.so带 Compose 运行时体积很大(arm64 ≈63 MB),好在 hvigor 打包自动 strip;追求更小体积改链linkReleaseShared*。
8.2 已知问题
- 渲染不是设备端 Compose:库里的 Compose 渲染器已编进
.so但不经桥调用,ArkTS Canvas 承担绘制;skiko-ohos 渲染管线可用后可再评估; - 交互子集:上游的缩放/平移/悬停/多选与布局动画不跨 C ABI,Demo 提供节点拖拽作为交互性代表;
- 边几何(
EdgePathFactory锚点、箭头裁剪)未走桥:Demo 的边绘制是中心连线 + 矩形边界裁剪的简化几何,完整几何随 KLIB 在设备上可用,后续可加op=edges出口; - 桥无会话状态、无订阅式推送:布局变更由 ArkTS 主动触发 dispatch;
- android / ios / js / wasmJs 目标与发布链路随根构建替换剥离(源文件留在树里),本 fork 不发 Maven Central;
commonTest依赖kotlinx-coroutines-test(无 ohos 制品),ohos 测试任务全局禁用——引擎正确性由 JVM 侧同一份代码的测试背书。
8.3 未来优化方向
- 增加
op=edges桥接入口,透传EdgePathFactory的真实边几何(锚点、弯曲、自环路径命令); - 布局动画跨桥:把
LayoutTransition的插值按帧拉取或由 ArkTS 侧驱动插值; - 评估 skiko-ohos 成熟后直接跑 Compose 渲染器,ArkTS 退化为承载视图;
- 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。
九、总结
9.1 核心难点回顾
上游 kuiver(Compose 库,commonMain 全量)
→ 验证 Compose 本体 ohos 制品线(1.12.0-0.1.0-09,含 internal 缺口)
→ 工具链对齐 Kotlin 2.4.20-0.1.0-07(Gradle 9.7.1 沿用)
→ 根构建替换(cmp_qrose 模式)+ 换线不换版解析规则
→ 整库连 Compose 编译 ohosArm64 / ohosX64 KLIB(唯二新增源文件)
→ C ABI(@CName × 2)+ 扁平 JSON 布局协议
→ C++ N-API(DisposeString 纪律)
→ ArkTS Canvas 绘制与触控逆变换
→ ArkUI 真机单页验收(双算法 / 方向 / 拖拽 / 双耗时 / 环检测)
9.2 封装层次
- 引擎层:上游原版
Kuiver图模型、hierarchical/forceDirected、边分类与环检测; - Compose 层:runtime / foundation / ui 随库整体编译进
.so(当前不经桥调用,是未来渲染升级的余地); - Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、JSON 校验、布局分发、画布绘制、触控与日志;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 先验证生态制品线再定适配路线:qrose 时代抽内核、KStore 时代 kotlinx 过线、本次 Compose 本体过线——每次路线选择都基于当天的制品可达性,而不是历史结论;对 Compose 库,"Compose 有没有 ohos 变体"是第一个要查的问题,答案决定整条路线;
- 换线规则要认识制品线的内部节奏:社区线不是上游版本的均匀重发布——
*-internal有自己的版本号且确实发布、runtime-annotation声明了却没发布;一条通用改写规则必然踩中其一,例外规则要跟.module元数据逐坐标对账; - 确定性引擎让桥可被精确钉住:两个布局算法都是纯函数,同一输入永远同一输出,JVM 侧才能用"层距恰为 150dp"级别的断言钉住协议;155 项双闸门测试(上游 140 + 协议 15)跑的是与设备
.so相同的代码,"换线不换版"由此才有说服力。
9.4 适配成果
当前适配已完成:
- Kuiver
0.4.2真实引擎(图模型 + 双布局算法 + 边分类 + Compose 渲染器全套)的双 ABI Kotlin/Native 编译——上游commonMain39 个文件零改动,仅新增 8 行 actual 与约 15 行公开入口; - Compose ohos 制品线(
1.12.0-0.1.0-09)在图可视化库上的完整落地,含*-internal/runtime-annotation三个缺口的处置方案; - 2 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号); - DevEco Stage ArkUI 单页验收页面(四预设、双算法、横纵方向、节点拖拽、BACK/SELF_LOOP 样式、双耗时统计、操作日志、版本握手);
- 155 项 JVM 交叉验证全绿(上游 140 + 协议 15)+ HAP 构建(双 ABI 六件套 so,约 53 MB)、签名、设备安装和真机验证;
- 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)