本文记录 bubbles-compose 接入 OpenHarmony 的完整过程,覆盖纯 Compose UI 控件库的路线选型、社区 Compose 鸿蒙制品线的依赖分线、整个上游 commonMain 的真实编译、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、expect/actual 接缝补位、无头门面(headless facade)、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

bubbles-compose 是 iOS「Bubbles UI」风格的 Compose Multiplatform 主题与控件库:明暗 ColorScheme(19 个语义色)、iOS 字阶 Typography(11 档)、Shapes(5 档圆角)、20+ 个 iOS 系统色(含高对比度无障碍变体),以及 Button / CheckBox(三态)/ RadioButton / Switch / Slider / SegmentedControl / TabRow / Scaffold / Nav bars / TextField / Separator 等控件和 bubblesTween / BubblesEasing 动画包。本次适配与社区早期路线有本质区别——qrose 把纯 Kotlin 编码器内核与 Compose Painter 隔离、KStore 直接换 kotlinx ohos 制品线,而 bubbles-compose 的公开 API 100% 是 Compose Composable,没有可提取的"零依赖内核",主题模型本身就是控件库。因此本仓走的是 compose-shimmer / eungabi 开创的路线:上游 commonMain 整库一行不改地编译进鸿蒙动态库,无头门面把真实主题 token、控件几何 token 与真实动画引擎(真实 Animatable 跑真实 bubblesTween(BubblesEasing))提升为 JSON C ABI,ArkUI 拿真实数据渲染控件并逐帧采样——页面上每个色值、每个尺寸、每一帧动画都来自 Kotlin 侧的编译产物,停止动画引擎画面立即冻结。

项目地址: AtomGit/oh-tpc/bubbles-compose.git

上游仓库: kalist28/bubbles-compose

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配

bubbles-compose 为 Compose 应用提供"iOS 设计语言"这一件事的完整答案:BubblesTheme 一行建主题(自动跟随系统深浅色),ColorScheme 给出 19 个语义色 token,Typography 摆好 11 档 iOS 字阶,Shapes 定下 5 档圆角,BubblesColors 内置全套 iOS 系统色(红橙黄绿蓝紫 + 灰阶 1~8,每个都带深色与高对比度变体),控件层则把 Button / Switch / Slider / 选择器 / 分段控件 / 标签行 / 脚手架 / 导航栏 / 输入框 / 分隔线全部收进同一套 token 体系,动效统一走 bubblesTween(400ms,cubic-bezier 0.2833/0.99/0.31833/0.99 的 iOS 视图过渡曲线)。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、浏览器共享。OpenHarmony 应用同样需要这套成体系的设计语言,但上游构建(Kotlin 2.2.0 / Gradle 8.13 / AGP 8.11.0)不会生成 OpenHarmony 变体。

「给鸿蒙写一套 ArkTS 版 Bubbles」不是适配——那是重写,主题值、字阶、圆角、动效曲线会立刻与上游漂移。如果只把示例页面改成 ArkTS,页面可以显示色块,却无法证明上游控件库本体在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证主题模型、控件几何、动画机制、Compose 制品线、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
没有纯内核库的全部 API 都是 @Composable,qrose 式"抽取编码器"路线不适用。
工具链不一致上游 Kotlin 2.2.0 / Gradle 8.13 / AGP 8.11.0(com.android.kotlin.multiplatform.library)与鸿蒙工具链无法共存于一个 Gradle 构建。
依赖矩阵commonMain 声明了 lifecycle-viewmodel-compose / lifecycle-runtime-compose(2.9.1)——ohos 制品线不发布;且这是构建声明与源码实际引用不一致的上游遗留问题。
API 代差上游按 Compose 1.8.2 编写;社区 ohos 线是 1.12.0-0.1.0-07,个别 API 标注收紧(BlurEffect.convertRadiusToSigma 变为 @InternalComposeUiApi)。
无组合宿主社区 Compose ohos 线发布 ui/foundation/animation 的 ohos klib,但 OHOS 上还没有 composition host,Composable 无法直接上屏。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。

1.2 库提供的能力

Demo 提供一个真实的"Bubbles 主题会话",对应桥接的全部入口:

  • 主题(theme):真实 lightColorScheme() / darkColorScheme() 的 19 个语义色 token(accent / label×4 / systemFill×4 / placeholderText / separator×2 / link / background×6),切换深浅色即时重新取值,页面自身的底色、卡片色、文字色也用 token 渲染——页面即主题;
  • 调色板(palette):真实 BubblesColors.system*(dark, highContrast) 的 21 个 iOS 动态系统色 + 14 个固定色,highContrast 直通上游自己的无障碍分支;
  • 排版与形状(typography / shapes):11 档字阶逐档列出(字号/行高/字重)并按真实字号渲染,5 档圆角(4/8/12/16/24dp)来自真实 Shapes;
  • 控件(tokens + theme):Button(filled/tinted/clear)、Switch、CheckBox(双态/三态)、Radio、Slider、SegmentedControl、TabRow、TextField、Divider 的交互画廊——尺寸取自真实 token(Switch 51×31dp、图标按钮 42dp、按钮按压/描边透明度),颜色取自当前主题;
  • 无障碍(contrast):真实 Color.accessible(isDark) 对比度调整(向白/黑 lerp 20%)前后对照;
  • 动效(anim.*):真实 Animatable 跑真实 bubblesTween(duration, easing = BubblesEasing),小球位置由 33ms 轮询采样驱动,采样序列绘出缓动曲线;涟漪默认时序(450ms 展开 / 180ms 淡出)来自库内真实对象;
  • 版本握手:bubblesVersion() 返回 fork 版本号,来自真实 Native 调用而非静态文字。

