开源鸿蒙平台 CMP 三方库「reveal」适配全流程
本文记录
reveal接入 OpenHarmony 的完整过程,覆盖纯 Compose UI 库的整库编译路线选型、双 Compose 制品线打通、ohosArm64/ohosX64目标、无头门面设计、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
reveal是 Compose Multiplatform 的"教学引导 / coach mark / onboarding"效果库:RevealState状态机 +Revealable挖孔几何 +RevealOverlayArrangement排布引擎 + 变暗遮罩DimRevealOverlayEffect,上游覆盖 Android、iOS、Desktop、Web。本次适配与社区早期路线(qrose 的"零依赖内核提取")有本质区别:社区 Compose 鸿蒙制品线已经升到 1.12.0,与上游构建线同代,因此不需要隔离或重写——上游三个库模块的全部源码(包括@Composable代码)原样编译成 OpenHarmony KLIB 与动态库,状态机、挖孔几何、排布算法在真机上执行,ArkUI 页面作为渲染与交互宿主。
项目地址: AtomGit/oh-tpc/reveal.git
上游仓库: svenjacobs/reveal
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
reveal 为 Compose Multiplatform 应用回答"第一次打开 App 怎么教用户"这一件事:Modifier.revealable(key) 把任意元素注册为引导目标,revealState.reveal(keys) 一行揭示,被揭示元素周围按形状(矩形 / 圆角矩形 / 圆形)挖孔,其余区域变暗,解释气泡通过 RevealOverlayScope.align 排布到挖孔区四周,reveal-shapes 模块再提供带箭头的 Balloon 气泡。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面共享。OpenHarmony 应用同样需要引导页能力,但上游构建(Kotlin 2.4.21 / Gradle 9.8.1 / AGP 9.4.1 / Compose 1.12.0)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以显示按钮,却无法证明引导引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证状态机、挖孔几何、排布算法、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.4.21 / Gradle 9.8.1 / AGP 9.4.1(com.android.kotlin.multiplatform.library)与鸿蒙工具链无法共存于一个 Gradle 构建。 |
| 依赖矩阵 | 引擎代码 import androidx.compose.*(runtime / foundation / ui / animation)与 kotlinx-coroutines,每一个都要有 ohos 变体才能解析。 |
| UI 机制跨不了桥 | 全屏 Popup 遮罩、BlendMode.DstOut 挖孔、movableContent 淡入淡出是 Compose 组合内机制,ArkTS 不能直接持有。 |
| 语言边界不同 | ArkTS 不能直接调用 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:三个库模块的 ohos 整库编译、根构建替换(cmp_qrose 模式)、库内无头门面(RevealHeadless)和 C ABI/N-API 桥接层。引导语义完全由上游原版实现负责,ArkTS 只负责调用 Native、解析 JSON 和渲染结果。
1.2 库提供的能力
Demo 提供一个真实的教学引导会话,对应桥接的全部入口:
- 三步引导(引导 Demo 页签):模拟 App 的菜单按钮 / 统计卡片 / 悬浮按钮作为引导目标,
reveal(key)逐个揭示;挖孔跟随目标真实布局位置,气泡由排布引擎放置,箭头锚点指向目标中心; - 挖孔与排布(几何页签):形状(矩形 / 圆 / 圆角)、corner、padding、四向排布(Start / End / Top / Bottom)、交叉对齐与 confine 开关全部可调,实时观察
computeArea的 padding 外扩、圆形外接与arrange的屏内钳位; - 状态机实验(状态机页签):
reveal A→reveal A,B→reveal B(观察 previous 淡出名单)→tryReveal未知 key(返回 false 不抛错)→hide→ 移除最后一个 current(上游自动隐藏语义); - 版本握手(关于页签):
revealVersion()返回 fork 版本号与工具链信息,来自真实 Native 调用而非静态文字。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
visible / current / previous / registered | 状态机快照:遮罩是否可见、当前与上一批揭示的 key、已注册目标。 |
items[].area | 挖孔区域(Float 像素),padding 与圆形外接已由上游 computeArea 应用。 |
items[].layout / items[].overlay | 排布引擎给出的排布区与气泡最终放置矩形(Int 像素)。 |
items[].arrowX / arrowY | 中心相对箭头锚点,与上游 RevealOverlayArrowAnchor 语义一致。 |
引擎侧完整能力(Mutex 串行、key 去重、“保留项不进入 previous”、最后一项移除自动隐藏、RTL 布局方向、reveal-shapes 的 Arrow / Balloon 形状模型)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 21 项 desktopTest 交叉验证用例,证明 ohos .so 里执行的引导逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | reveal-common / reveal-core / reveal-shapes 的全部 commonMain 文件原样编译,不提取、不重写、不停靠;仅新增 1 个无头门面源文件(全仓唯一新增库源码)。 |
| 平台目标 | 根构建直接声明 jvm("desktop") + ohosArm64 + ohosX64,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);android-tests / demo-app / convention-plugins 留盘不进构建。 |
| 桥接稳定 | 使用 2 个 C ABI 和 UTF-8 扁平 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | Compose 双制品线(ohos 线 1.12.0-0.1.0-07 / jvm 线 1.12.0,换线不换版),coroutines 双线(1.11.0-0.1.0-07 / 1.10.2),上游测试零改动全绿。 |
| UI 完整 | 四页签覆盖全部桥接入口:引导、几何、状态机、关于。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。跑在真机上的是引擎(状态机 + 几何 + 排布);
@Composable面已编入 klib 但处于休眠,视觉效果由 ArkTS 按引擎返回的几何复现。
二、实现路线图
第 1 阶段:项目初始化 ── 模块与依赖边界盘点、确认社区 Compose 1.12.0 ohos 制品线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版、双制品线分线声明
第 3 阶段:门面与协议建模 ── RevealHeadless 无头门面、8 个 op 的扁平 JSON 契约、错误通道
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 四页签页面、Canvas 挖孔渲染、目标位置回注册、操作日志和版本握手
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
reveal-common/ 上游共享代码(commonMain 原样,进 ohos 构建)
reveal-core/ 上游核心库(commonMain 原样,进 ohos 构建)
reveal-core/src/skikoMain/ 上游 popup actual——desktop 与 ohos 目标共享
reveal-core/src/ohosMain/ 新增:RevealHeadless 无头门面(全仓唯一新增库源文件)
reveal-core/src/androidMain/ 上游 Android actual(FLAG_NOT_TOUCHABLE),留盘不进构建
reveal-shapes/ 上游气泡形状库(commonMain 原样,进 ohos 构建)
example/nativeApp/ Kotlin/Native 桥(Bridge + MiniJson → libohosreveal.so)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
example/BRIDGE_CONTRACT.md 桥接 JSON 契约(Kotlin 侧与 ArkTS 侧共同遵守)
doc/ 本文章与真机效果图
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md / *.upstream 上游原文与构建文件封存
android-tests/ demo-app/ convention-plugins/ 上游遗留,留盘不进 settings.gradle.kts
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :reveal-common、:reveal-core、:reveal-shapes 与 :example:nativeApp,rootProject.name = "ohos_reveal"。上游的 kotlinter / binary-compatibility-validator / roborazzi / dokka / vanniktech 发布插件、AGP classpath、convention-plugins includeBuild 全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven) |
| JDK | 21(gradle.properties 的 org.gradle.java.home 指向本机 JDK 21) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm("desktop") 保留作交叉验证) |
| Gradle | 9.6.1(自上游 9.8.1 降级;2.4.20 线与 9.6.1 是已验证组合) |
| Compose | ohos 线 1.12.0-0.1.0-07(eazytec);jvm 线 1.12.0(Maven Central,与上游同版) |
| coroutines | ohos 线 1.11.0-0.1.0-07 / jvm 线 1.10.2(RevealState 的 Mutex 需要) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.reveal |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 模块 | commonMain 文件 | 依赖形态 | 处置 |
|---|---|---|---|
reveal-common | 1 | compose.runtime(箭头锚点的 snapshot state + CompositionLocal) | 整体编译(jvm + 双 ohos) |
reveal-core | 17 | compose.runtime / foundation / ui / animation + coroutines(Mutex) | 整体编译(零源码手术) |
reveal-shapes | 3 | compose.runtime / foundation / ui(Arrow / Balloon / BalloonShape) | 整体编译 |
expect/actual 全库只有 1 处:internal expect fun revealOverlayPopupProperties(passthrough: Boolean)。上游给了两个 actual——androidMain(窗口 flag 触摸穿透)和 skikoMain(弱化版 PopupProperties,desktop / iOS / js / wasmJs 共用)。ohos 补的不是新 actual,而是把 ohosMain 挂进 skiko 源码集家族(见 2.3),上游 skiko actual 直接为 ohos 目标编译——社区 Compose ohos 后端本身就是 skiko 系。
引擎里适合无头驱动的三块:RevealState 状态机(public API,reveal() 只做状态更新立即返回)、Revealable.computeArea 挖孔几何(internal)、RevealOverlayArrangement.arrange/align 排布算法(public 纯静态)。这三块就是桥接的全部真实入口。
1.4 页面边界
标题区 reveal · OpenHarmony / 副标题(CMP 教学引导库 · Kotlin/Native ohosArm64/ohosX64)
Tab 切换 引导 Demo / 几何 / 状态机 / 关于
引导 Demo 模拟 App(头部菜单 + 统计卡片 + FAB + 底栏)+ 开始引导按钮
遮罩层 全屏 Canvas:变暗遮罩 + destination-out 挖孔 + 气泡底与箭头三角
气泡层 标题 + 描述 + 下一步/跳过按钮(定位到引擎返回的 overlay 矩形)
几何页签 形状/排布/交叉对齐/ confine 选择器 + left/top/size/corner/padding 滑杆 + 信息行
状态机页签 注册/揭示/tryReveal/hide/移除 按钮组 + current/previous 双列表 + 操作日志(上限 100 条)
关于页签 版本握手、库简介、架构分层、仓库与社区链接
页面启动时先取 display.getDefaultDisplaySync() 的密度与屏幕像素做 configure,再调 revealVersion() 握手;引导目标的 onAreaChange 把真实布局位置(vp × density → px)回注册给引擎——对应上游 Modifier.revealable 里 onGloballyPositioned 的布局上报语义。
第 2 阶段:目标与依赖打通
2.1 制品线验证(本适配的成败手)
先查社区 Maven(https://maven.eazytec-cloud.com)上 Compose 的 ohos 制品。用 .module 元数据逐个验证:
org.jetbrains.compose.runtime:runtime:1.12.0-0.1.0-07
variants: ohosArm64ApiElements-published / ohosX64ApiElements-published / …
org.jetbrains.compose.foundation:foundation:1.12.0-0.1.0-07
org.jetbrains.compose.ui:ui:1.12.0-0.1.0-07
org.jetbrains.compose.animation:animation:1.12.0-0.1.0-07
variants: 同上(ohos 双 ABI 齐全)
三个关键结论:其一,社区 ohos 线的版本主体是 1.12.0,与上游 reveal 构建所用的 JetBrains Compose 1.12.0 完全同代——这就是"换线不换版",上游源码一行不用改;其二,ohos 制品不发布 *-desktop 子制品(jvm 变体),所以 jvm("desktop") 交叉验证目标必须从 Maven Central 的 1.12.0 解析,两条线并存;其三,kotlinx-coroutines-core 必须钉 1.11.0-0.1.0-07 后缀线(compose ohos 运行时同款),Maven Central 的 1.10.2 留给 jvm 线。
2.2 工具链钉版(为什么是 2.4.20-0.1.0-07)
compose 1.12.0-0.1.0-07 制品线构建在 HarmonyOS Kotlin 2.4.20-0.1.0-07 上,工具链版本跟着制品线走。上游源码本来就是 Kotlin 2.4.21 编的,2.4.20 与之同代,源码级兼容成本为零。Gradle 方面:capsule / KStore 先例验证了 2.4.20 线与 Gradle 9.6.1 的组合,上游 wrapper 9.8.1 在该工具链线上未验证过,降到 9.6.1。
另有一个隐性坑:上游提交了 gradle/gradle-daemon-jvm.properties(toolchainVersion=25),Gradle 9.6.1 配置期就会去找 JDK 25 daemon 并通过 foojay 解析下载链接。本 fork 把它移除(封存为 .upstream),daemon 直接用 JDK 21。
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_reveal"
include(":reveal-common")
include(":reveal-core")
include(":reveal-shapes")
include(":example:nativeApp")
2.3 编译真实引擎
reveal-core/build.gradle.kts 收敛为纯 KMP 模块,核心是双制品线分线声明 + skiko 源码集复用:
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotlin.compose.compiler) // 版本走 Kotlin 线 2.4.20-0.1.0-07
}
kotlin {
explicitApi()
applyDefaultHierarchyTemplate()
jvm("desktop")
ohosArm64()
ohosX64()
sourceSets {
// 上游的中间源码集,重接线:popup actual 由 desktop 与 ohos 共享
val skikoMain by creating { dependsOn(commonMain.get()) }
val ohosMain by getting
val desktopMain by getting
desktopMain.dependsOn(skikoMain)
ohosMain.dependsOn(skikoMain) // 社区 Compose ohos 后端是 skiko 系
commonMain.dependencies {
api(project(":reveal-common")) // commonMain 零 compose 声明(line-agnostic)
}
ohosMain.dependencies {
api(libs.compose.runtime) // 1.12.0-0.1.0-07(eazytec)
api(libs.compose.runtime.saveable)
api(libs.compose.foundation)
api(libs.compose.animation)
api(libs.compose.ui)
implementation(libs.kotlinx.coroutines.core)
}
desktopMain.dependencies {
api(libs.compose.runtime.jvm) // 1.12.0(Maven Central)
api(libs.compose.foundation.jvm)
api(libs.compose.animation.jvm)
api(libs.compose.ui.jvm)
implementation(libs.kotlinx.coroutines.core.jvm)
}
}
}
为什么 commonMain 一个 compose 依赖都不声明:目标编译的 classpath 是其源码集闭包(commonMain → skikoMain → ohosMain)全部依赖的并集,所以 commonMain 里的 androidx.compose.* import 在 ohos 目标向社区线解析、在 jvm 目标向 Maven Central 线解析。若在 commonMain 声明,Gradle 元数据会把同一坐标坍缩成一个版本,两头死路(社区线无 jvm 变体、上游线无 ohos 变体)。实测:
./gradlew :reveal-common:compileKotlinOhosArm64 :reveal-common:compileKotlinOhosX64 # BUILD SUCCESSFUL
./gradlew :reveal-core:compileKotlinOhosArm64 :reveal-core:compileKotlinOhosX64 # BUILD SUCCESSFUL
./gradlew :reveal-shapes:compileKotlinOhosArm64 :reveal-shapes:compileKotlinOhosX64 # BUILD SUCCESSFUL
三个库模块的 commonMain 文件一个没动,Reveal、DimRevealOverlayEffect 等 @Composable 全部编入 ohos klib(休眠待组合宿主),唯一的新增是 src/ohosMain 里的无头门面。
2.4 测试依赖的处置:测试统一在 JVM 跑
上游测试有三层:RevealOverlayArrangementTest(commonTest,纯几何)、ArrowTest(reveal-shapes commonTest)、三个 desktopTest 集成套件(runComposeUiTest 走真实 skiko 场景)。处置是两条纪律的组合:测试统一在 JVM 跑,根构建全局禁用 ohos 测试任务:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
desktopTest 的依赖按 jvm 线声明:org.jetbrains.compose.ui:ui-test:1.12.0 加 skiko 原生运行时 org.jetbrains.compose.desktop:desktop-jvm-windows-x64:1.12.0(上游用 compose 插件的 compose.desktop.currentOs,本构建不应用该插件,直接声明坐标)。这样上游测试文件零改动保留,ohos 侧只承担编译与链接。
第 3 阶段:门面与协议建模
3.1 JSON 契约
com.svenjacobs.reveal/**(上游引擎 + 形状库,原样编译)
↓ RevealHeadless 门面(reveal-core/src/ohosMain)
Bridge.kt(@CName × 2,扁平 JSON 进出)
↓ UTF-8 JSON
N-API revealVersion() / revealDispatch()
↓
ArkUI Index.ets(四页签)
请求与响应(完整契约见 example/BRIDGE_CONTRACT.md):
请求 : {"op":"configure","density":3,"screenWidthPx":1260,"screenHeightPx":2720,"rtl":false}
{"op":"register","key":"menu","leftPx":24,"topPx":96,"widthPx":120,"heightPx":88,
"shape":"roundrect","cornerDp":12,"paddingDp":10,"borderDp":2,"borderHex":"#FF4F46E5"}
{"op":"reveal","keys":"menu"}
{"op":"layout","overlayWidthPx":840,"overlayHeightPx":372,
"arrangement":"bottom","crossAlign":"center","confine":true}
响应 : {"ok":true,"visible":true,"current":["menu"],"previous":[],"registered":["menu","card","fab"],
"items":[{"key":"menu","area":{…},"layout":{…},"overlay":{…},"arrowX":0,"arrowY":null}]}
{"ok":false,"op":"reveal","error":"Revealables with keys \"x\" not found"}
op 共 8 个:configure / register / unregister / reveal / tryReveal / hide / snapshot / layout。请求是扁平 JSON(MiniJson 纪律,同 qrose / capsule / bubbles-compose):不支持嵌套对象与数组,列表用逗号分隔字符串;响应里的数组手工拼装。数字输出 4 位小数定点,绝不泄漏科学计数法。
3.2 无头门面:RevealHeadless
NAPI .so 里没有组合宿主,@Composable 编得进、跑不了。bubbles-compose 的 BubblesHeadless 给出范式:门面放在库模块自己的 ohosMain,签名不带任何 Compose 类型。放在库内的决定性好处是可以访问 internal API——Revealable.computeArea 是 internal,跨模块不可见,同模块的门面直接调:
public object RevealHeadless {
public fun register(
key: String, leftPx: Float, topPx: Float, widthPx: Float, heightPx: Float,
shape: RevealShape, padding: PaddingValues, borderStroke: BorderStroke?,
) {
state.addRevealable( // 真实 RevealState.addRevealable
Revealable(
key = key, shape = shape, padding = padding, borderStroke = borderStroke,
layout = Revealable.Layout(
offset = Offset(x = leftPx, y = topPx),
size = Size(width = widthPx, height = heightPx),
),
onClick = null,
),
)
}
public fun reveal(keys: List<String>) { runBlocking { state.reveal(keys) } }
public fun tryReveal(keys: List<String>): Boolean = runBlocking { state.tryReveal(keys) }
public fun hide() { runBlocking { state.hide() } }
public fun layout(
overlayWidthPx: Int, overlayHeightPx: Int,
arrangement: RevealOverlayArrangement.Horizontal,
crossAlignment: Alignment.Vertical, confineHeight: Boolean,
): List<RevealHeadlessItem> = currentItems { revealable, area ->
val revealableRect = area.toIntRect()
val layoutRect = arrangement.arrange( // 上游原版,含 #376 钳位
revealable = revealableRect, space = space,
confineHeight = confineHeight, layoutDirection = layoutDirection,
)
// …align + coerceWithin + 箭头锚点(逐字复述 RevealOverlayLayout 的放置算术)
}
}
三个设计点:其一,register 重复调用同一 key 就是上游"元素移动 / 尺寸变化后布局回调重新上报"的语义(addRevealable 会把新 Layout 同步进 current/previous 列表);其二,reveal() 只做状态更新立即返回(不会挂起等隐藏),runBlocking 包裹安全,门面全程不碰 Dispatchers.Main(鸿蒙线 Main 跨线程派发有已知问题);其三,layout 的放置算术(align() 结果 + 屏内钳位 + 箭头锚点)是对 RevealOverlayLayout(Compose Layout,无法无头运行)的逐字复述,状态机与 computeArea / arrange 则是上游原代码在跑——desktopTest 的集成测试是"复述没漂"的闸门。
3.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("RevealDispatch")
fun revealDispatch(request: String): String {
var op = ""
return try {
val json = MiniJson.parse(request)
op = json.str("op")
when (op) { "configure" -> …; "reveal" -> …; "layout" -> …
else -> err(op, "unsupported op") }
} catch (t: Throwable) {
err(op, t.message ?: t.toString()) // {"ok":false,"op":…,"error":…}
}
}
非法 JSON、缺 op / key、未知 key(对应上游 IllegalArgumentException)、不支持的 op,全部走同一个 ok:false 通道,ArkTS 侧统一渲染进状态行与日志。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libreveal_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosreveal.so)
│ RevealDispatch / RevealVersion
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复刻引导语义,所以选择 C ABI + JSON。
4.2 Kotlin/Native 导出函数
@CName("RevealVersion")
fun revealVersion(): String = buildString {
append("{\"ok\":true,\"name\":\"reveal\",\"version\":\"5.2.2-ohos.1\"")
append(",\"upstream\":\"5.2.2\",\"kotlin\":\"2.4.20-0.1.0-07\"")
append(",\"compose\":\"1.12.0-0.1.0-07\",\"targets\":[\"ohosArm64\",\"ohosX64\"]}")
}
@CName("RevealDispatch")
fun revealDispatch(request: String): String // 8 个 op 一口进
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
RevealDispatch | revealDispatch(req) | configure 会话 + register/unregister + reveal/tryReveal/hide 状态机 + snapshot + layout 几何 |
RevealVersion | revealVersion() | fork 版本握手(真机"关于"页版本行的数据来源) |
4.3 C++ N-API 方法分发
static napi_value NapiRevealDispatch(napi_env env, napi_callback_info info) {
// …argc / napi_typeof 校验必须是 string,OH_LOG_INFO 打字节数…
const char *raw = RevealDispatch(request.c_str());
napi_value out = nullptr;
napi_status st = napi_create_string_utf8(env, raw, NAPI_AUTO_LENGTH, &out);
DisposeKnString(raw); // 归还 Kotlin 运行时,绝不 free()
return out;
}
napi_property_descriptor desc[] = {
{"revealDispatch", nullptr, NapiRevealDispatch, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"revealVersion", nullptr, NapiRevealVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
KN 返回的字符串必须经 libohosreveal_symbols()->DisposeString 归还 Kotlin 运行时。libohosreveal_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。
4.4 CMake 和类型声明
set(KN_SO_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/${OHOS_ARCH})
set(KN_SO ${KN_SO_DIR}/libohosreveal.so) # 缺失 FATAL_ERROR,提示先跑 link 任务
add_library(ohosreveal SHARED IMPORTED)
set_target_properties(ohosreveal PROPERTIES IMPORTED_LOCATION ${KN_SO} IMPORTED_NO_SONAME TRUE)
add_library(reveal_napi SHARED napi_init.cpp)
target_link_options(reveal_napi PRIVATE "-Wl,--no-undefined" "-Wl,-rpath,\$ORIGIN")
target_link_libraries(reveal_napi PUBLIC libace_napi.z.so libhilog_ndk.z.so ohosreveal)
# POST_BUILD 拷贝 libohosreveal.so 与 libc++_shared.so 到 NAPI 同目录
OHOS_ARCH 为空或 libohosreveal.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const revealDispatch: (request: string) => string;
export const revealVersion: () => string;
第 5 阶段:ArkUI 宿主
5.1 注入屏幕参数和触发操作
aboutToAppear(): void {
const d = display.getDefaultDisplaySync();
this.density = d.densityPixels;
this.screenW = d.width; // 物理像素
this.screenH = d.height;
const raw = revealVersion(); // 版本握手:来自真实 Native 调用
const parsed = JSON.parse(raw) as VersionResp;
this.versionLine = `${parsed.version} · upstream ${parsed.upstream} · Kotlin ${parsed.kotlin} …`;
const req: ConfigureReq = { op: 'configure', density: this.density, fontScale: 1,
screenWidthPx: this.screenW, screenHeightPx: this.screenH, rtl: false };
this.send(JSON.stringify(req));
}
// 引导目标的位置上报(对应上游 onGloballyPositioned)
.onAreaChange((_o: Area, n: Area) => {
this.registerTarget('menu',
Number(n.globalPosition.x), Number(n.globalPosition.y),
Number(n.width), Number(n.height)); // Area 的 Length 类型必须显式转 number
})
每个引导步骤的调用链:reveal(keys) → layout(overlayWidthPx, …) → 拿到 area(挖孔)/ overlay(气泡位置)/ arrowX·arrowY(箭头锚点)→ Canvas 重绘 + 气泡组件重新定位。
5.2 页面交互
渲染实现对应上游 DimRevealOverlayEffect 的 drawRect + drawPath(BlendMode.DstOut):全屏 Canvas 先画 rgba(0,0,0,0.72) 变暗层,globalCompositeOperation='destination-out' 后按挖孔路径(rect / arcTo 拼装的圆角矩形 / arc 圆形)抠洞,再画气泡底与箭头三角;文字与按钮是定位到 overlay 矩形上的 ArkUI 组件。坐标单位纪律:桥进出全是物理像素,Canvas ctx.scale(1/density, 1/density) 后直接用 px,组件定位 px / density 转 vp。
每条操作日志带 [HH:mm:ss] 时间戳,→ 行显示请求、← 行显示响应,最新在前、上限 100 条。
5.3 对比度和错误边界
主操作按钮深色系、破坏性操作(hide / 移除)红色,语义对齐;ok:false 时错误原文进状态行与日志。Native 调用本身抛错(理论不该发生)同样被 try/catch 收进日志区。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosreveal)+ copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ └── Bridge.kt @CName × 2 + MiniJson(扁平 JSON)
├── harmonyApp/
│ ├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
│ ├── AppScope/app.json5 bundleName org.terminator.ohos.reveal、versionName 5.2.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/libohosreveal_api.h
│ ├── src/main/cpp/types/libreveal_napi/{index.d.ts, oh-package.json5}
│ └── src/main/ets/pages/Index.ets ArkUI 四页签验收页面
└── BRIDGE_CONTRACT.md 桥接 JSON 契约
6.2 构建、签名和安装
./gradlew :reveal-core:compileKotlinOhosArm64 :reveal-core:compileKotlinOhosX64 # klib
./gradlew desktopTest # 21 项交叉验证
./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.reveal -m entry
四、完整代码对照
4.1 整体架构
com.svenjacobs.reveal/**(上游引擎原样编译)
│ RevealState 状态机 / Revealable.computeArea / RevealOverlayArrangement
com.svenjacobs.reveal.shapes/**(上游气泡形状库原样编译)
│ Arrow / Balloon / BalloonShape(待组合宿主接回)
RevealHeadless(reveal-core/src/ohosMain,无头门面)
│ register/reveal/tryReveal/hide/snapshot/layout
Bridge.kt + MiniJson
│ RevealDispatch / RevealVersion
libreveal_napi.so
│ revealDispatch / revealVersion
Index.ets
│ JSON parse + @State 更新 + Canvas 渲染
▼
ArkUI 真机页面(四页签)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_reveal、只 include 4 模块 |
gradle/libs.versions.toml | Kotlin 2.4.20-0.1.0-07 + Compose/coroutines 双制品线钉版 + compose 编译器插件 |
build.gradle.kts(根) | 版本 5.2.2-ohos.1、全局禁用 *Ohos*Test* 任务 |
reveal-{common,core,shapes}/build.gradle.kts | 纯 KMP:jvm(“desktop”) + ohosArm64 + ohosX64,分线依赖,skikoMain 复用接线 |
reveal-core/src/ohosMain/.../RevealHeadless.ohos.kt | 唯一新增库源文件(无头门面,访问 internal computeArea) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、copyKnSo* 回填三件套 |
nativeApp/.../Bridge.kt | 2 个 C ABI、8 个 op、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 四页签验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(15 节) |
doc/开源鸿蒙平台 CMP 三方库「reveal」适配全流程.md | 本文 |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | RevealState.reveal(keys) / tryReveal / hide | 揭示 / 宽容揭示 / 隐藏,suspend,内部 Mutex 串行 + 去重 |
| 引擎 | RevealState.addRevealable / removeRevealable | 注册 / 移除目标;重复注册同步新 Layout;最后 current 项移除自动隐藏 |
| 引擎 | Revealable.computeArea | 挖孔区域:density、布局方向、padding 外扩、Circle 外接圆 |
| 引擎 | RevealOverlayArrangement.arrange / align | 四向排布 + 屏内钳位(上游 issue #376 修复)与交叉对齐 |
| 门面 | RevealHeadless.layout | computeArea + arrange/align + 放置算术 → 挖孔 / 排布区 / 气泡矩形 / 箭头锚点 |
| Native | RevealDispatch / RevealVersion | C ABI JSON 导出 |
| N-API | revealDispatch(req) / revealVersion() | 向 ArkTS 暴露能力 |
| ArkTS | dispatch / sendLayout | 组请求、触发操作、刷新 UI 与日志 |
4.4 ArkTS 与 Kotlin 的边界
const revealReq: RevealReq = { op: 'reveal', keys: 'menu' };
this.send(JSON.stringify(revealReq));
const layoutReq: LayoutReq = { op: 'layout', overlayWidthPx: 840, overlayHeightPx: 372,
arrangement: 'bottom', crossAlign: 'center', confine: true };
const resp = JSON.parse(revealDispatch(JSON.stringify(layoutReq))) as LayoutResp;
// resp.items[0].area = 挖孔矩形;resp.items[0].overlay = 气泡位置;arrowX = 箭头锚点
RevealHeadless.register(key = "menu", leftPx = 24f, topPx = 96f, widthPx = 120f, heightPx = 88f,
shape = RevealHeadless.shapeRoundRect(12f),
padding = RevealHeadless.padding(10f),
borderStroke = RevealHeadless.border(2f, 0xFF4F46E5))
runBlocking { state.reveal(listOf("menu")) } // 门面内部
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:整库编译,而不是提取内核或 ArkTS 重写
社区已过审库给出三条路:qrose / Cloudy 的"纯内核提取 + Compose 面停靠"(适用于有零依赖内核的库)、Orbital 的"ArkTS 语义重写"(适用于能力全部构建在 Compose 运行时机制上的库)、capsule / bubbles-compose 的"整库编译 + 无头门面"。reveal 100% 是 Compose 代码、没有可隔离的内核,而社区 ohos 线已升到与上游同代的 1.12.0——整库编译让状态机、几何、排布全部是上游原件,重写反而引入漂移风险。
决策 2:工具链升到 Kotlin 2.4.20-0.1.0-07,Gradle 降到 9.6.1
compose 1.12.0-0.1.0-07 制品线构建在 2.4.20-0.1.0-07 上,工具链版本跟着制品线走;上游本来就是 Kotlin 2.4.21,同代零降级。Gradle 方面 2.4.20 线与 9.6.1 是 capsule / KStore 验证过的组合,上游 wrapper 9.8.1 在该线上未验证,主动降级。
决策 3:根构建替换(cmp_qrose 模式),上游遗留留盘
上游根构建(kotlinter / BCV / roborazzi / dokka / vanniktech 发布 / AGP classpath)对鸿蒙没有保留价值,整个替换为极简 ohos 构建,原件封存为 *.upstream。android-tests / demo-app / convention-plugins 留盘不进 settings.gradle.kts——它们的存在方式只有这一种。
决策 4:无头门面放在库的 ohosMain,skiko actual 直接复用
门面与桥的分界:门面在 reveal-core/src/ohosMain(能访问 internal 的 computeArea,签名不带 Compose 类型),桥在 example/nativeApp(零 compose 依赖,@CName 导出)。ohos 目标不新写 popup actual,而是 ohosMain.dependsOn(skikoMain) 复用上游 skiko 弱化版——社区 Compose ohos 后端就是 skiko 系,上游桌面端"popup 无法触摸穿透"的既有限制在鸿蒙侧同样成立且如实记录。
决策 5:desktopTest 交叉验证 + 测试零改动
引擎逻辑在 commonMain,JVM 与 ohos 跑同一份代码;21 项 desktopTest 跑的就是上游原测试文件(含走真实 skiko 场景的三套集成测试),是"引擎没漂"的闸门。compose ui-test 不全有 ohos 制品,处置不是改测试,而是"测试统一 JVM + 根构建禁用 *Ohos*Test*"——测试文件一个字节不动,交叉验证的有效性才成立。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)、JDK 21、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
./gradlew desktopTest
21 项用例全部通过(上游原测试文件,零改动):
| 测试类 | 用例数 | 钉住什么 |
|---|---|---|
RevealOverlayArrangementTest | 4 | arrange 几何契约 + issue #376 回归 |
RevealMultipleRevealablesTest | 5 | 多项揭示的注册 / 揭示 / 移除闭环 |
RevealOverlayEdgeFlushTest | 5 | 目标贴边时的排布塌缩 |
RevealOverlayGeometryTest | 3 | overlay 几何集成(skiko 真实场景) |
ArrowTest | 4 | 箭头钳位几何(reveal-shapes) |
依赖换线没有 API 漂移:jvm 线的 Compose 1.12.0 与上游完全同版,上游测试照跑全绿——这正是"换线不换版"的直接证据。
6.3 产物与 HAP 验证
:reveal-common/:reveal-core/:reveal-shapes:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / OhosX64: BUILD SUCCESSFUL
llvm-nm -D: RevealDispatch / RevealVersion 均为全局 T 符号
libohosreveal.so: arm64 29,138,648 B / x86_64 27,881,248 B(debugShared 未 strip,
含 Kotlin/Native 运行时 + Compose 运行时 + reveal 引擎)
libc++_shared.so: 1,850,184 B / 1,731,432 B(copyKnSo* 自动回填 harmonyApp/entry/libs)
CompileArkTS: finished(ArkTS 严格模式修复后)
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
6.4 功能用例
- 启动后"关于"页日志出现
revealVersion() → 5.2.2-ohos.1 · upstream 5.2.2 · Kotlin 2.4.20-0.1.0-07 · Compose 1.12.0-0.1.0-07(来自真实 Native 调用,非静态文字); configure自动注入真机屏幕物理像素与 density,onAreaChange把三个引导目标的真实布局位置回注册给引擎;- 点"开始引导",第一步挖孔精确套住头部菜单按钮(圆角矩形 + padding 外扩 + 描边),气泡排布在下方居中,箭头指向目标中心,日志出现
→ reveal [menu]与← layout menu 挖孔(…); - 点挖孔区进入下一步(卡片 → FAB 圆形挖孔外接),点"跳过"或点遮罩外部返回
hide(); - 几何页签把目标拖到屏幕边缘,排布区按上游钳位规则塌缩、气泡被推回屏内;
- 状态机页签:
reveal A,B后current=[A,B];reveal B后previous=[A];tryReveal Z返回revealed=false不崩;移除最后一个 current 项后visible自动变false; - 每步操作的操作日志按时间戳滚动留痕(上限 100 条)。
6.5 验证结论
状态机、挖孔几何、排布引擎、形状库、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的教学引导会话:版本握手、三步引导、挖孔跟随、箭头锚点、状态机双列表与自动隐藏语义(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/reveal.git
cd reveal
export JAVA_HOME="/path/to/jdk-21"
./gradlew :reveal-common:compileKotlinOhosArm64 :reveal-core:compileKotlinOhosArm64 \
:reveal-shapes:compileKotlinOhosArm64 # klib
./gradlew desktopTest # 21 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 \
:example:nativeApp:linkDebugSharedOhosX64 # 双 ABI so + 三件套回填
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.reveal -m entry
hdc shell hilog | grep Reveal
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让三个库模块真正编译成
ohosArm64/ohosX64KLIB 与动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- Compose 依赖不能声明在 commonMain:Gradle 元数据会把同一坐标坍缩成一个版本,而 ohos 线无 jvm 变体、上游线无 ohos 变体——必须按源码集分线(ohosMain / desktopMain)声明;
- 上游提交的
gradle/gradle-daemon-jvm.properties(toolchainVersion=25)会让 Gradle 在配置期寻找 JDK 25 daemon,ohos 构建用 JDK 21,必须移除(封存.upstream); - eazytec Nexus 是懒代理,首次拉取制品可能超过默认 30s 超时,
gradle.properties配 180s socket/connection 超时兜底; - 自定义命名目标(
jvm("desktop")、ohos)在同一构建脚本里不生成类型化源集访问器,desktopMain.dependencies {}直接写会Unresolved reference,要用val desktopMain by getting惯用法; - 桥接层 Float/Double 要严格对齐:门面签名是 Float,JSON 解析出的 Double 必须
.toFloat(),否则 Kotlin/Native 编译直接红; - ArkTS 严格模式四连:裸对象字面量必须对应显式接口(
const req: XxxReq = {...}再 stringify);Area.globalPosition.x等是Length类型(number | string),传给number参数要Number(...)转换;组件不能直接当参数传给.overlay(),要抽@Builder;Circle().fill()需要 API 26,5.1.0(18) 上改用Column + borderRadius + backgroundColor; - JSON 里的
null与undefined在 ArkTS 侧要同时判断,否则箭头轴向判断会错; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - 签名材料由 DevEco 自动写入
build-profile.json5,证书绝不进入 AtomGit 仓库。
8.2 已知问题
- 鸿蒙侧无组合宿主:
Reveal/DimRevealOverlayEffect/ Balloon 等@Composable已编入 klib 但休眠,视觉效果由 ArkTS 按引擎几何复现——跑着的是引擎,不是 Compose UI; - 桥的放置算术是对
RevealOverlayLayout(ComposeLayout)的逐字复述,不是同一份代码在跑;状态机与computeArea/arrange是上游原版,desktopTest 是"复述没漂"的闸门; - 触摸穿透(
OnClick.Passthrough)未桥接:上游 Android 靠窗口 flag,skiko 系后端本就不支持; - 入场/退场动画未桥接(
animateFloatAsState/movableContent是组合内机制),previousRevealables通过 snapshot 可观察,淡出动画留给 ArkTS 侧; RevealShape.Custom(Kotlin lambda)不跨桥,桥支持 Rect / Circle / RoundRect 三种声明式形状;- android / ios / js / wasmJs 目标剥离(源文件留在树里),发布、dokka、kover、BCV 一并剥离,本 fork 不发 Maven Central。
8.3 未来优化方向
- ArkTS 侧补齐淡入淡出动画(消费
previous名单与 per-item alpha); - 气泡箭头接入 reveal-shapes 的 Arrow 模型(clampArrowOffset 已在 klib 中,等组合宿主或桥接补充);
- 等社区 Compose-ohos 组合宿主方案成熟后接回
@Composable面,桥降级为备用路径——库本体已为 ohos 编译完毕; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套。
九、总结
9.1 核心难点回顾
上游 reveal 5.2.2(100% Compose,无零依赖内核)
→ 验证社区 Compose ohos 制品线(1.12.0-0.1.0-07,与上游同代)
→ 工具链对齐 Kotlin 2.4.20-0.1.0-07(Gradle 9.6.1,daemon-jvm.properties 移除)
→ 根构建替换(cmp_qrose 模式,*.upstream 封存)
→ 双制品线分线声明 + skikoMain 复用 → 三模块零源码手术整库编译
→ RevealHeadless 无头门面(internal computeArea 可达,suspend 状态机可驱动)
→ C ABI(@CName × 2)+ 8 op 扁平 JSON
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON 解析 + Canvas 挖孔渲染
→ ArkUI 真机四页签验收(引导 / 几何 / 状态机 / 版本握手)
9.2 封装层次
- 引擎层:上游原版
RevealState(Mutex 串行、去重、previous 语义、自动隐藏)、Revealable.computeArea、RevealOverlayArrangement; - 形状层:上游原版 Arrow / Balloon / BalloonShape(随编译进 klib,待组合宿主);
- 门面层:
RevealHeadless——compose-free 签名、runBlocking收编 suspend、放置算术复述; - Native 层:双 ABI 动态库,2 个 C ABI 符号输出 JSON;
- N-API 层:参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:页面状态、请求组包、Canvas 渲染、日志与错误文本;
- DevEco 层:CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 先验证生态制品线再定适配路线:社区 Compose ohos 线升到与上游同代的 1.12.0 后,纯 Compose UI 库的正确答案从"隔离内核"(qrose 时代)变成"整库编译 + 无头门面"(capsule / bubbles / 本仓)——路线选择必须基于当天的制品可达性,而不是历史结论;
- 双制品线的关键是"换线不换版"加"分线声明":ohos 线与上游线版本主体保持一致,依赖按源码集声明而不是坍缩在 commonMain,交叉验证目标才能与上游 CI 跑同一条链路;
- 无头门面放库内、桥放库外:门面在库的 ohosMain 里能访问 internal API 并保持 compose-free 签名,桥与 Demo 因此零 Compose 依赖;"Kotlin 算几何,ArkTS 画"的边界让每一层都能被单独验证(desktopTest 钉引擎、llvm-nm 钉符号、真机页面钉链路)。
9.4 适配成果
当前适配已完成:
- reveal
5.2.2三个库模块的双 ABI Kotlin/Native 编译(上游 commonMain 零重写,唯一新增库源文件是 ohosMain 无头门面); - 社区 Compose 1.12.0 ohos 制品线在纯 UI 效果库上的完整落地(runtime / saveable / foundation / animation / ui 双 ABI + compose 编译器插件走 Kotlin 线);
- 2 个 C ABI 符号、8 个 op 的桥接协议和 C++ N-API 包装层(
llvm-nm验证全局 T 符号); - DevEco Stage ArkUI 四页签验收页面(引导、几何、状态机、版本握手、操作日志);
- 21 项 JVM 交叉验证全绿(上游原测试文件)+ 双 ABI 动态库链接(arm64 约 29.1MB / x64 约 27.9MB,debugShared 未 strip)、HAP 构建、签名、设备安装和真机验证;
- 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)