本文记录 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 MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
JDK21(gradle.properties 的 org.gradle.java.home 指向本机 JDK 21)
OpenHarmony 目标ohosArm64 + ohosX64(jvm("desktop") 保留作交叉验证)
Gradle9.6.1(自上游 9.8.1 降级;2.4.20 线与 9.6.1 是已验证组合)
Composeohos 线 1.12.0-0.1.0-07(eazytec);jvm 线 1.12.0(Maven Central,与上游同版)
coroutinesohos 线 1.11.0-0.1.0-07 / jvm 线 1.10.2(RevealState 的 Mutex 需要)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.reveal
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块commonMain 文件依赖形态处置
reveal-common1compose.runtime(箭头锚点的 snapshot state + CompositionLocal)整体编译(jvm + 双 ohos)
reveal-core17compose.runtime / foundation / ui / animation + coroutines(Mutex)整体编译(零源码手术)
reveal-shapes3compose.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 方法覆盖内核
RevealDispatchrevealDispatch(req)configure 会话 + register/unregister + reveal/tryReveal/hide 状态机 + snapshot + layout 几何
RevealVersionrevealVersion()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.tomlKotlin 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.kt2 个 C ABI、8 个 op、MiniJson、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 四页签验收页面
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.layoutcomputeArea + arrange/align + 放置算术 → 挖孔 / 排布区 / 气泡矩形 / 箭头锚点
NativeRevealDispatch / RevealVersionC ABI JSON 导出
N-APIrevealDispatch(req) / revealVersion()向 ArkTS 暴露能力
ArkTSdispatch / 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 项用例全部通过(上游原测试文件,零改动):

测试类用例数钉住什么
RevealOverlayArrangementTest4arrange 几何契约 + issue #376 回归
RevealMultipleRevealablesTest5多项揭示的注册 / 揭示 / 移除闭环
RevealOverlayEdgeFlushTest5目标贴边时的排布塌缩
RevealOverlayGeometryTest3overlay 几何集成(skiko 真实场景)
ArrowTest4箭头钳位几何(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 功能用例

  1. 启动后"关于"页日志出现 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 调用,非静态文字);
  2. configure 自动注入真机屏幕物理像素与 density,onAreaChange 把三个引导目标的真实布局位置回注册给引擎;
  3. 点"开始引导",第一步挖孔精确套住头部菜单按钮(圆角矩形 + padding 外扩 + 描边),气泡排布在下方居中,箭头指向目标中心,日志出现 → reveal [menu] 与 ← layout menu 挖孔(…);
  4. 点挖孔区进入下一步(卡片 → FAB 圆形挖孔外接),点"跳过"或点遮罩外部返回 hide();
  5. 几何页签把目标拖到屏幕边缘,排布区按上游钳位规则塌缩、气泡被推回屏内;
  6. 状态机页签:reveal A,B 后 current=[A,B];reveal B 后 previous=[A];tryReveal Z 返回 revealed=false 不崩;移除最后一个 current 项后 visible 自动变 false;
  7. 每步操作的操作日志按时间戳滚动留痕(上限 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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让三个库模块真正编译成 ohosArm64 / ohosX64 KLIB 与动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Compose 依赖不能声明在 commonMain:Gradle 元数据会把同一坐标坍缩成一个版本,而 ohos 线无 jvm 变体、上游线无 ohos 变体——必须按源码集分线(ohosMain / desktopMain)声明;
  4. 上游提交的 gradle/gradle-daemon-jvm.properties(toolchainVersion=25)会让 Gradle 在配置期寻找 JDK 25 daemon,ohos 构建用 JDK 21,必须移除(封存 .upstream);
  5. eazytec Nexus 是懒代理,首次拉取制品可能超过默认 30s 超时,gradle.properties 配 180s socket/connection 超时兜底;
  6. 自定义命名目标(jvm("desktop")、ohos)在同一构建脚本里不生成类型化源集访问器,desktopMain.dependencies {} 直接写会 Unresolved reference,要用 val desktopMain by getting 惯用法;
  7. 桥接层 Float/Double 要严格对齐:门面签名是 Float,JSON 解析出的 Double 必须 .toFloat(),否则 Kotlin/Native 编译直接红;
  8. 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;
  9. JSON 里的 null 与 undefined 在 ArkTS 侧要同时判断,否则箭头轴向判断会错;
  10. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  11. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  12. 签名材料由 DevEco 自动写入 build-profile.json5,证书绝不进入 AtomGit 仓库。

8.2 已知问题

  • 鸿蒙侧无组合宿主:Reveal / DimRevealOverlayEffect / Balloon 等 @Composable 已编入 klib 但休眠,视觉效果由 ArkTS 按引擎几何复现——跑着的是引擎,不是 Compose UI;
  • 桥的放置算术是对 RevealOverlayLayout(Compose Layout)的逐字复述,不是同一份代码在跑;状态机与 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 三条经验

  1. 先验证生态制品线再定适配路线:社区 Compose ohos 线升到与上游同代的 1.12.0 后,纯 Compose UI 库的正确答案从"隔离内核"(qrose 时代)变成"整库编译 + 无头门面"(capsule / bubbles / 本仓)——路线选择必须基于当天的制品可达性,而不是历史结论;
  2. 双制品线的关键是"换线不换版"加"分线声明":ohos 线与上游线版本主体保持一致,依赖按源码集声明而不是坍缩在 commonMain,交叉验证目标才能与上游 CI 跑同一条链路;
  3. 无头门面放库内、桥放库外:门面在库的 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。

参考文档

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