响应统一走 {"ok":true,"op":...} / {"ok":false,"op":...,"error":...} 通道:

字段作用
ok / op操作是否成功、回显本次操作。
colors[].name/argb/hex主题与调色板条目;hex 为 ArkUI 直用的 #RRGGBB / #AARRGGBB。
styles[].fontSizeSp/lineHeightSp/fontWeight字阶档位指标。
switchWidthDp 等控件几何 token。
value / elapsedMillis / durationMillis动画引擎实时采样。

引擎侧完整能力(全部控件 Composable、涟漪 Indication、触感/无障碍 expect-actual 体系)全部在上游 commonMain 里原样随编译进入 .so,ArkTS 侧只消费门面暴露的数据面。

1.3 实现适配

维度要求
代码复用bubbles 全部 commonMain 文件(30 个 Kotlin 源文件)原样编译,不提取、不改写;新增源码仅 ohosMain 两个文件(无头门面 + Haze scrim actual)。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);composeApp / iosApp / kotlin-js-store / build-configuration 留盘不进构建。
制品纪律换线不换版:Compose 用社区 ohos 线 1.12.0-0.1.0-07,JVM 交叉验证用 Maven Central 1.11.0(同代);coroutines ohos 线 1.11.0-0.1.0-07 / JVM 1.10.2。
桥接稳定2 个 C ABI(BubblesDispatch / BubblesVersion)+ UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
UI 完整单页五 Tab 覆盖主题 / 调色板 / 控件 / 动效 / 关于全部桥接入口。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现——OHOS 尚无社区 Compose 线的组合宿主。页面上每个色值、每个尺寸、每一帧动画都来自 libohosbubbles.so 里编译进的上游库对象,这是"真编译"与"照着写"的分界线。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、验证社区 Compose ohos 制品线、确定「整库编译」路线
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版、lifecycle 剥离
第 3 阶段:源集与接缝     ── nonIosMain / skikoMain 重接线、Accessibility/触感复用、HazeNode scrim 降级、整库 ohos 编译
第 4 阶段:无头门面       ── BubblesHeadless:主题/调色板/字阶/形状/token/无障碍/真实动画引擎
第 5 阶段:原生桥接       ── @CName C ABI、MiniJson、C++ N-API、CMake、参数校验和内存释放
第 6 阶段:ArkUI 宿主     ── 五 Tab 验收页面(主题即页面、token 驱动画廊、动画逐帧采样)
第 7 阶段:示例与验证     ── 双 ABI 动态库、符号表验证、HAP、签名、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
bubbles/                               上游控件库(进 ohos 构建,jvm + ohosArm64 + ohosX64)
bubbles/src/commonMain/                30 个 Kotlin 源文件:主题/排版/形状/全部控件/动画包,原样编译,零改动
bubbles/src/nonIosMain/                上游 non-iOS actual(Accessibility 返回 false、LocalHapticFeedback),ohos 经 dependsOn 复用
bubbles/src/ohosMain/                  新增:BubblesHeadless 无头门面 + HazeNode scrim actual(全库仅有的新增源文件)
bubbles/src/skikoMain/                 上游 Skia RuntimeShader 模糊 actual,仅服务 jvm 目标,留盘不进 ohos
example/nativeApp/                     Kotlin/Native 动态库(Bridge + MiniJson,baseName=ohosbubbles)
example/harmonyApp/                    DevEco Stage、CMake N-API 和 ArkTS 页面(bundleName org.terminator.ohos.bubbles)
composeApp/ iosApp/ kotlin-js-store/ build-configuration/   上游演示与发布工程,留盘不进构建
README.OpenHarmony.md / _CN.md         鸿蒙适配说明(中英双份)
README.upstream.md                     上游原文保存;上游构建文件以 *.upstream 后缀留盘

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :bubbles 与 :example:nativeApp,rootProject.name = "ohos_bubbles"。上游的 AGP classpath、compose-hot-reload、vanniktech 发布、nyx 发布流程全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
Compose(ohos 线)社区 org.jetbrains.compose.* 1.12.0-0.1.0-07(runtime / runtime-saveable / foundation / animation / ui,ohos klib)
Compose(JVM 交叉验证线)Maven Central 1.11.0(同代,desktop 变体齐全)
coroutinesohos 线 1.11.0-0.1.0-07 / JVM 线 1.10.2(上游版本主体)
JDK21
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle9.6.1(2.4.20 线支持 Gradle 9.x,沿用 compose-shimmer 先例)
DevEco product5.1.0(18) 兼容
DevEco Studio26.0.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.bubbles
版本0.0.1-ohos.1(上游未发布 Maven Central,本地版本 0.0.1 + ohos 后缀)
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点

上游 bubbles/build.gradle.kts 在 commonMain 声明了 lifecycle-viewmodel-compose / lifecycle-runtime-compose(2.9.1),但全库源码没有任何文件 import androidx.lifecycle(逐文件核查)——典型的"幽灵依赖"。ohos 制品线不发布这两个坐标,处置:ohos 构建里直接剥离,源码零引用、剥离无副作用。

