开源鸿蒙平台 CMP 三方库「Capsule」适配全流程
本文记录
capsule接入 OpenHarmony 的完整过程,覆盖平滑圆角形状库盘点、Compose 鸿蒙制品线选型、真实几何引擎编译、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
capsule是基于 Compose Multiplatform 的平滑圆角形状库:ContinuousRoundedRectangle/ContinuousCapsule用 G2 曲率连续的"贝塞尔 + 圆弧"混合转角替换RoundedCornerShape的"直线接圆弧"硬转角,G2ContinuityProfile四参数调节转角曲线族,concentricInset做同曲率中心内缩,全部形状实现Interpolatable可插值。本次适配与社区早期路线(qrose 的"composeMain 隔离、内核提取")有本质区别:社区 Maven 上的 HarmonyOS Compose 制品线(1.12.0-0.1.0-07,runtime / foundation / ui 全部带ohosArm64/ohosX64klib 变体)已经就位,因此不需要隔离任何源码——上游commonMain全部 16 个文件(形状、连续性层级、Double 精度 PathSegment 几何模型、toPath/toSvg)一个字没改,原样编译成 OpenHarmony KLIB,一个曲率连续的完整几何引擎运行在真机上,ArkUI Canvas 页面作为验收宿主。
项目地址: AtomGit/oh-tpc/capsule.git
上游仓库: Kyant0/Capsule
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
capsule 为 Compose 应用提供"圆角"这一件事的完整答案:ContinuousRoundedRectangle(16.dp) 一行替换 RoundedCornerShape,转角从 G1(切线连续、曲率跳变)升到 G2(曲率连续);ContinuousCapsule 是平滑胶囊;G2ContinuityProfile(extendedFraction, arcFraction, bezierCurvatureScale, arcCurvatureScale) 把转角曲线族暴露成四个可调参数,预置 RoundedRectangle / Capsule / G1Equivalent 三档;concentricInset(padding) 让内缩后的内容保持同曲率中心(角半径随内缩减小、钳到 0);所有形状实现 Interpolatable,lerp 手工插值或交给 Compose Styles API 动画。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套形状能力,但上游构建(Kotlin 2.4.20 / Gradle 9.8.0 / AGP 9.4.1 / Compose 1.12.1)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS 画几个圆角矩形,页面可以显示图形,却无法证明公共几何引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证形状引擎、Compose 类型边界、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.4.20 / Gradle 9.8.0 / AGP 9.4.1(com.android.kotlin.multiplatform.library)与鸿蒙工具链无法共存于一个 Gradle 构建。 |
| 依赖矩阵 | 库依赖 compose.runtime / compose.foundation / compose.ui 三件套,上游版本 1.12.1,鸿蒙侧必须有带 ohos klib 变体的替代线。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,更不能持有 Compose Shape / Outline,需要 C ABI、C++ N-API 和 JSON。 |
| 渲染语义跨不了桥 | CornerBasedShape / CornerSize / Dp / Density 是 Compose 概念,OHOS 上没有组合宿主(无 ComposeUIViewController 等价物),Shape 编译得过但渲染不了。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:完整形状库的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。几何计算完全由上游原版实现负责,ArkTS 只负责调用 Native、解析 JSON 和把 PathSegment 模型画到 Canvas 上。
1.2 库提供的能力
Demo 提供一个真实的几何会话,对应桥接的全部入口:
- 形状(shape):
Continuity.createRoundedRectanglePathSegments(w, h, tl, tr, br, bl)按连续性(G0 直角 / G1 圆弧 / G2 平滑)产出 Double 精度路径段序列,支持统一半径、百分比半径(50% 即胶囊)与四角独立覆盖; - 胶囊(capsule):
AdvancedContinuity.createCapsulePathSegments(w, h)走真实的横 / 竖 / 圆三分支捷径;G0 / G1 回落为全半径圆角矩形,对应上游Continuity.createCapsuleOutline的默认实现; - 插值(lerp):
Continuity.lerp(from, to, fraction)真实执行——G1→G2 的中间态是G2ContinuityProfile逐参数插值出的G2Continuity; - 同心(concentric):按上游
ConcentricCornerSize.toPx语义(角半径随内缩减小、钳 0,内容盒四边内缩)逐圈重应用; - 版本握手:
capsuleVersion()返回 fork 版本号,来自真实 Native 调用而非静态文字。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
op / kind | 回显本次操作与形状类别(rounded-rect / capsule)。 |
radii / profile | 生效的四角半径;g2 时回显 profile 四参数。 |
segments | 路径段数组(move / line / arc / cubic / circle 五种编码,Double 坐标)。 |
svg | 桥内自建的 SVG path 字符串(诊断与文档用)。 |
count | 路径段数量。 |
引擎侧完整能力(四类 CornerBasedShape 子类、Interpolatable 插值、LayoutDirection 镜像、concentric / lerp 装饰器)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 21 项 jvmTest 交叉验证用例(上游 LerpTest 12 项 + 新增纯几何 CapsuleGeometryTest 9 项),证明 ohos .so 里执行的几何逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | :capsule 全部 16 个 commonMain 文件原样编译,不提取、不重写、不隔离;库侧零新增源文件(全仓源码级变动仅两处:LerpTest 从 commonTest 移到 jvmTest,新增纯几何测试 CapsuleGeometryTest)。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 android / macos / ios / js / wasmJs 目标与 dokka / vanniktech 发布段随替换剥离;5 个上游构建脚本封存为 *.upstream。 |
| 桥接稳定 | 使用 2 个 C ABI 和 UTF-8 扁平 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | jvm 交叉验证线用上游声明原版本 Compose 1.12.1(Maven Central),ohos 线用社区 1.12.0-0.1.0-07——按目标分线、同代换线(换线不换版)。 |
| UI 完整 | 四页签覆盖全部桥接入口:形状 playground、G0/G1/G2 对比、连续性插值、同心多圈,外加版本握手行。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。OHOS 上没有组合宿主,
@Composable编译得过但渲染不了;capsule 恰好零@Composable、零 expect/actual,Compose 只出现在Shape/Outline类型边界上——几何引擎过桥,渲染职责交给 ArkUI Canvas,与上游"Compose 拿 Outline 画 Canvas"的分工一一对应。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认 Compose ohos 制品线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版、双 Compose 制品线分线
第 3 阶段:桥协议建模 ── PathSegment 载荷、四 ops JSON 契约、segments 编码、错误通道
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 版本握手、四页签、Canvas 段绘制和错误边界
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
capsule/ 上游形状库(进 ohos 构建,jvm + ohosArm64 + ohosX64,16 文件零改动)
capsule/src/commonTest/ 新增:CapsuleGeometryTest(9 项纯几何交叉验证)
capsule/src/jvmTest/ 上游 LerpTest 移入(Compose 线交叉验证,12 项)
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 四页签页面
example/BRIDGE_CONTRACT.md 桥接契约(Kotlin 桥与 ArkTS 页共同遵守的协议)
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
*.upstream 上游 5 个构建脚本封存(settings / root build / gradle.properties /
libs.versions.toml / capsule build),不参与构建
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :capsule 与 :example:nativeApp,rootProject.name = "ohos_capsule"。上游的 dokka / vanniktech 发布插件、GitHub Packages 发布段、Android / apple / js / wasmJs 目标全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向 Eclipse Adoptium jdk-21.0.12.101-hotspot) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 9.6.1(上游 9.8.0 降级;2.4.20 线已验证组合,见 2.2) |
| Compose(ohos 线) | org.jetbrains.compose.*:1.12.0-0.1.0-07(runtime / foundation / ui,ohosArm64 / ohosX64 klib) |
| Compose(jvm 线) | 1.12.1(Maven Central,上游 libs.versions.toml.upstream 声明的原版本) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.capsule |
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
| 分层 | 文件 | 依赖形态 | 处置 |
|---|---|---|---|
| 纯数学 | MathUtils.kt | 零依赖 | 整体编译 |
| 几何代数 | core/Point.kt、core/CubicBezier.kt | 仅 @Immutable/@Stable 注解 | 整体编译 |
| 路径模型 | path/PathSegment.kt、PathSegments.kt、PathSegmentsBuilder.kt | 自研 Double 精度段模型;toPath() 触碰 Compose Path | 整体编译 |
| 连续性策略 | continuities/G0/G1/G2/G2ContinuityProfile | G2 数学全在 Double 域;G1 有一条 Outline.Rounded 捷径 | 整体编译 |
| 形状口径 | Continuity.kt、AdvancedContinuity.kt | 接口含 Size/Outline 方法 | 整体编译 |
| Compose 集成 | Continuous/Absolute/Concentric/LerpContinuousRoundedRectangle.kt | CornerBasedShape / CornerSize / Dp / Density 深度绑定 | 整体编译(ohos 线提供全部类型) |
expect / actual 声明全库 0 处;@Composable 函数全库 0 个。几何计算全部在 Double 域完成,Compose 类型只出现在接口末端的 Outline / Shape 层——这就是"整库零手术"成立的根本原因:挡住 qrose 的是"ohos 上没有 Compose 制品",本次适配首先验证这个障碍已经被社区 Compose ohos 线(1.12.0-0.1.0-07)消除。qrose 需要 composeMain 目录隔离把 Painter 挪出不编译;capsule 什么都不用挪。
全仓源码级变动只有两处:其一是上游 LerpTest.kt 从 commonTest 移到 src/jvmTest——它测的是 Shape 层插值(Density / dp / RoundedCornerShape),属于 jvm 交叉验证靶的职责;其二是新增 commonTest/CapsuleGeometryTest.kt(9 项纯几何用例),见 6.2。
1.4 页面边界
标题区 CAPSULE · OPENHARMONY / 版本握手行(KMP 平滑圆角形状库 · Kotlin/Native ohosArm64/ohosX64 · v1.2.0-ohos.1)
Tab 切换 形状 / 对比 / 插值 / 同心
形状页 宽 / 高 / 圆角百分比 slider、G0/G1/G2 单选、profile 预置芯片(Custom 展开四参数 slider)、
填充 / 描边开关、Canvas 重绘与信息行(count / radii / profile)
对比页 共享圆角百分比 slider + G0 直角 / G1 圆弧 / G2 平滑三联画(200×140 定尺寸)
插值页 G1 → G2 fraction slider(0–1 步进 0.01),resolved 连续性与 fraction 信息行
同心页 rings(1–6)/ inset(2–32)/ radiusPercent(10–50)三 slider,单 Canvas 多圈色阶绘制
关于区 版本握手、库简介、仓库与社区链接
页面启动时先调 capsuleVersion() 做握手;四个页签的每个参数变化都实时发起 capsuleDispatch 调用重算几何。
第 2 阶段:目标与依赖打通
2.1 制品线验证(本适配的成败手)
先查社区 Maven(https://maven.eazytec-cloud.com)上有没有 ohos 的 Compose 制品。用 .module 元数据验证:
org.jetbrains.compose.runtime:runtime:1.12.0-0.1.0-07
variants: ohosArm64Api-published / ohosX64Api-published
org.jetbrains.compose.foundation:foundation:1.12.0-0.1.0-07
variants: 同上
org.jetbrains.compose.ui:ui:1.12.0-0.1.0-07
variants: 同上
三个关键结论:其一,社区线版本主体 1.12.0 与上游声明的 1.12.1 同代相邻(上游是它的 patch 跟进版),API 面一致,源码级兼容成本为零;其二,该线不发布 jvm 消费所需的 desktop / jvm 子制品,所以 jvm 交叉验证目标不能从这条线解析——必须分线:ohosMain 走社区线、jvmMain 走 Maven Central 的上游原版本 1.12.1,这就是 krogress / bubbles-compose / eungabi / FlowTab-CMP 已经用过的"按目标分线"写法;其三,androidx.annotation.FloatRange 等 Android 生态注解随 ui klib 传递可用,上游源码的 import 一行不用改。
2.2 工具链钉版(为什么是 2.4.20-0.1.0-07 + Gradle 9.6.1)
上游 capsule 本来就是 Kotlin 2.4.20 构建,鸿蒙化 Kotlin 钉同代的 2.4.20-0.1.0-07,源码级兼容成本为零,也不需要像 qrose(2.2.21 旧线)那样把 Gradle 降到 8.14.1。Gradle 方面上游 wrapper 是 9.8.0,本机实测该发行版下载超时不可达,改钉 9.6.1——这是与 2.4.20 鸿蒙线共同验证过的组合(KStore / krogress 同款),gradle/wrapper/gradle-wrapper.properties 一行改动即完成。
另一个必须写进 gradle.properties 的网络事实:eazytec Nexus 是懒加载代理,首次请求一个它没缓存过的制品时,回源时间可能超过 Gradle 默认的 30 秒 socket 超时;超时还会被 Gradle 缓存成"制品缺失",后续重试秒失败。处置是双管齐下:
org.gradle.internal.http.socketTimeout=180000
org.gradle.internal.http.connectionTimeout=180000
加超时余量;万一已经缓存了缺失结论,用 --refresh-dependencies 强制重查一次即可恢复。
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_capsule"
include(":capsule")
include(":example:nativeApp")
2.3 编译真实形状库
capsule/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
explicitApi()
applyDefaultHierarchyTemplate()
jvm {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
ohosArm64() // the adaptation
ohosX64()
sourceSets {
commonTest {
dependencies {
implementation(kotlin("test"))
}
}
ohosMain.dependencies {
api(libs.compose.runtime) // 1.12.0-0.1.0-07
api(libs.compose.foundation)
api(libs.compose.ui)
}
jvmMain.dependencies {
api(libs.compose.runtime.jvm) // 1.12.1(上游原版本,Maven Central)
api(libs.compose.foundation.jvm)
api(libs.compose.ui.jvm)
}
}
}
两个 capsule 特有的差异点:其一,Compose 依赖必须按目标线声明(ohosMain / jvmMain 各自 api),不能写在 commonMain——同坐标两版本跨源集会冲突,这正是 2.1 结论二的落地;其二,不需要 compose compiler 插件——capsule 全库零 @Composable,不存在需要编译器变换的内容(krogress 的指示器库是 @Composable 重度用户,两个模块都要挂插件;capsule 的库模块和桥模块都可以省)。上游的 explicitApi() 保留,公开 API 本来就是显式标记的。实测:
.\gradlew.bat :capsule:compileKotlinJvm # BUILD SUCCESSFUL
.\gradlew.bat :capsule:compileKotlinOhosArm64 :capsule:compileKotlinOhosX64 # BUILD SUCCESSFUL
上游 16 个 commonMain 文件一个没动,在两条制品线上各编译了一遍。
2.4 测试依赖的处置:LerpTest 触碰 Compose Shape 层
上游 LerpTest 断言的是 ContinuousRoundedRectangle.lerp 折算像素后的值(Density(1f) / CornerSize.toPx / RoundedCornerShape),这些类型在 ohos 线上同样存在,但它是行为验证而非几何验证,归属 jvm 交叉验证靶更干净——移到 src/jvmTest(文件内容零改动)。commonTest 新增纯几何的 CapsuleGeometryTest(只依赖 kotlin.test 与库自身的 PathSegment 模型,jvm / ohos 都能编)。ohos 测试任务照旧全局禁用:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
第 3 阶段:桥协议建模
3.1 JSON 契约
com.kyant.capsule/**(上游形状库 + PathSegment 模型,原样编译)
↓ Continuity.createRoundedRectanglePathSegments / createCapsulePathSegments / lerp
Bridge.kt(@CName × 2,扁平 JSON 进出)
↓ UTF-8 JSON
N-API capsuleVersion() / capsuleDispatch()
↓
ArkUI Index.ets(四页签 + Canvas)
请求与响应:
请求 : {"op":"shape","width":240,"height":160,"radiusPercent":25,"continuity":"g2","profile":"rounded"}
{"op":"capsule","width":280,"height":96,"continuity":"g2"}
{"op":"lerp","lerpFrom":"g1","lerpTo":"g2","lerpFraction":0.5,...}
{"op":"concentric","width":280,"height":200,"rings":4,"inset":14,...}
响应 : {"ok":true,"op":"shape","kind":"rounded-rect","width":...,"height":...,
"continuity":"g2","radii":{...},"profile":{...},"count":16,
"segments":[[0,0,72],[3,...],[2,...],...],"svg":"M0 72 C... Z"}
{"ok":false,"op":"...","error":"..."}
3.2 载荷类型为什么是 segments 数组
上游的渲染边界是 PathSegments.toPath() → Outline.Generic(path) → Compose Canvas;ohos 侧没有这个宿主,桥就把"最后一公里"的原料直接送出去——PathSegment 的四种子类型(Line / Arc / Circle / Cubic)编码成紧凑数组:
| 编码 | 含义 | ArkTS Canvas 对应 |
|---|---|---|
[0, x, y] | moveTo | ctx.moveTo(x, y) |
[1, x, y] | lineTo | ctx.lineTo(x, y) |
[2, cx, cy, r, a0, sweep] | 圆弧(弧度制) | ctx.arc(cx, cy, r, a0, a0+sweep, sweep<0) |
[3, x1,y1, x2,y2, x3,y3] | 三次贝塞尔 | ctx.bezierCurveTo(...) |
[4, cx, cy, r] | 整圆 | ctx.arc(cx, cy, r, 0, PI*2) |
三条设计纪律:其一,弧度制与 PathSegment.Arc 的存储形式一致,ArkTS arc() 恰好也吃弧度,零换算;其二,坐标保留 Double 精度(序列化时裁到 4 位小数防科学计数法泄漏),缩放由 ArkTS 侧 ctx.scale 统一处理;其三,SVG 字符串在桥内自建而不是调用上游 toSvg()——那个实现借道 toPath().getBounds()(Compose Path),桥的运行路径刻意不触碰 Compose 图形栈,几何序列化与 Compose 解耦。
同心 op 的每圈 segments 已经加上 (left, top) 偏移,ArkTS 单画布整体绘制即可;每圈半径按上游 ConcentricCornerSize.toPx 语义(radius - inset 钳 0)在 Kotlin 侧算好回传。
3.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("CapsuleDispatch")
fun capsuleDispatch(request: String): String {
var op = "shape"
return try {
val json = MiniJson.parse(request)
op = json.str("op").ifBlank { "shape" }
when (op) { "shape" -> …; "capsule" -> …; "lerp" -> …; "concentric" -> …
else -> err(op, "unsupported op …") }
} catch (t: Throwable) {
err(op, t.message ?: t.toString())
}
}
非法 JSON、缺字段、不支持的 op、半径越界(全部参数 coerceIn 限幅 + 默认值兜底),全部走同一个 ok:false 通道,ArkTS 侧统一渲染到信息行。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libcapsule_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohoscapsule.so)
│ CapsuleDispatch / CapsuleVersion
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制几何语义,所以选择 C ABI + JSON。
4.2 Kotlin/Native 导出函数
@CName("CapsuleVersion")
fun capsuleVersion(): String = "1.2.0-ohos.1" // 版本握手
@CName("CapsuleDispatch")
fun capsuleDispatch(request: String): String // shape / capsule / lerp / concentric 一口进
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
CapsuleDispatch | capsuleDispatch(req) | createRoundedRectanglePathSegments + 胶囊捷径 + Continuity.lerp + concentric 语义 |
CapsuleVersion | capsuleVersion() | fork 版本握手(真机版本行的数据来源) |
llvm-nm -D 验证(双 ABI 均为全局 T 符号):
arm64 : 000000000109d80c T CapsuleDispatch 000000000109d990 T CapsuleVersion
x64 : 000000000106e2e0 T CapsuleDispatch 000000000106e460 T CapsuleVersion
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"capsuleDispatch", nullptr, NapiCapsuleDispatch, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"capsuleVersion", nullptr, NapiCapsuleVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohoscapsule_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohoscapsule_api.h 直接采用 KN 构建生成的原版头(5,106 字节,linkDebugShared 产物拷入 cpp/include/),napi_init.cpp 链接期绑定两个业务符号。
4.4 CMake 和类型声明
add_library(ohoscapsule SHARED IMPORTED)
set_target_properties(ohoscapsule PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(capsule_napi SHARED napi_init.cpp)
target_link_libraries(capsule_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohoscapsule)
OHOS_ARCH 为空或 libohoscapsule.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,加载时互相可见。
export const capsuleDispatch: (request: string) => string;
export const capsuleVersion: () => string;
第 5 阶段:ArkUI 宿主
5.1 版本握手和触发重算
aboutToAppear(): void {
this.version = capsuleVersion(); // 版本握手:来自真实 Native 调用
}
private refreshShape(): void {
const req: ShapeRequest = {
op: 'shape', width: this.width, height: this.height,
radiusPercent: this.radiusPercent, continuity: this.continuity,
profile: this.profile, …
};
const res = JSON.parse(capsuleDispatch(JSON.stringify(req))) as SegmentsResp;
this.apply(res); // @State 刷新 Canvas + 信息行
}
每个 slider / 单选 / 开关的 onChange 都走 refreshXxx(),一次交互就是一次真机 Native 几何重算;Canvas 的 onReady 触发各页签首次绘制。
5.2 页面交互
四页签自上而下:形状页是完整 playground(尺寸 / 圆角 / 连续性 / profile 四参数 / 填充描边开关,信息行回显 count、radii 与 profile 数值);对比页共享一个圆角百分比,G0 直角 / G1 圆弧 / G2 平滑三联画并排(200×140 定尺寸,肉眼可直接看出 G2 转角没有曲率突变的光泽感);插值页拖动 fraction 从 G1 渐变到 G2,信息行显示 resolved 连续性;同心页三 slider 控制圈数 / 内缩 / 半径,多圈以 #7EC8FF → #2C5F8A 蓝阶着色叠加在单画布上。
5.3 绘制映射和错误边界
统一绘制辅助按响应的 width / height 等比缩放居中(四周留 10% 边距),描边宽度按缩放倒数补偿为恒定视觉宽度;五类段编码逐一映射到 moveTo / lineTo / arc / bezierCurveTo,收尾 closePath + fill + stroke。ok:false 时错误原文进信息行红字;Native 调用本身抛错(理论不该发生)同样被 try/catch 收进信息行。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── BRIDGE_CONTRACT.md 桥接契约(两侧共同遵守的字段 / 编码 / 错误通道)
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohoscapsule)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ └── Bridge.kt @CName × 2 + MiniJson + segments/SVG 序列化
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.capsule、versionName 1.2.0-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/libohoscapsule_api.h KN 生成的原版导出头
├── src/main/cpp/types/libcapsule_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 四页签验收页面
6.2 构建、签名和安装
.\gradlew.bat :capsule:jvmTest # 21 项交叉验证
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填带 material 的完整配置)。之后直接 Run 或命令行:
cd example\harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
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.capsule -m entry
四、完整代码对照
4.1 整体架构
com.kyant.capsule/**(上游形状库原样编译)
│ Continuity 层级(G0/G1/G2)+ Double 精度 PathSegment 模型
Bridge.kt + MiniJson
│ CapsuleDispatch / CapsuleVersion
libcapsule_napi.so
│ capsuleDispatch / capsuleVersion
Index.ets
│ JSON parse + segments 数组 → Canvas 路径
▼
ArkUI 真机页面(四页签)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_capsule |
gradle/libs.versions.toml | Kotlin 2.4.20-0.1.0-07 + 双 Compose 线钉版(ohos 1.12.0-0.1.0-07 / jvm 1.12.1) |
build.gradle.kts(根) | 版本 1.2.0-ohos.1、全局禁用 Ohos*Test* 任务 |
capsule/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,Compose 按目标线声明、零 compose compiler 插件 |
capsule/src/commonTest/.../CapsuleGeometryTest.kt | 新增纯几何交叉验证(9 项) |
capsule/src/jvmTest/.../LerpTest.kt | 上游测试移入 jvm 线(内容零改动,12 项) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 2 个 C ABI、四 ops、segments/SVG 序列化、MiniJson、错误通道 |
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 四页签验收页面(Canvas 段绘制) |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(15 章) |
example/BRIDGE_CONTRACT.md | 桥接契约(两侧的字段级协议) |
*.upstream(5 个) | 上游构建脚本封存,不参与构建 |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | Continuity.createRoundedRectanglePathSegments(w,h,tl,tr,br,bl) | 按连续性产出 Double 精度路径段(shape op 内核) |
| 引擎 | AdvancedContinuity.createCapsulePathSegments(w,h) | 胶囊横 / 竖 / 圆三分支捷径(capsule op 内核) |
| 引擎 | Continuity.lerp(stop, fraction) | 连续性插值,G1↔G2 中间态为 profile 插值(lerp op 内核) |
| 引擎 | ConcentricCornerSize.toPx(语义) | 同心内缩:半径减 padding 钳 0(concentric op 语义来源) |
| 引擎 | G2ContinuityProfile(四参数 + 三预置) | 转角曲线族调节(custom profile 透传) |
| 桥 | CapsuleDispatch / CapsuleVersion | C ABI JSON 导出(四 ops 一口进) |
| N-API | capsuleDispatch(req) / capsuleVersion() | 向 ArkTS 暴露能力 |
| ArkTS | drawSegments(ctx, segments, …) | 段数组 → Canvas 路径(等比缩放居中 + 描边补偿) |
4.4 ArkTS 与 Kotlin 的边界
const res = JSON.parse(capsuleDispatch(JSON.stringify({
op: 'lerp', lerpFrom: 'g1', lerpTo: 'g2', lerpFraction: 0.5,
width: 240, height: 160, radiusPercent: 30
})));
// res.ok === true;res.lerp.resolved === 'g2';res.segments 即插值连续性的路径段
val from = G1Continuity
val to = G2Continuity()
val resolved = from.lerp(to, 0.5) // G2Continuity(profile = lerp(G1Equivalent, RoundedRectangle, 0.5))
val segments = resolved.createRoundedRectanglePathSegments(240.0, 160.0, r, r, r, r)
两者之间只传输扁平 JSON 字符串与段数组,不传输 Kotlin 对象;每一笔圆角都是 G2Continuity 的真实数学在设备上算出来的。
五、关键决策说明
决策 1:真编译整库而不是 qrose 式内核隔离
qrose 抽内核隔离 Painter,是因为当时 ohos 上没有 Compose 制品,只能把纯 Kotlin 编码器单独编译。本次适配先验证制品线(runtime / foundation / ui 全部存在 ohos klib 变体),障碍已消失——再隔离等于把 ContinuousRoundedRectangle 一族从库里切出去,反而制造两套几何。真编译让 G2 求解、胶囊捷径、插值、同心语义全部是上游原件,且库侧一行新增源码都没有(KStore 尚需 8 行 dispatcher actual,capsule 连这个都不需要——它零 expect/actual)。
决策 2:双 Compose 制品线,换线不换版
社区 ohos 线 1.12.0-0.1.0-07 不发布 jvm 子制品,jvm 交叉验证只能走 Maven Central;上游声明的 1.12.1 原样保留给 jvm,ohos 用同代社区线。两线 API 面一致(1.12.0 与 1.12.1 相邻 patch),同一份 commonMain 在两条线上各编译一遍,本身就是"API 没漂"的证明。依赖声明必须放在 ohosMain / jvmMain 源集而不是 commonMain——同坐标两版本跨源集冲突。
决策 3:根构建替换(cmp_qrose 模式)+ *.upstream 封存
上游根构建(dokka / vanniktech 发布 / GitHub Packages / AGP classpath / 六平台目标)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。与 KStore 一致,但本仓把被替换的 5 个上游构建脚本显式封存为 *.upstream(settings / root build / gradle.properties / libs.versions.toml / capsule build)留在树里——审阅者能逐行对照"改了什么、没改什么",上游升级时也有三方合并的基线。
决策 4:几何 JSON 过桥,而不是位图光栅
qrose 的 Painter 产出的是成品图像,所以桥走"Kotlin 软光栅化 + PNG + ImageKit";capsule 的产出是矢量几何,位图化反而有损(分辨率钉死、放大发虚)。桥直接送 PathSegment 段数组,ArkTS Canvas 矢量重绘——与上游分工严格对应:上游是"Compose 拿 Outline 画",鸿蒙是"ArkTS 拿段数组画",库的数学始终是唯一几何源。SVG 字符串在桥内自建(不经 toSvg() 的 Compose Path 依赖),仅作诊断输出。
决策 5:jvmTest 交叉验证 + LerpTest 搬家不改动
LerpTest 是上游的行为测试(Shape 层插值折算像素),移到 jvmTest 后内容零改动地跑在上游原版 Compose 1.12.1 上;commonTest 换成纯几何的 CapsuleGeometryTest(段数 / 链式连续 / 有界性 / 胶囊路由 / 插值 profile / 确定性),jvm 与 ohos 共同编译、jvm 执行。测试文件的"只搬家不改写"保住了交叉验证的说服力。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x86_64)、JDK 21(Eclipse Adoptium)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
.\gradlew.bat :capsule:jvmTest
21 项用例全部通过:
| 测试类 | 用例数 | 钉住什么 |
|---|---|---|
LerpTest(上游原件,移入 jvmTest) | 12 | Shape 层插值:Dp / 百分比圆角插值、continuity 插值、RectangleShape / 未知 Shape 边界、concentric 后插值、Interpolatable 双向 |
CapsuleGeometryTest(新增,纯几何) | 9 | G0 全直线段、G1 四段四分之一圆弧、G2 默认 16 段且链式连续 + 有界、四角独立半径、G1Equivalent 扫满弧、胶囊三分支路由与整圆、全半径胶囊捷径、Continuity.lerp 混合 profile、确定性 |
LerpTest 在上游 Compose 1.12.1 上照跑全绿、CapsuleGeometryTest 与 ohos KLIB 同源编译——"换线不换版"在形状库上的直接证据。
6.3 HAP 验证
:capsule:compileKotlinJvm / compileKotlinOhosArm64 / compileKotlinOhosX64: BUILD SUCCESSFUL
:capsule:jvmTest: 21 项全绿
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: CapsuleDispatch / CapsuleVersion / libohoscapsule_symbols 均为全局 T 符号(双 ABI)
BuildNativeWithCmake: finished(双 ABI 编出 libcapsule_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
产物尺寸(debug 未 strip,含 Kotlin/Native 运行时与两条 Compose 线链接进来的 klib):libohoscapsule.so arm64 28,728,368 字节、x86_64 27,508,032 字节;libc++_shared.so 分别 1,850,184 / 1,731,432 字节;另产出 capsule-jvm-1.2.0-ohos.1.jar。
6.4 功能用例
- 启动后标题区出现
v1.2.0-ohos.1(来自capsuleVersion()真实 Native 调用,非静态文字); - 形状页拖动圆角百分比,Canvas 实时重绘,信息行 count 随 G2 的 16 段结构稳定回显;
- 切到 G1,段数变为 8(4 线 + 4 弧),转角变为标准圆弧;切到 G0 只剩直线段;
- Custom profile 展开四参数 slider,
extFrac拉大可见转角过渡沿边缘延伸,信息行回显生效 profile 数值; - 对比页三联画同参数并排:G0 直角、G1 圆弧、G2 平滑肉眼可辨,G2 转角无曲率突变的高光过渡连续;
- 插值页 fraction 从 0 拖到 1,形状从 G1 圆弧连续渐变为 G2 平滑,
resolved恒为 g2(中间态是 profile 插值); - 同心页增减圈数 / 内缩,多圈蓝阶叠加,内圈半径按
radius - inset递减钳 0,圆角耗尽时退化为直角内圈; - 填充 / 描边开关即时生效,描边宽度在任意缩放下视觉恒定。
6.5 验证结论
形状引擎、Compose 类型边界、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的几何会话:版本握手、三种连续性的形状生成、胶囊捷径、连续性插值与同心多圈(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/capsule.git
cd capsule
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
.\gradlew.bat :capsule:compileKotlinOhosArm64 :capsule:compileKotlinOhosX64 # klib
.\gradlew.bat :capsule:jvmTest # 21 项交叉验证
.\gradlew.bat :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.capsule -m entry
hdc shell hilog | grep Capsule
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:capsule真正编译成ohosArm64/ohosX64KLIB 并链出动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- eazytec Nexus 是懒加载代理:首次请求未缓存制品会超时,且超时会被 Gradle 缓存成"缺失"导致后续秒败——
socketTimeout/connectionTimeout提到 180 秒,已缓存缺失时用--refresh-dependencies强制重查; - Gradle wrapper 发行版下载本身也可能超时不可达:改钉本机已有且与鸿蒙 Kotlin 2.4.20 线共同验证过的 9.6.1(KStore / krogress 同款),一行
distributionUrl完成; - Compose 依赖不能写在
commonMain:ohos 线与 jvm 线同坐标不同版本,跨源集直接冲突——按ohosMain/jvmMain分线声明(krogress 先例); - capsule 零
@Composable,库与桥都不需要 compose compiler 插件;有@Composable的库(如 krogress)必须挂——两类库的插件需求不能互相套用; - ArkUI 没有
Switch组件,开关是Toggle({ type: ToggleType.Switch })——组件名按 ArkUI 手册写,Android / Flutter 的惯性命名过不了CompileArkTS; build-profile.json5的signingConfigs条目缺material字段会被 hvigor schema 拒绝——仓库内留空数组且 product 不引用签名,勾选自动签名后由 IDE 回填完整配置;- HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可; - DevEco 打开的是
example/harmonyApp子目录而不是仓库根目录——根目录是 Gradle 工程,DevEco 会当错误工程类型处理; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - 签名材料由 DevEco 自动写入
build-profile.json5,提交前用git update-index --skip-worktree保护,证书绝不进入 AtomGit。
8.2 已知问题
- OHOS 无组合宿主:
@Composable与 Shape 渲染链路编译得过但跑不起来,Demo 由 ArkTS Canvas 重绘段模型承担渲染(这也是本批 CMP 库的共同边界); - 桥只送几何不送渲染:描边样式、渐变填充等视觉参数留在 ArkTS 侧,上游对应能力(
Modifier.background(shape)全家桶)不在桥的范围内; - MiniJson 只支持扁平 JSON(qrose / krogress 同款纪律),嵌套请求载荷按设计拒绝;
- 验证产物是 debug so(约 27 MB,未 strip),release 链接与裁剪未做;
- android / js / wasmJs / apple 目标剥离(源文件留在树里),发布、dokka 一并剥离,本 fork 不发 Maven Central;
- HAP 安装必须使用与 bundle name
org.terminator.ohos.capsule匹配的签名。
8.3 未来优化方向
- release 模式链接与符号裁剪,把
.so从 27 MB 级压到可用体积; - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 把
toSvg()的 Compose Path 依赖解掉后直接复用上游实现(当前桥内是自建序列化); - 在 CI 中加入 ohos 编译、
llvm-nm符号检查和 HAP 构建; - 补充模拟器(x86_64)验收矩阵、四页签自动截图脚本与 G0/G1/G2 曲率数值断言。
九、总结
9.1 核心难点回顾
上游 capsule(全部 commonMain,16 文件)
→ 验证 Compose ohos 制品线(runtime / foundation / ui @ 1.12.0-0.1.0-07)
→ 工具链对齐 Kotlin 2.4.20-0.1.0-07(Gradle 9.6.1)
→ 根构建替换(cmp_qrose 模式,5 个上游脚本封存 *.upstream)
→ ohosArm64 / ohosX64 KLIB,库侧零新增源码
→ C ABI(@CName × 2,四 ops)+ PathSegment 段数组载荷
→ C++ N-API(DisposeString 纪律)
→ ArkTS 段数组 → Canvas 矢量重绘
→ ArkUI 真机四页签验收(形状 / 对比 / 插值 / 同心)
9.2 封装层次
- 引擎层:上游原版
Continuity层级(G0/G1/G2)、G2ContinuityProfile曲线族、Double 精度PathSegment模型; - 形状层:上游原版
ContinuousRoundedRectangle/Absolute…/ContinuousCapsule、concentric / lerp 装饰器(随编译进.so,Compose 宿主就绪即可用); - Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、请求组包、段数组到 Canvas 的矢量重绘与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 先验证生态制品线再定适配路线:Compose ohos 线补齐后,形状类库的正确答案就是"整库真编译 + 几何 JSON 过桥"——capsule 零 expect/actual、零
@Composable,适配面比 KStore(8 行 actual)还小;路线选择必须基于当天的制品可达性,而不是 qrose 时代的历史结论; - 渲染职责跟着宿主走而不是跟着库走:上游的渲染边界在 Compose Canvas(
Outline),鸿蒙的宿主是 ArkUI Canvas,桥就把"边界前的原料"(PathSegment)送过去——矢量过桥优于位图过桥,库的数学始终是唯一几何源; - 交叉验证要"同源双编"才有说服力:同一份
commonMain在上游线(Compose 1.12.1)与社区线(1.12.0-0.1.0-07)各编译一遍、上游测试文件只搬家不改写、21 项 jvmTest 全绿——三条合起来才是"换线不换版"的完整证据链。
9.4 适配成果
当前适配已完成:
- capsule
1.2.0(上游由 Kyant 创建、xfqwdsj 维护,Apache-2.0)完整形状库的双 ABI Kotlin/Native 编译(上游commonMain16 文件零改动,库侧零新增源文件); - HarmonyOS Compose 制品线在形状库上的完整落地(runtime / foundation / ui
1.12.0-0.1.0-07,jvm 线保持上游1.12.1); - 2 个 C ABI 符号(
llvm-nm双 ABI 验证全局 T 符号)和 C++ N-API 包装层; - DevEco Stage ArkUI 四页签验收页面(形状 playground、G0/G1/G2 对比、连续性插值、同心多圈、版本握手);
- 21 项 JVM 交叉验证全绿(上游 LerpTest 12 + 新增纯几何 9)+ HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证;
- 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)