模块commonMain 文件依赖形态处置
bubbles30Compose runtime/foundation/animation/ui + coroutines(源码 import 面逐项核对);lifecycle 声明未被源码使用整体编译(jvm + 双 ohos),lifecycle 剥离

按源码实际 import 面收敛 Compose 四件套(rememberSaveable 补 runtime-saveable);compose.components.resources / uiToolingPreview 同样只在上游构建声明里、源码未用,一并剥离。

expect 声明全库 4 处:Accessibility.isHighContrastEnabled / isReduceTransparencyEnabled、rememberBubblesHapticFeedback()、HazeNode。处置见第 3 阶段。

1.4 页面边界
标题区          BUBBLES · OHOS / 版本握手行(CMP iOS 风格主题与控件库 · Kotlin/Native ohosArm64/ohosX64)
Tab 切换        主题 / 调色板 / 控件 / 动效 / 关于
主题区          浅色/深色切换 + 19 token 色块网格 + Shapes 圆角卡 + Typography 字阶列表
调色板区        深浅色 + 高对比度双开关,35 色网格;Color.accessible 对照卡
控件区          Button/Switch/CheckBox/Radio/Slider/SegmentedControl/TabRow/TextField/Divider 画廊
动效区          时长与缓动选择、播放/停止、进度条 + 小球 + 实时读数、缓动曲线采样图、涟漪时序卡
关于区          版本握手、库简介、架构分层、仓库与社区链接

页面启动时先调 bubblesVersion() 做握手,再依次拉取 theme / typography / shapes / tokens / ripple 建立本地状态。

第 2 阶段:目标与依赖打通

2.1 制品线验证(整库编译路线的成败手)

先查社区 Maven(https://maven.eazytec-cloud.com)上的 Compose ohos 线,用 .module 元数据逐个验证:

org.jetbrains.compose.ui:ui:1.12.0-0.1.0-07
  variants: ohosArm64ApiElements-published / ohosX64ApiElements-published
            + android/desktop/ios/macos 变体(根 artifact 重定向到子模块)
org.jetbrains.compose.animation:animation:1.12.0-0.1.0-07
  variants: 同上(ohosArm64 / ohosX64 齐全)
org.jetbrains.compose.runtime / runtime-saveable / foundation:…  同构

两个关键结论:其一,bubbles 源码 import 面(runtime/foundation/animation/ui)在 ohos 线全部有 ohosArm64/ohosX64 klib,「整个 commonMain 编译进 ohos」在依赖层面成立;其二,ohos 线不发布 *-desktop 子 artifact,JVM 交叉验证目标不能从这条线解析——所以 Compose 依赖按"目标线"声明(ohosMain 用社区线、jvmMain 用 Maven Central 1.11.0 线),与 compose-shimmer / eungabi 的做法一致:换线不换版。

2.2 工具链钉版(为什么是 2.4.20-0.1.0-07)

coroutines ohos 线 1.11.0-0.1.0-07 依赖 stdlib 2.4.20-0.1.0-07(元数据 2.4.0),Kotlin 2.2.x 只能读到 2.3.0,编译期直接报元数据不兼容。工具链版本跟着依赖走:Kotlin 钉 2.4.20-0.1.0-07(KStore 适配已验证过这条推导链)。Gradle 用 9.6.1(2.4.20 线支持 Gradle 9.x,compose-shimmer 同款 wrapper 直接沿用,上游 8.13 wrapper 一并升级)。

settings.gradle.kts 把 eazytec 双仓写在最前:

pluginManagement {
    repositories {
        // HarmonyOS Kotlin toolchain must resolve first, otherwise ohosArm64()
        // fails with "Unresolved reference" at configuration time.
        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_bubbles"

include(":bubbles")
include(":example:nativeApp")

gradle.properties 重写(上游留 gradle.properties.upstream):org.gradle.java.home 指向本机 JDK 21、version=0.0.1-ohos.1、kotlin.native.ignoreDisabledTargets=true、kotlin.native.distribution.downloadFromMaven=true。版本目录重写(上游留 .upstream),只保留 kotlin-multiplatform 与 kotlin-compose-compiler 两个插件别名(compose 编译器插件随 Kotlin 版本走,org.jetbrains.kotlin.plugin.compose:2.4.20-0.1.0-07)。根 build.gradle.kts 全局禁用 Ohos*Test* 任务——ohos 测试需要设备/模拟器,库当前没有 commonTest 源集,未来测试统一 JVM 跑。

2.3 编译真实控件库

bubbles/build.gradle.kts 重写为纯 KMP 模块(上游留 build.gradle.kts.upstream),双线依赖:

kotlin {
    applyDefaultHierarchyTemplate()

    compilerOptions {
        // compose 1.11/1.12 把 BlurEffect.convertRadiusToSigma(上游 skikoMain
        // 模糊 actual 使用)标为 @InternalComposeUiApi;上游按 1.8.2 编写时还是
        // public。源码不动——opt-in 随构建文件走。
        optIn.add("androidx.compose.ui.InternalComposeUiApi")
    }

    jvm { compilerOptions { jvmTarget.set(JvmTarget.JVM_21) } }  // 交叉验证
    ohosArm64()                                                  // 适配本体
    ohosX64()

    sourceSets {
        val nonIosMain by creating { dependsOn(commonMain.get()) }
        val skikoMain by creating { dependsOn(commonMain.get()) }
        jvmMain.get().dependsOn(nonIosMain)
        jvmMain.get().dependsOn(skikoMain)
        ohosMain.get().dependsOn(nonIosMain)      // ohos 复用 non-iOS actual

        ohosMain.dependencies {                  // 社区 ohos 线
            api(libs.compose.runtime)
            api(libs.compose.runtime.saveable)
            api(libs.compose.foundation)
            api(libs.compose.animation)
            api(libs.compose.ui)
            implementation(libs.kotlinx.coroutines.core)
        }
        jvmMain.dependencies {                   // Maven Central 上游线
            api(libs.compose.runtime.jvm)
            api(libs.compose.runtime.saveable.jvm)
            api(libs.compose.foundation.jvm)
            api(libs.compose.animation.jvm)
            api(libs.compose.ui.jvm)
            implementation(libs.kotlinx.coroutines.core.jvm)
        }
    }
}

实测:

./gradlew :bubbles:compileKotlinJvm                                    # BUILD SUCCESSFUL
./gradlew :bubbles:compileKotlinOhosArm64 :bubbles:compileKotlinOhosX64 # BUILD SUCCESSFUL

上游 commonMain 30 个文件一个没动,只有 ohosMain 两个新文件。

第 3 阶段:源集与接缝(技术难点)

3.1 自定义中间源集重接线

上游为「iOS 专属触感/无障碍」和「Skia 模糊」手工建了两个中间源集,按 8 个目标接线;ohos 构建只保留 jvm + 双 ohos,接线收敛:

val nonIosMain by creating { dependsOn(commonMain.get()) }
val skikoMain by creating { dependsOn(commonMain.get()) }
jvmMain.get().dependsOn(nonIosMain)
jvmMain.get().dependsOn(skikoMain)
ohosMain.get().dependsOn(nonIosMain)      // ohos 复用 non-iOS actual

上游 androidMain / iosMain / jsMain / wasmJsMain 目录与 appleMain / macosMain 接线全部留盘不进构建。

3.2 expect/actual 接缝的 ohos 补位
expect(commonMain)上游 actualohos 处置
Accessibility.isHighContrastEnabled / isReduceTransparencyEnablediosMain(UIKit)/ nonIosMain(false)复用 nonIosMain(返回 false),经 dependsOn 图自动生效
rememberBubblesHapticFeedback()iosMain(UIKit 生成器)/ nonIosMain(LocalHapticFeedback)复用 nonIosMain
HazeNode(glassmorphism 模糊节点)androidMain(半透明 scrim 降级)/ skikoMain(Skia RuntimeShader 全功能)新增 ohosMain scrim actual——与上游 androidMain 同策略

Haze.ohos.kt 的取舍:社区 Compose ohos 线没有暴露 skikoMain 模糊着色器所需的 org.jetbrains.skia.RuntimeEffect 面,且 OHOS 尚无组合宿主;上游在 Android 上对不支持 RenderEffect 的设备本来就是画 scrim 降级——ohos 沿用同一策略,skikoMain 的全功能模糊继续作为 jvm/desktop actual。这是全库唯一新增的 actual 类(约 50 行,与上游 androidMain 逐行同构)。

3.3 API 代差:opt-in 进构建文件

JVM 交叉验证在 Compose 1.11.0 线上撞到唯一的代差:上游 Haze.skiko.kt 调用的 BlurEffect.convertRadiusToSigma 在 1.11/1.12 被标注 @InternalComposeUiApi(上游按 1.8.2 编写时还是普通 public)。处置:源码不动,kotlin.compilerOptions.optIn 加 androidx.compose.ui.InternalComposeUiApi——上游语义原样,opt-in 随构建文件走。

第 4 阶段:无头门面(BubblesHeadless)

4.1 分层
io.github.kalist28.bubbles/**(上游 commonMain 原样编译)
        │ ColorScheme / Typography / Shapes / BubblesColors / bubblesTween
BubblesHeadless.ohos.kt(门面,UI-free)
        │ 快照 + 动画引擎
Bridge.kt(@CName × 2,扁平 JSON 进出)
        ↓ UTF-8 JSON
N-API bubblesDispatch() / bubblesVersion()
        ↓
ArkUI Index.ets(五 Tab 滚动)

OHOS 无组合宿主,Composable 上不了屏;能跨过 C ABI 的是数据与计算。BubblesHeadless(eungabi EunGabiHeadless / compose-shimmer ShimmerHeadless 的同款门面模式)把库内 UI-free 的部分提升为公开面:

门面 API提升的上游能力说明
schemeSnapshot(dark)lightColorScheme() / darkColorScheme()19 个语义色 → ARGB;页面主题即真实 ColorScheme
paletteSnapshot(dark, highContrast)BubblesColors.system*(dark, highContrast)21 动态色 + 14 固定色;highContrast 直通上游无障碍分支(与 Accessibility.isHighContrastEnabled 同一开关)
typographySnapshot()Typography()11 档字阶:字号/行高(sp)+ 字重名
shapesSnapshot()Shapes()5 档圆角 dp;RoundedCornerShape(4.dp) 构造时以 dp 数值存 px,密度 1 下读回即声明值
rippleSnapshot()RippleIndication 默认值展开 450ms / 淡出 180ms / systemGray4.copy(.5f) 颜色(调参数化函数,绕开 @Composable getter)
tokenSnapshot()BubblesSwitchDefaults / BubblesButtonTokensSwitch 51×31dp、图标按钮 42dp、按钮按压/描边透明度
accessibleColor(argb, dark)Color.accessible(isDark)真实无障碍对比度调整
start/stop/progressAnimatable + bubblesTween(duration, easing = BubblesEasing)真实动画引擎,跑的正是每个控件动画用的那条 tween 工厂
4.2 动画引擎与帧时钟

Animatable.animateTo 需要 MonotonicFrameClock(Android 上由 Recomposer 提供,测试里由 runTest 提供),无头侧用墙钟驱动(8ms/帧、TimeSource.Monotonic 时间戳);协程上下文带 CoroutineExceptionHandler,引擎异常经 progress().error 通道浮出、绝不杀进程。鸿蒙线 Dispatchers.Main 跨线程派发有已知问题(resaca 仓适配时记录过),门面跑在专用单线程 dispatcher(newSingleThreadContext("bubbles-engine"),失败回落 Dispatchers.Default),全程不碰 Main:

init {
    // The exact spec widgets get from bubblesTween(duration, easing =
    // BubblesEasing): instantiated through the real factory, not mirrored.
    val spec: AnimationSpec<Float> = bubblesTween(
        durationMillis = durationMillis,
        easing = if (easingName == "linear") LinearEasing else BubblesEasing,
    )
    scope.launch {
        animatedState.animateTo(targetValue = 1f, animationSpec = spec)
    }
}

Demo 提供 bubbles / linear 两档缓动对照——停止引擎小球冻结、重启从 0 开始,每一帧都可归因到 Kotlin 侧的 Animatable。

第 5 阶段:原生桥接

5.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libbubbles_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohosbubbles.so)
  │ BubblesDispatch / BubblesVersion
  ▼
UTF-8 JSON + DisposeString
5.2 Kotlin/Native 导出函数
@CName("BubblesVersion")
fun bubblesVersion(): String = "0.0.1-ohos.1"     // 版本握手

@CName("BubblesDispatch")
fun bubblesDispatch(request: String): String      // 10 个 op 一口进
C ABI 符号ArkTS 方法覆盖内核
BubblesDispatchbubblesDispatch(req)theme / palette / typography / shapes / ripple / tokens / contrast / anim.start / anim.progress / anim.stop
BubblesVersionbubblesVersion()fork 版本握手(真机版本行的数据来源)

JSON 读写用手写 MiniJson(cmp_qrose 传承,协议只传扁平字段);浮点序列化用定点 %.4f(KN 无 String.format,Float.toString() 可能进科学计数法——wavy-slider 同款处理)。每个入口整体 try/catch,Kotlin 异常永远不跨 FFI。

5.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"bubblesDispatch", nullptr, NapiBubblesDispatch, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"bubblesVersion", nullptr, NapiBubblesVersion, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosbubbles_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosbubbles_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。

5.4 CMake 和类型声明
add_library(ohosbubbles SHARED IMPORTED)
set_target_properties(ohosbubbles PROPERTIES
  IMPORTED_LOCATION ${KN_SO}
  IMPORTED_NO_SONAME TRUE)

add_library(bubbles_napi SHARED napi_init.cpp)
target_link_libraries(bubbles_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohosbubbles)

OHOS_ARCH 为空或 libohosbubbles.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。

export const bubblesDispatch: (request: string) => string;
export const bubblesVersion: () => string;

第 6 阶段:ArkUI 宿主

6.1 握手与数据装载
aboutToAppear(): void {
  this.version = bubblesVersion();          // 版本握手:来自真实 Native 调用
  this.loadTheme(false);                    // theme → 19 token
  this.loadTypography();                    // 11 档字阶
  this.loadShapes();                        // 5 档圆角
  this.loadTokens();                        // 控件几何 token
  this.loadRipple();                        // 涟漪默认时序
  this.loadPalette(false, false);           // 35 色调色板
}
6.2 页面交互

五个 Tab 自上而下:主题 Tab 是 19 token 色块网格 + Shapes 圆角卡 + Typography 列表,页面底色/卡片色/文字色即当前 scheme(页面即主题);调色板 Tab 深浅色与高对比度双开关,21 个动态色实时切变体;控件 Tab 画廊里 Switch 恰为 token 给出的 51×31vp、滑块 27vp、指示条 400ms bubbles 曲线滑动;动效 Tab 33ms 轮询 anim.progress 驱动进度条与小球,运行期采样绘出缓动曲线(BubblesEasing 实机采样 vs LinearEasing 对照);关于 Tab 展示版本握手行、ArkTS → NAPI → C ABI → Kotlin 引擎 → Compose 制品线 分层说明与仓库/社区链接。

6.3 错误边界

所有 JSON 解析防御式处理:ok:false 时错误原文进状态行与 hilog;Native 调用本身抛错(理论不该发生)同样被 try/catch 收进状态行。定时器在 aboutToDisappear 清理,页面无任何 ohpm 三方依赖。

第 7 阶段:示例与验证

7.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohosbubbles)+ copyKnSo*
│   └── src/ohosMain/kotlin/example/nativeapp/
│       ├── Bridge.kt                         @CName × 2 + 10 个 op 分发 + 错误通道
│       └── MiniJson.kt                       扁平 JSON 读取 + 定点浮点序列化
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.bubbles、versionName 0.0.1-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/libohosbubbles_api.h
        ├── src/main/cpp/types/libbubbles_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 验收页面(五 Tab)
7.2 构建、签名和安装
./gradlew :bubbles:compileKotlinJvm                                            # 交叉验证
./gradlew :bubbles:compileKotlinOhosArm64 :bubbles:compileKotlinOhosX64        # ohos KLIB
./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.bubbles -m entry

四、完整代码对照

4.1 整体架构

io.github.kalist28.bubbles/**(上游 commonMain 原样编译)
    │ ColorScheme / Typography / Shapes / BubblesColors / bubblesTween / BubblesEasing
bubbles/src/ohosMain(新增)
    │ BubblesHeadless 门面(真实 token + 真实动画引擎) + HazeNode scrim actual
Bridge.kt + MiniJson
    │ BubblesDispatch / BubblesVersion(扁平 JSON 进出)
libbubbles_napi.so
    │ bubblesDispatch / bubblesVersion
Index.ets
    │ JSON parse + @State 更新 + 逐帧采样
    ▼
ArkUI 真机页面(五 Tab 验收)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_bubbles、demo 模块移出
gradle/libs.versions.tomlKotlin 2.4.20-0.1.0-07 + 双 Compose 线(ohos 1.12.0-0.1.0-07 / jvm 1.11.0)钉版
build.gradle.kts(根)版本 0.0.1-ohos.1、全局禁用 Ohos*Test* 任务
bubbles/build.gradle.ktsjvm + ohosArm64 + ohosX64;nonIosMain/skikoMain 重接线;双线依赖;lifecycle 剥离;InternalComposeUiApi opt-in
bubbles/src/ohosMain/.../BubblesHeadless.ohos.kt无头门面:主题/调色板/字阶/形状/涟漪/token/无障碍/动画引擎
bubbles/src/ohosMain/.../effect/Haze.ohos.ktHazeNode scrim 降级 actual(上游 androidMain 同策略)
example/nativeApp/build.gradle.kts双 ABI sharedLib(baseName=ohosbubbles)+ copyKnSo* 回填 harmonyApp/libs
example/nativeApp/.../Bridge.kt2 个 C ABI、10 个 op 分发、错误通道
example/nativeApp/.../MiniJson.kt手写扁平 JSON 读取 + 定点浮点序列化
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 五 Tab 验收页面
README.OpenHarmony_CN.md鸿蒙适配主文档

4.3 关键 API 对照

层次API作用
引擎lightColorScheme() / darkColorScheme()19 token 语义色方案(页面主题数据源)
引擎BubblesColors.system*(dark, highContrast)iOS 系统色 + 无障碍高对比度变体
引擎Typography() / Shapes()11 档字阶 / 5 档圆角
引擎bubblesTween(duration, easing = BubblesEasing)控件过渡规范(400ms cubic-bezier 0.2833/0.99/0.31833/0.99)
引擎Color.accessible(isDark)无障碍对比度调整
门面BubblesHeadless.schemeSnapshot 等上表能力的 UI-free 快照
NativeBubblesDispatch / BubblesVersionC ABI JSON 导出
N-APIbubblesDispatch(req) / bubblesVersion()向 ArkTS 暴露能力
ArkTSloadTheme() / pollProgress()数据装载与逐帧采样

4.4 ArkTS 与 Kotlin 的边界

const req: ThemeRequest = { op: 'theme', dark: this.isDark };
const res = JSON.parse(bubblesDispatch(JSON.stringify(req))) as ThemeResponse;
// res.colors: 19 个真实 token(accent=#FF007AFF、label、systemFill…)
// 页面底色/卡片/文字立即按 token 重绘
fun schemeSnapshot(dark: Boolean): SchemeSnapshot {
    val scheme = if (dark) darkColorScheme() else lightColorScheme()
    val colors = listOf(
        "accent" to scheme.accent,
        "label" to scheme.label,
        // …全部 19 个 token
    ).map { (name, color) -> NamedColor(name, color.toArgb()) }
    return SchemeSnapshot(dark = scheme.isDark, colors = colors)
}

两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。

五、关键决策说明

决策 1:整库编译而不是提取「主题模型子集」

库的公开 API 全部是 Composable,但数据模型本身(ColorScheme/Typography/Shapes/BubblesColors)是纯 Kotlin 值对象,随 commonMain 一起编译毫无障碍。只提取模型会丢掉两样东西:控件几何 token(散在各 *Defaults 里)和动画包——而它们才是「控件库」的灵魂。整库编译 + 无头门面让 demo 的每个数值都能指着库里的真实对象。

决策 2:双制品线(ohos 社区线 + JVM 上游线)

ohos 线没有 *-desktop 子 artifact,JVM 交叉验证必须从 Maven Central 解析;两条线的版本主体同代(1.12.0-ohos / 1.11.0,上游 1.8.2 的下一代),API 面一致,源码不用分叉。这是 compose-shimmer / eungabi 已验证的「换线不换版」。

决策 3:HazeNode 沿用上游 Android 的 scrim 降级

上游自己就对不支持 RenderEffect 的 Android 画 scrim;ohos 无组合宿主 + 社区线无 skiko RuntimeEffect 面,scrim 是语义最诚实的降级。全功能模糊保留在 skikoMain(jvm/desktop)。

决策 4:lifecycle 剥离(幽灵依赖处置)

构建声明了、源码零引用、ohos 线无制品——三条件同时满足,剥离是唯一自洽处置。JVM 交叉验证同样不需要,行为面零影响。

决策 5:真实动画引擎进 demo 而不是 ArkTS 自演

动效 Tab 的小球、进度条、缓动曲线全部由 anim.progress 采样驱动:停止引擎小球冻结、重启从 0 开始——每一帧都可归因到 Kotlin 侧的 Animatable。这是「真编译」与「照着写」的分界线。

六、测试与验证

6.1 测试环境

本次验证使用 Windows x64、JDK 21、DevEco Studio 26.0(SDK 5.1.0(18)+)、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。

6.2 编译与链接验证

./gradlew :bubbles:compileKotlinJvm                                     # 上游源码在 Compose 1.11.0 线交叉验证
./gradlew :bubbles:compileKotlinOhosArm64 :bubbles:compileKotlinOhosX64 # ohos KLIB
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
任务结果
compileKotlinJvmBUILD SUCCESSFUL(上游 30 文件零改动)
compileKotlinOhosArm64 / OhosX64BUILD SUCCESSFUL
linkDebugSharedOhosArm64arm64-v8a/libohosbubbles.so(debug 未 strip 约 29.1MB)+ libc++_shared.so
linkDebugSharedOhosX64x86_64/libohosbubbles.so(debug 未 strip 约 27.8MB)+ libc++_shared.so
copyKnSoArm64 / copyKnSoX64三件套回填 example/harmonyApp/entry/libs/

体积说明:debugShared 未 strip、内含 Kotlin/Native 运行时 + Compose runtime/foundation/animation/ui 全线(对比 qrose 无 Compose 的 ~4.7MB);HAP 打包 strip 后显著缩小。

6.3 符号与 HAP 验证

llvm-nm -D(双 ABI): BubblesDispatch / BubblesVersion 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libbubbles_napi.so)
CompileArkTS: finished(五 Tab 页面 + 类型声明)
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

6.4 功能用例

  1. 启动后「关于」Tab 显示 bubblesVersion() → 0.0.1-ohos.1(来自真实 Native 调用,非静态文字);
  2. 「主题」Tab 切换深浅色,19 个 token 色块与页面底色/卡片色/文字色同步变化,数值来自真实 darkColorScheme() / lightColorScheme();
  3. 「调色板」Tab 开启深色与高对比度,21 个系统色逐个切换为对应变体(systemRed 深色 #FF6961 → 高对比度 #D70015),与上游源码逐值一致;
  4. Color.accessible(isDark=true) 对照卡显示向白色 lerp 20% 后的调整色;
  5. 「控件」Tab Switch 尺寸恰为 51×31vp(token 原值),按钮/分段控件/标签行交互正常,TabRow 指示条以 400ms bubbles 曲线滑动;
  6. 「动效」Tab 播放 400ms bubbles 缓动:小球平滑起停,采样曲线呈典型 ease-out 形状;点停止小球立即冻结;linear 对照明显不同;
  7. 涟漪卡片显示真实默认时序 450ms/180ms,按压闪现动画使用相同时序;
  8. 全程无崩溃、无 ANR,hilog 无 Native 异常。

6.5 验证结论

主题模型、iOS 系统调色板(含无障碍变体)、字阶、形状、控件几何 token、Color.accessible 对比度调整、真实 bubblesTween 动画引擎、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的"Bubbles 主题会话":版本握手、深浅色切换、高对比度调色板、token 驱动控件画廊与逐帧采样的缓动演示(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/bubbles-compose.git
cd bubbles-compose
export JAVA_HOME="/path/to/jdk-21"
./gradlew :bubbles:compileKotlinJvm                                            # 交叉验证
./gradlew :bubbles:compileKotlinOhosArm64 :bubbles:compileKotlinOhosX64        # ohos KLIB
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.bubbles -m entry
hdc shell hilog | grep Bubbles

八、遗留问题与改进方向

8.1 踩坑复盘

  1. settings.gradle.kts 里 eazytec 双仓必须排在最前,否则配置期直接 Unresolved reference: ohosArm64;
  2. ohos Compose 线不发布 *-desktop 子 artifact——Compose 依赖必须按目标线(ohosMain/jvmMain)分开声明;
  3. 上游 commonMain 的 lifecycle 声明是幽灵依赖(源码零 import):先 grep 源码 import 面再定依赖处置,不要照抄构建声明;
  4. Compose 1.8→1.11/1.12 的 API 收紧(BlurEffect.convertRadiusToSigma → @InternalComposeUiApi):源码不动,opt-in 进构建文件;
  5. BubblesColors.systemXxx 同名双面:@Composable 属性 getter 不能在非组合上下文调用,门面一律走参数化函数(systemGray4(false));systemGray7/8 无 highContrast 变体,签名逐个核对;
  6. 该 Compose 线没有 Color(Int) 工厂重载:ARGB int 用分量构造;
  7. CornerBasedShape.topStart 静态类型是 CornerSize,取 dp 需 cast 到 CornerRadius 再按密度 1 换算(RoundedCornerShape(4.dp) 构造时 dp 数值即存为 px,读回无损);
  8. Animatable.animateTo 无头驱动必须自备 MonotonicFrameClock(8ms 墙钟帧),并用 CoroutineExceptionHandler 兜底——引擎异常走 progress().error 通道,不杀进程;
  9. 鸿蒙线 Dispatchers.Main 跨线程派发有已知问题(resaca 记录):门面用专用单线程 dispatcher,全程不碰 Main;
  10. ArkTS 严格模式:Color 枚举与 hex 字符串不互通(Color.Transparent 传给 string 参数直接编译失败),页面侧颜色统一为字符串(#RRGGBB / #AARRGGBB);curves.cubicBezier 已弃用,用 cubicBezierCurve 同签名替换;
  11. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  12. build-profile.json5 的 signingConfigs 留空数组、不引用签名,勾选 DevEco 自动签名后由 IDE 回填,提交前用 git update-index --skip-worktree 保护;HAP 缺当前 ABI 的 so 报安装失败 9568347,双 ABI 每档三件套(NAPI so + KN so + libc++)缺一不可。

8.2 已知问题

  • OHOS 无组合宿主:Composable 本体编译进 so 但不上屏,由 ArkTS 按真实 token/动画数据渲染(社区 Compose ohos 宿主可用后可切换为直接托管);
  • HazeNode ohos 走 scrim 降级(全功能 RuntimeShader 模糊仅 jvm/desktop);
  • 触感/无障碍 actual 复用 non-iOS 桩(isHighContrastEnabled=false、LocalHapticFeedback),鸿蒙系统级无障碍开关未接(后续可在 ohosMain 读系统设置补 actual);
  • android / ios / js / wasmJs 目标与发布流程剥离(源文件/构建文件留盘),本 fork 不发 Maven Central;
  • 上游无 commonTest 源集,交叉验证暂为「编译通过」级;补测试后统一 JVM 跑。

8.3 未来优化方向

  • 跟进社区 Compose ohos 宿主,把 demo 从「token 驱动 ArkTS 渲染」升级为直接托管 Composable;
  • ohosMain 补系统级高对比度/深浅色/触感 actual;
  • 桥接补状态推送(N-API 线程安全回表),动画改订阅而非轮询;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建冒烟;
  • 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵。

九、总结

9.1 核心难点回顾

上游 bubbles(commonMain 全是 Composable)
    → 验证社区 Compose ohos 制品线(runtime/foundation/animation/ui 全有 ohosArm64/ohosX64)
    → 工具链对齐 Kotlin 2.4.20-0.1.0-07(Gradle 9.6.1)
    → 根构建替换(cmp_qrose 模式,lifecycle 幽灵依赖剥离)
    → nonIosMain/skikoMain 重接线 + HazeNode scrim actual + InternalComposeUiApi opt-in
    → 整库 ohos KLIB(上游源码零改动)
    → BubblesHeadless 无头门面(真实 token + 真实 Animatable/bubblesTween)
    → C ABI(@CName × 2)+ 扁平 JSON
    → C++ N-API(DisposeString 纪律)
    → ArkTS 五 Tab 真机验收(主题即页面 / token 驱动画廊 / 动画逐帧采样)

9.2 封装层次

  • 库层:上游原版主题模型、全部控件、动画包(commonMain 零改动);
  • 门面层:BubblesHeadless 把 token/无障碍/动画提升为 UI-free 面(ohosMain 新增);
  • Native 层:双 ABI 动态库,2 个 C ABI 输出 JSON;
  • N-API 层:参数检查、字符串转换、Native 内存释放;
  • ArkTS 层:页面状态、JSON 校验、分发、日志与错误渲染;
  • DevEco 层:CMake、HAP、签名、安装与真机运行。

9.3 三条经验

  1. UI 库的适配单位是「数据面」而不是「控件面」:Composable 上不了屏时,主题 token、几何常量、动画规范就是库的 API——把它们原样送过 FFI,页面自然「像」Bubbles,而且每个像素可归因;
  2. 制品线决定路线:先验证社区 ohos 线覆盖了源码的全部 import 面(bubbles 四件套齐全),「整库编译」才成立;同时摸清它的缺口(无 *-desktop、无 lifecycle),缺口即处置清单;
  3. 真实动画是最硬的验收证据:色值可以照抄、尺寸可以照量,但「停止引擎画面冻结、重启从 0 开始」只有真引擎在跑才做得到——demo 里必须留一个由 Kotlin 逐帧驱动的活物。

9.4 适配成果

当前适配已完成:

  • 上游 commonMain 全部 30 个源文件零改动的 ohosArm64 / ohosX64 Kotlin/Native 编译与双 ABI 动态库链接(含完整 Compose runtime/foundation/animation/ui 社区线 1.12.0-0.1.0-07);
  • 2 个 C ABI 符号(llvm-nm 验证全局 T)+ C++ N-API 包装层(libbubbles_napi.so);
  • BubblesHeadless 无头门面:19 token 主题 / 35 色(含无障碍变体)/ 11 档字阶 / 5 档圆角 / 控件几何 token / Color.accessible / 真实 bubblesTween 动画引擎;
  • DevEco Stage ArkUI 五 Tab 验收页面(bundleName org.terminator.ohos.bubbles)+ HAP 构建、签名、设备安装和真机验证(深色模式调色板实测截图);
  • JVM 交叉验证编译通过(Compose 1.11.0 线);
  • AtomGit 项目文档、OpenHarmony README(中英双份)、上游原版留档(README.upstream.md + *.upstream 构建文件)与本地效果图。

适配过程 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

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