本文记录 CrossMessages 接入 OpenHarmony 的完整过程,覆盖三种短消息载体(Snackbar / Toast / Alert)的语义盘点、HarmonyOS Kotlin 工具链选型、消息引擎提取、ohosArm64 / ohosX64 目标编译、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 四 Tab 真机页面、HAP 构建与真机验收。

CrossMessages 是 Compose Multiplatform 的三载体短消息库:Snackbar、Toast、Alert 各有一套 Host 与 HostState,showSnackbar(...) / showAlert(...) 是挂起函数并返回用户的选择,三个 HostState 内部都用 Mutex + suspendCancellableCoroutine 做真排队,时长是 Short = 4000ms / Long = 10000ms / Indefinite、Toast 是 2000 / 3500ms、按住暂停、Alert 动作越界退化为关闭——这些是行为契约,不是样式偏好。

本次适配与本系列里「真编译整库」的路线不同:CrossMessages 的上游 Host 全部依赖 androidx.compose.runtime 与 Material 3,鸿蒙线上没有 Compose 制品,因此走社区已审的 cmp_qrose 引擎提取路线——把可测的消息语义搬进零依赖 commonMain,把 Compose UI 整体留档到 composeMain,再用 ArkUI 按引擎色板与度量重绘。引擎在真机上跑,样式由宿主画,语义一行不丢。

项目地址: AtomGit/oh-tpc/KMP-CrossMessages.git

上游仓库: berkayKirecci/KMP-CrossMessages

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

CrossMessages 解决的是「一个 App 里三种短消息」的问题,并且坚持用平台自己的外观:Android 上 Snackbar 走 Material 3 Compose、Toast 走 android.widget.Toast、Alert 走 Material 3 AlertDialog;iOS 上 Toast 是 UIKit 浮层、Alert 是 UIAlertController;Desktop / Web 全部走 Compose。KMP 项目把这三套能力放在 commonMain,四个平台共享同一份调用代码。

它真正值钱的不是「画一个黑条」,而是三件容易被忽略的事:

  1. 挂起式 API。showSnackbar(...) 返回 CrossSnackbarResult,调用方直接知道用户点了动作还是超时消失;showAlert(...) 返回 CrossAlertResult,拿到的是被点的那颗按钮的 index 与内容。没有回调、没有事件总线,协程挂起就是答案。
  2. 真正的排队。三个 HostState 内部都是 Mutex + suspendCancellableCoroutine,同一时刻只有一条消息在屏上,重复调用按顺序排队,不会互相顶掉。连续三次 show 的结果是「一条一条播」,不是「叠成一坨」。
  3. 可测的数值规则。Short = 4000ms / Long = 10000ms、Toast 2000 / 3500ms、按住暂停冻结剩余时间、Indefinite 永不超时、Alert 空动作自动补 OK、动作 index 越界退化为「关闭」。这些都是可断言的契约。

OpenHarmony 应用同样需要这三件能力,但上游构建(Kotlin 2.4.10 / Compose 1.11.1 / AGP 9.3.1)不会生成 OpenHarmony 变体,Host 本身也编不进去。

如果只把示例页面改成 ArkTS,页面可以显示一个绿色卡片,却无法证明「排队、暂停、动作回执」这些语义在鸿蒙设备上成立。适配需要同时验证消息引擎、Compose 类型剥离、双 ABI 动态库、C ABI、N-API、ArkUI 宿主和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.4.10 / Compose 1.11.1 / AGP 9.3.1;HarmonyOS Kotlin 线是 2.2.21-1.0.0,社区 CMP 线是 1.9.2,三方对不齐。
依赖矩阵上游 Host 依赖 androidx.compose.runtime(mutableStateOf、Mutex、suspendCancellableCoroutine)与 Material 3 Surface,鸿蒙线上没有这些制品。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
消息语义跨不了桥挂起返回值、lambda 动作回调、State/Flow 都是 Kotlin 侧概念,C ABI 只能同步一来一回。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:消息引擎的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。排队、计时、暂停、动作合成、色板全部由引擎负责,ArkTS 只负责调用 Native、解析 JSON 和把状态画出来。

1.2 库提供的能力

Demo 是一个四 Tab 的验收台,把桥接的全部入口都摆了出来:

  • Snackbar:消息输入、五套 style(Neutral / Success / Warning / Error / Info)预览、三种 duration(Short / Long / Indefinite)、两种 position(Top / Bottom)、动作开关与文案、关闭按钮开关、图标开关;Show / 连发三条 / 关闭 / 清空;
  • Toast:消息、两种时长、Show / 连发三条 / 关闭;底部黑色半透明气泡,2s 或 3.5s 自动消失;
  • Alert:标题、正文、三套动作预设(单 OK / 取消+删除 / 三动作)、dismissible 开关;模态对话框,返回值直接打到页面底部;
  • About:引擎版本握手、上游版本、metrics 全量数值表、浅色/深色切换(走 op=theme,色板重新解析)。

每次调用都返回一份完整状态,宿主不需要自己算:

字段作用
ok调用是否成功,失败时 error 带原文。
kind / op回显本次操作,便于日志对齐。
changed本次调用是否改变了状态(timeout 未到期时为 false)。
actionFiredSnackbar 动作是否真的触发(只有卡上有 actionLabel 才为 true)。
resultAlert 的结果:Dismissed 或 ActionPerformed(index, label, style)。
reasonqueued / not-due / not-dismissible / empty 等语义原因。
removed本次被移除的消息快照,便于宿主做退场动画。
state引擎的完整真相:current、pendingCount、paused、remainingMillis、colors、deadlineAt。

引擎侧还保留着宿主用不到但必须存在的语义:Mutex 式串行化、换卡重新计时、暂停冻结剩余、Alert 空动作合成、动作越界退化、深色色板重着色不停表。JVM 侧 11 项用例交叉验证,证明 ohos .so 里执行的逻辑没有漂移。

1.3 实现适配

维度要求
代码复用引擎语义从上游 HostState 逐条搬运,不重新发明;Compose UI 原样留档在 composeMain,一个字节不改。
平台目标根构建声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);sample/、sample-android/、iosApp/ 留盘不进构建。
桥接稳定只导出 1 个 C ABI 和 UTF-8 扁平 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律引擎零第三方依赖(无 coroutines、无序列化库、无 Compose),klib 与 so 里没有 cinterop 包袱。
UI 完整单页四 Tab 覆盖全部桥接入口:Snackbar / Toast / Alert / About。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。composeMain 是参考实现,ArkUI 是鸿蒙上的唯一实现;改视觉改 Index.ets,改语义改 commonMain。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点三载体语义、拆 Compose 与引擎、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、双 ohos 目标
第 3 阶段:桥协议建模     ── 扁平 JSON 契约、kind/op 分发、宿主时钟、错误通道
第 4 阶段:原生桥接       ── @CName 单入口 C ABI、C++ N-API、DisposeString 内存纪律
第 5 阶段:ArkUI 宿主     ── 四 Tab、引擎色板驱动重绘、PanGesture 滑关、按住暂停
第 6 阶段:示例与验证     ── 双 ABI so、HAP、签名、设备安装、真机效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
library/src/commonMain/               纯 Kotlin 消息引擎(进 ohos 构建,零依赖)
library/src/composeMain/              上游 Compose UI + composeResources(留档,不编译)
library/src/commonTest/               11 项 JVM 用例,钉住语义
example/nativeApp/                    Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/                   DevEco Stage、CMake N-API 和 ArkTS 四 Tab
README.OpenHarmony.md / _CN.md        鸿蒙适配说明(中英双份)
README.upstream.md                    上游原文保存
sample/ sample-android/ iosApp/       上游示例,留盘不进构建

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :library 与 :example:nativeApp,rootProject.name = "ohos_crossmessages"。上游的 AGP、Compose 插件、Maven Publish 配置全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDKTemurin 21(org.gradle.java.home 指向本机 JDK 21)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(上游 wrapper 9.7.0 降下来)
引擎依赖零(无 coroutines、无 serialization、无 Compose)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.crossmessages
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块 / 目录文件数依赖形态处置
library/src/commonMain(新写引擎)7纯 Kotlin,零第三方依赖整体编译(jvm + 双 ohos)
library/src/composeMain(上游 UI)10 个 .kt + 5 个 drawableandroidx.compose.* + Material 3留档,不参与任何编译
library/src/commonTest1只用 kotlin("test")JVM 跑,钉住语义
sample/ / sample-android/ / iosApp/—AGP 9 + Compose 1.11 + Android SDK留盘不进 settings.gradle.kts

上游 commonMain 里的 10 个 Kotlin 文件全部 import 了 androidx.compose.*,没有一个能直接编进 ohos。处理办法分两步:

第一步,搬。 整个 library/src/commonMain/kotlin 与 library/src/commonMain/composeResources 平移到 library/src/composeMain/。composeMain 不是 Kotlin 源集,Gradle 完全不认它——这正是「留档但不编译」。

第二步,抽。 在空的 commonMain 里重写一份引擎,只保留语义:

上游文件(composeMain)引擎文件(commonMain)抽走了什么
snackbar/CrossSnackbarHostState.ktsnackbar/CrossSnackbarQueue.ktMutex 排队、current/pending、暂停、动作语义
snackbar/CrossSnackbarVisuals.ktsnackbar/CrossSnackbar.kt数据模型:TextAlign → 枚举、DrawableResource → Named(name)、Color → ARGB
snackbar/CrossSnackbarDefaults.ktcore/CrossMessagePalette.kt + core/CrossMessageMetrics.ktM3 baseline 色板 + 全套 dp/ms 度量
toast/CrossToastHostState.kt + CrossToastHost.kttoast/CrossToast.kt2000/3500ms、排队、CrossToastDuration
alert/CrossAlertHostState.kt + CrossAlertHost.ktalert/CrossAlert.kt动作合成、index 回传、dismissible
core/SwipeToDismiss.kt不抽手势留在 ArkTS 的 PanGesture
CrossSnackbarHost / CrossAlertHost / CrossToastHost 的 Composable不抽ArkUI 重绘

抽的时候有一刀必须手洗:上游把类型定义和 UI 文件绑在一起。CrossSnackbarColors 与 LocalCrossSnackbarColors 在 CrossSnackbarDefaults.kt(Compose Color),CrossToastDuration 在 CrossToastHostState.kt(纯 Kotlin,但同文件的 CrossToastData 用了 @Stable)。拆完的结果是:枚举与数据类进 commonMain,Compose 版本原样留在 composeMain。

对外入口还补了一个不依赖 Compose 的 CrossMessages 门面:CrossMessages.VERSION、UPSTREAM_VERSION 与六个枚举解析函数。上游只有 @Composable rememberCrossSnackbarHostState(),Kotlin/Native 消费者看不见 Composable,桥必须有个普通对象可调——和 qrose 补 Qrose.encodeQr 是同一个动作。

1.4 页面边界
标题区          CROSSMESSAGES · OPENHARMONY / 版本握手行(引擎 2.0.0-ohos.1 · 上游 2.0.0)
Tab 切换        Snackbar / Toast / Alert / About
Snackbar 区     时长 chips、位置 chips、动作开关 + 文案、关闭按钮开关、图标开关、
                Show / Queue 3 / Dismiss / Clear、实时卡片、剩余时间进度条、pendingCount
Toast 区        消息、时长 chips、Show / Queue 3 / Dismiss、底部气泡
Alert 区        标题/正文输入、三套动作预设、dismissible 开关、模态对话框、result 行
About 区        版本握手、metrics 全量数值、深色模式切换、仓库与社区链接

页面启动时先调 op=version 做握手,再调 op=metrics 把全部 dp/ms 数值取回——尺寸不靠猜。

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

2.1 三个目标与一个 sharedLib

library/build.gradle.kts 只声明三个目标,不开 binaries.sharedLib:

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)

    jvm()
    ohosArm64()
    ohosX64()

    sourceSets {
        // 零第三方依赖:引擎要被 Kotlin/Native sharedLib 直接消费
        commonMain.dependencies {}

        commonTest.dependencies {
            implementation(kotlin("test"))
        }
    }
}

sharedLib 只放 example/nativeApp,否则每个模块都会吐一份带 Kotlin/Native 运行时的 so,HAP 体积和符号都会乱(qrose 的 README 里也记了这条)。

2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)

ohosArm64() / ohosX64() 只存在于 HarmonyOS Kotlin 插件。settings.gradle.kts 必须先写 eazytec Maven,再写 gradlePluginPortal():

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_crossmessages"

include(":library")
include(":example:nativeApp")

顺序错了会解析到官方 Kotlin 插件,配置期直接 Unresolved reference: ohosArm64。

上游用 Kotlin 2.4.10 + Compose 1.11.1 + AGP 9.3.1,这套在鸿蒙线上不存在(AGP 9 甚至没有 ohos 概念)。但 CrossMessages 的处境比依赖重的库宽松:引擎零依赖,没有 kotlinx-coroutines 的 stdlib 元数据把人往回拽,工具链只需要满足「能编 ohos」,不需要「跟某个 ohos 制品线对齐」。因此直接钉社区已审的 2.2.21-1.0.0,Gradle Wrapper 同步降到 8.14.1(2.2.21 线只验证到 8.14.x;上游 9.7.0 的 wrapper 跑不动)。

版本目录也从「AGP + Compose + Maven Publish 全家桶」缩到只剩 Kotlin 一行:

[versions]
kotlin = "2.2.21-1.0.0"

[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }

这不是洁癖:libs.versions.toml 里留着 com.android.kotlin.multiplatform.library,Gradle 在配置期就会去 google() 解析 AGP,而 AGP 会要求 Android SDK,配置直接失败。

2.3 编译引擎
./gradlew :library:compileKotlinOhosArm64 :library:compileKotlinOhosX64
./gradlew :library:jvmTest

实测:

> Task :library:compileKotlinOhosArm64
> Task :library:compileKotlinOhosX64
BUILD SUCCESSFUL

> Task :library:jvmTest
11 tests completed, 0 failed

引擎 7 个文件一行没改就同时通过了 JVM 与两个 ohos 目标——零依赖的直接回报。

2.4 测试任务处置

HarmonyOS Kotlin 插件会为每个 ohos 目标生成 ohos*Test 任务,这些任务需要真机或真机。测试统一在 JVM 跑,根构建全局禁用:

subprojects {
    tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
        .configureEach { enabled = false }
}

这样 commonTest 一个字节不用动,ohos 侧只承担编译与链接。

第 3 阶段:桥协议建模

3.1 JSON 契约
io.github.berkaykirecci.crossmessages/**(引擎,原样编译)
        ↓ 三条队列 / 计时 / 暂停 / 动作 / 色板
Bridge.kt(@CName × 1,扁平 JSON 进出)
        ↓ UTF-8 JSON
N-API crossMessagesCommand()
        ↓
ArkUI Index.ets(四 Tab)

请求与响应:

请求 : {"kind":"snackbar","op":"show","message":"已保存到你的收藏","actionLabel":"撤销",
        "withDismissAction":true,"duration":"Short","position":"Bottom","style":"Success",
        "icon":"FromStyle","textAlign":"Start","now":1761000000000}
       {"kind":"snackbar","op":"timeout","now":1761000000400}
       {"kind":"alert","op":"action","index":1,"now":1761000000000}
响应 : {"ok":true,"kind":"snackbar","op":"show","changed":true,"actionFired":false,
        "reason":null,"removed":null,"state":{...}}
       {"ok":false,"kind":"alert","op":"action","error":"unknown op"}

公共字段只有三个:kind(snackbar / toast / alert,缺省 snackbar)、op(缺省 state)、now(宿主时钟)。其余字段按 kind 与 op 平铺。

Alert 的多动作打平成 action0Label / action0Style … action4Style,MAX_ACTIONS = 5 是显式约束。

3.2 为什么扁平 JSON + 宿主时钟

三个理由:

  1. 没有序列化依赖。ohos 线上不引 kotlinx-serialization,klib 体积与编译时间都省;MiniJson 用字符串状态机跳引号内逗号,标题里有逗号、冒号也不会把对象切坏。qrose 与 compose-stacked-snackbar 都是同一套写法。
  2. 协议可读。抓一次 hilog 就能看出宿主传了什么、引擎回了什么,联调不用猜二进制布局。
  3. C ABI 只有字符串。Kotlin/Native 的 struct 跨语言映射随版本变,const char* 不会。

now 由宿主传是本适配最关键的一个设计:引擎里所有计时判定都是 present(visuals, now) / dismiss(now) 这样的纯函数,JVM 上可以逐毫秒断言「4000ms 关、3999ms 不关」,不需要 delay、不需要真机、不需要挂钟。

3.3 错误通道

导出函数整体 try/catch,Kotlin 异常永远不跨 FFI:

@CName("CrossMessagesCommand")
public fun crossMessagesCommand(request: String): String {
    return try {
        val json = MiniJson.parse(request)
        val kind = json.str("kind").ifBlank { "snackbar" }.lowercase()
        val op = json.str("op").ifBlank { "state" }.lowercase()
        val now = json.long("now", 0L)
        // ……按 kind / op 分发
    } catch (t: Throwable) {
        """{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
    }
}

非法 JSON、缺字段、未知 op、动作越界,全部走同一个 ok:false 通道,ArkTS 侧统一渲染成状态行与日志。

第 4 阶段:原生桥接(技术难点)

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libcrossmessages_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohoscrossmessages.so)
  │ CrossMessagesCommand
  ▼
UTF-8 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制消息语义,所以选择 C ABI + JSON,且只开一个入口——kind 与 op 在 Kotlin 侧分发,比给三种消息各开一个符号更容易扩展。

4.2 Kotlin/Native 导出函数
@CName("CrossMessagesCommand")
public fun crossMessagesCommand(request: String): String   // snackbar / toast / alert 一口进
C ABI 符号ArkTS 方法覆盖内核
CrossMessagesCommandcrossMessagesCommand(req)三条队列的 show / action / dismiss / timeout / pause / resume / state / reset,以及 version / metrics / theme

单符号的代价是 kind 必须显式传,收益是 N-API 层只有一份参数校验、一份内存释放、一份日志。

4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"crossMessagesCommand", nullptr, NapiCrossMessagesCommand, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr}};

回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打请求与响应的字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohoscrossmessages_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。

libohoscrossmessages_api.h 按社区已审仓库同款手写:只声明 ExportedSymbols 结构、DisposeString、libohoscrossmessages_symbols() 与业务符号,不拖整份生成头。

4.4 CMake 和类型声明
if(NOT DEFINED OHOS_ARCH OR OHOS_ARCH STREQUAL "")
    message(FATAL_ERROR "OHOS_ARCH is empty; expected arm64-v8a or x86_64")
endif()

set(KN_SO ${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/${OHOS_ARCH}/libohoscrossmessages.so)
if(NOT EXISTS ${KN_SO})
    message(FATAL_ERROR "Missing ${KN_SO}. Run linkDebugSharedOhos* first.")
endif()

add_library(ohoscrossmessages SHARED IMPORTED)
set_target_properties(ohoscrossmessages PROPERTIES
    IMPORTED_LOCATION ${KN_SO}
    IMPORTED_NO_SONAME TRUE)

add_library(crossmessages_napi SHARED napi_init.cpp)
target_link_options(crossmessages_napi PRIVATE
    "-Wl,--no-undefined"
    "-Wl,-rpath,\$ORIGIN")
set_target_properties(crossmessages_napi PROPERTIES
    BUILD_WITH_INSTALL_RPATH TRUE
    INSTALL_RPATH "$ORIGIN")

target_link_libraries(crossmessages_napi PUBLIC
    libace_napi.z.so
    libhilog_ndk.z.so
    ohoscrossmessages)

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

export const crossMessagesCommand: (input: string) => string;

第 5 阶段:ArkUI 宿主

5.1 启动握手与请求分发
aboutToAppear(): void {
  this.loadVersion();     // op=version → 引擎 2.0.0-ohos.1 / 上游 2.0.0
  this.loadMetrics();     // op=metrics → 全部 dp/ms 数值
  this.applyTheme(false); // op=theme  → 色板
}

private call(req: Record<string, Object>): string | null {
  try {
    const json: string = JSON.stringify(req);
    hilog.info(0x0000, TAG, 'engine -> %{public}s', json);
    const raw: string = crossMessagesCommand(json);
    hilog.info(0x0000, TAG, 'engine <- %{public}s', raw);
    return raw;
  } catch (err) {
    this.fail((err as Error).message);
    return null;
  }
}

三种消息各有一层 snackbarCall / toastCall / alertCall,负责 JSON.parse、ok !== true 转错误行、把 state 灌进 @State。页面自己不做任何计时判断,只维持一个 250ms 的 setInterval,在有卡在屏上时调 op=timeout,没卡就停表。

5.2 页面交互

Snackbar:顶部是时长、位置、动作/关闭/图标三组开关,中间是 Show / Queue 3 / Dismiss / Clear 四个按钮,底部是实时卡片。卡片从引擎给的 colors.container / content 取色,350ms 滑入,左上角 24vp 图标按 style 变化(✓ / ! / i / ✕),消息最多两行省略,右侧是动作文字按钮与关闭 ✕。PanGesture 横向滑动超过阈值调 dismiss,LongPress 调 pause、松手 resume,进度条宽度取 remainingMillis / durationMs。队列里还有卡时,右上角显示 pendingCount。

Toast:底部黑色 75% 气泡,圆角 12vp,白字 14sp,水平 20vp 边距、底部 50vp,250ms 淡入淡出——参数全部来自引擎(metrics.toast.native*),与上游 iOS 的 UIKit 方案一致。

Alert:居中模态,surface 色、28vp 圆角、宽度 min(88%, 320vp)、内边距 24vp,标题 24sp、正文 14sp,动作右对齐;Destructive 用错误红,Cancel 与 Default 分开渲染。点外部或返回键调 dismiss(force=false),dismissible=false 时引擎回 not-dismissible,页面给一行提示。

About:版本握手、上游版本、metrics 全量数值表、浅色/深色切换。切深色只重算色板,不重启计时——正在看的提示不会因为切主题被提前关掉。

5.3 颜色、尺寸和手势的归属
归属内容理由
引擎容器色 / 文字色 / 图标色 / 动作色、全部 dp/ms、时长、剩余时间这些是上游语义的一部分,必须跨平台一致
ArkTS布局、字体、圆角、阴影、手势、动画曲线这些是宿主平台的表现层,ArkUI 有更合适的 API
引擎dismiss / pause / resume / action 的判定手势只产生事件,语义仍由引擎裁决

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohoscrossmessages)+ copyKnSo*
│   └── src/ohosMain/kotlin/Bridge.kt         @CName × 1 + MiniJson(扁平 JSON,继承 cmp_qrose 审核模式)
└── harmonyApp/
    ├── build-profile.json5                   signingConfig "default"(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.crossmessages
    └── 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/libohoscrossmessages_api.h
        ├── src/main/cpp/types/libcrossmessages_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 四 Tab 验收页面(约 2000 行)
6.2 构建、签名和安装
./gradlew :library:jvmTest                                        # 11 项交叉验证
./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 只保留 "signingConfig": "default" 占位,勾选自动签名后由 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.crossmessages -m entry

四、完整代码对照

4.1 整体架构

library/src/commonMain/**(纯 Kotlin 引擎)
    │ CrossSnackbarQueue / CrossToastQueue / CrossAlertQueue
    │ CrossMessagePalette(M3 baseline 明暗)+ CrossMessageMetrics(dp/ms)
library/src/composeMain/**(上游 Compose UI,留档不编译)
    │ CrossSnackbarHost / CrossToastHost / CrossAlertHost(参考实现)
Bridge.kt + MiniJson
    │ CrossMessagesCommand
libcrossmessages_napi.so
    │ crossMessagesCommand
Index.ets
    │ JSON parse + @State 更新
    ▼
ArkUI 真机页面(四 Tab)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_crossmessages、sample 移出
gradle/libs.versions.tomlKotlin 2.2.21-1.0.0 单行目录
build.gradle.kts(根)版本 2.0.0-ohos.1、全局禁用 Ohos*Test* 任务
gradle.propertiesJDK 21、version=2.0.0-ohos.1、ignoreDisabledTargets
library/build.gradle.kts纯 KMP:jvm() + ohosArm64() + ohosX64(),零依赖
library/src/commonMain/.../CrossMessages.kt门面:VERSION、UPSTREAM_VERSION + 枚举解析
library/src/commonMain/.../snackbar/CrossSnackbarQueue.kt排队 / 计时 / 暂停 / 动作
library/src/commonMain/.../toast/CrossToast.ktToast 队列 / 2000-3500ms
library/src/commonMain/.../alert/CrossAlert.ktAlert 队列 / 动作合成 / index 回传
library/src/commonMain/.../core/CrossMessagePalette.ktM3 baseline 明暗色板 + 语义槽解析
library/src/commonMain/.../core/CrossMessageMetrics.kt全部 dp/ms 常量(与 Compose 逐条对应)
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt1 个 C ABI、MiniJson、错误通道
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鸿蒙适配主文档(17 节)

4.3 关键 API 对照

层次API作用
引擎CrossSnackbarQueue.show(visuals, now)空闲立即上屏,占用则入队并回 queued
引擎CrossSnackbarQueue.timeout(now)到期关闭并晋升下一条,未到期回 not-due
引擎CrossSnackbarQueue.pause/resume(now)冻结 / 恢复剩余时间
引擎CrossSnackbarQueue.performAction(now)有标签才 actionFired=true
引擎CrossToastQueue.show(visuals, now)2000 / 3500ms 队列
引擎CrossAlertQueue.performAction(index, now)越界退化为 Dismissed
引擎CrossMessagePalette.colors(style)五套语义色,明暗各一套
引擎CrossMessageMetricselevation / margin / padding / 350ms / 4000ms …
NativeCrossMessagesCommandC ABI JSON 单入口
N-APIcrossMessagesCommand(req)向 ArkTS 暴露能力
ArkTSsnackbarCall(req) / toastCall / alertCall触发操作并刷新 UI

4.4 ArkTS 与 Kotlin 的边界

const res = JSON.parse(crossMessagesCommand(JSON.stringify({
  kind: 'snackbar', op: 'show', now: Date.now(),
  message: '已保存到你的收藏', actionLabel: '撤销',
  withDismissAction: true, duration: 'Short',
  position: 'Bottom', style: 'Success'
})));
// res.state.current.colors.container === '#FFBEF2C4'
// res.state.pendingCount === 0
val outcome = queue.show(
    CrossSnackbarVisuals(
        message = json.str("message"),
        actionLabel = json.strOrNull("actionLabel"),
        duration = CrossMessages.snackbarDuration(json.str("duration")),
        style = CrossMessages.snackbarStyle(json.str("style")),
        position = CrossMessages.snackbarPosition(json.str("position")),
        // ……
    ),
    now = json.long("now"),
)

两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。动作的 lambda 留在宿主:桥只回 index / label / style,ArkTS 自己执行对应逻辑。

五、关键决策说明

决策 1:提取引擎,而不是把 Compose Host 整库编进 ohos

「真编译整库」那条路能走通,前提是依赖(coroutines / serialization / kotlinx-io 之类)在社区 Maven 上都有 ohos 制品。CrossMessages 的 Host 依赖 androidx.compose.runtime 与 Material 3,鸿蒙线上没有 Compose 制品;社区 CMP 线停在 1.9.2,上游是 Compose 1.11.1 + Kotlin 2.4.10,强行对齐等于再打一套 Compose 发行版。

所以选择社区已审的引擎提取路线:把 Mutex 排队、计时、暂停、动作合成、色板、度量搬进零依赖 commonMain,Compose UI 原样留档。代价是 ArkUI 要重画一遍;收益是语义可测、构建可复现、klib 目录不到 300 KB(不含 Kotlin/Native 运行时,运行时只在最终 so 里出现一次)。

决策 2:工具链钉 2.2.21-1.0.0,Gradle 降 8.14.1

ohosArm64() 只在这套插件里;2.2.21 线只验证到 Gradle 8.14.x,所以 wrapper 从上游 9.7.0 降到 8.14.1,gradle-wrapper.jar / .bat / sh 一起换。引擎零依赖意味着不用被 kotlinx 制品线的 stdlib 元数据反推版本——这是「库的依赖越少,工具链自由度越大」的直接体现。

决策 3:根构建替换(cmp_qrose 模式),sample 留盘

上游根构建(AGP classpath、Compose 插件、Maven Publish、kover/dokka)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。sample/、sample-android/、iosApp/ 需要 AGP 9 + Compose 1.11 + Android SDK,留盘但绝不进 settings.gradle.kts——用它们在鸿蒙上 sync 只会得到一屏 AGP 报错。

决策 4:宿主时钟 + 纯函数计时 + 显式快照

引擎不读系统时钟,所有判定都吃 now。好处有三:JVM 上逐毫秒断言、不需要 delay、真机上不会因为主线程卡顿把 4s 判成 4.2s。状态用普通可变量 + snapshot() 输出纯数据,而不是 mutableStateOf——后者是 Compose 类型,Kotlin/Native 消费者根本看不见。

决策 5:色板与度量常量化,ArkUI 只做重绘

鸿蒙没有 MaterialTheme,引擎钉 M3 baseline 色板(inverseSurface / errorContainer / success / warning / info 明暗各一套),输出 #AARRGGBB;度量把 Composable 里的数字搬成常量,通过 op=metrics 一次性交给 ArkTS。这样「换深色模式卡片变色但不停表」「卡片 350ms 滑入」「Toast 2s 消失」都能在引擎侧被断言,而不是靠页面里的魔法数字。

六、测试与验证

6.1 测试环境

项值
适配机Windows 11 x64
JDKTemurin 21.0.12
Gradle8.14.1(wrapper)
Kotlin/NativeHarmonyOS Kotlin 2.2.21-1.0.0
HAP 构建 / 签名DevEco Studio 5.1.0 Release(5.1.0(18) 兼容),由维护者手动完成
真机HarmonyOS arm64 设备

6.2 JVM 交叉验证

./gradlew :library:jvmTest

11 项用例全部通过:

用例钉住什么
showsImmediatelyWhenIdle空闲时首条立即上屏
secondMessageQueuesUntilFirstLeaves第二条入队,第一条离开后晋升
shortDurationDismissesAtFourSecondsShort = 4000ms,3999ms 不关
longDurationDismissesAtTenSecondsLong = 10000ms
indefiniteNeverTimesOutIndefinite 永不超时
promotedCardRestartsItsClock换卡重新锚定 shownAt,不是接着上一条计时
actionOnlyFiresWhenLabelPresent无 actionLabel 时 actionFired = false
pauseFreezesRemainingTime1000ms 处暂停,9000ms 时剩余仍是 3000ms
styleSelectsTheSamePaletteAsComposeNeutral / Error 容器色与 Compose 一致
darkPaletteRecolorsTheLiveCard切深色重着色,但不改 deadlineAt
clearDropsQueuedCardsTooreset 同时清空当前与排队

引擎与 ohos .so 跑的是同一份 commonMain,JVM 全绿是「内核没漂」的闸门。

6.3 双 ABI 与符号验证

./gradlew :example:nativeApp:linkDebugSharedOhosArm64
./gradlew :example:nativeApp:linkDebugSharedOhosX64
产物位置大小
libohoscrossmessages.so (arm64)entry/libs/arm64-v8a/4,152,168 B(≈3.96 MiB)
libohoscrossmessages.so (x86_64)entry/libs/x86_64/3,858,224 B(≈3.68 MiB)
libc++_shared.so ×2各 ABI 目录随 SDK,与 KN so 同 triple

符号检查(llvm-nm -D --defined-only):

00000000001c1394 T CrossMessagesCommand     # arm64-v8a
00000000001b1140 T CrossMessagesCommand     # x86_64

两个 ABI 都是全局 T 符号,NAPI 层可以稳定 dlsym。

链接期会有一条来自 HarmonyOS Kotlin 生成胶水的 clang 警告(non-void function does not return a value in all control paths),不是业务代码,so 仍能正常产出。

6.4 HAP 与真机

在 DevEco Studio 中打开 example/harmonyApp(不是仓库根目录),Build > Build Hap(s)。验收点:

  • entry/libs/<abi>/ 里两个 so 都在(CMake 会检查,缺了直接 FATAL_ERROR 并提示跑哪个 Gradle 任务);
  • libcrossmessages_napi.so、libohoscrossmessages.so、libc++_shared.so 在同一个 libs/<abi> 输出目录;
  • entry/build/default/outputs/default/entry-default-*.hap 生成。

签名走 File > Project Structure > Signing Configs > Automatically generate signature,仓库不带证书。安装:

hdc -t <设备序列号> install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start -a EntryAbility -b org.terminator.ohos.crossmessages -m entry
hdc shell hilog | grep CrossMsg

6.5 功能用例

  1. 启动后 About 页显示引擎 2.0.0-ohos.1 / 上游 2.0.0(来自真实 Native 调用,非静态文字);
  2. Snackbar 页点 Show(Success / Short / Bottom):绿色卡片从底部滑入,350ms 到位,4s 后自动消失;
  3. 连续点 Queue 3:一次只显示一条,右上 pendingCount = 2 → 1 → 0,逐条播放不重叠;
  4. 切 Indefinite 再 Show:不自动消失,点 ✕ 才走;
  5. 按住卡片 2s 后松手:进度条停住再继续,总停留时间明显变长;
  6. 左右滑动卡片超过阈值:卡片滑出并关闭,底部响应出现 dismiss;
  7. 打开 Action 开关、填「撤销」再 Show,点动作:卡片关闭,响应 actionFired: true;
  8. 切 Top:卡片贴顶滑入,不遮挡状态栏内容;
  9. 五套 style 逐个点:容器/文字/图标颜色按色板表变化,深色模式下取暗色列;
  10. Toast 页 Show:底部黑色半透明气泡,2s 消失;切 Long 约 3.5s;
  11. Alert 页「取消+删除」预设 Show → 点删除:对话框关闭,响应为 ActionPerformed index:1 style:Destructive;
  12. 关闭 dismissible 后 Show,点对话框外部:对话框不关,页面提示 not-dismissible;
  13. About 页切深色:三个页面底色与卡片色同步切换,正在显示的卡片不被关闭。

6.6 验证结论

消息引擎、计时语义、色板与度量、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上安装并进入前台,页面展示了真实的 Snackbar 会话:绿色 Success 卡片、撤销动作、关闭按钮、底部进度条(见下节效果图)。11 项 JVM 用例与真机行为一致。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/KMP-CrossMessages.git
cd KMP-CrossMessages
export JAVA_HOME="/path/to/jdk-21"
./gradlew :library:jvmTest                                             # 11 项交叉验证
./gradlew :library:compileKotlinOhosArm64 :library:compileKotlinOhosX64 # 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.crossmessages -m entry
hdc shell hilog | grep CrossMsg

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :library 真正编译成 ohosArm64 / ohosX64 KLIB,并让 :example:nativeApp 链接出双 ABI 的 .so;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. 上游 libs.versions.toml 里的 AGP / Compose 别名必须在配置期之前删掉,留着就会去 google() 解析 AGP,而 AGP 会要 Android SDK;
  4. Gradle 9.7 wrapper 跑不动 HarmonyOS Kotlin 2.2.21,必须整体降到 8.14.1(jar / bat / sh 一起换,只改 properties 不够);
  5. CrossSnackbarDuration 的枚举项叫 Long,会把 kotlin.Long 遮住,写 Long.MAX_VALUE 编译不过,必须写 kotlin.Long.MAX_VALUE;
  6. 上游把 Resource 当图标类型,引擎改成 data class Named,when (icon) 立刻报不穷尽,要补 is Named -> 分支;
  7. 色板解析第一版写在 companion object 里,看不见实例属性(dark 标记),改成实例方法 palette.colors(style) 才通;
  8. 引擎第一版「上一条一消失,第二条立刻上屏」,测试却断言「第二条是 current」失败——上游 Mutex 是排队语义,第二条在第一条结束前只能 queued;改测试而不是改引擎;
  9. ArkTS import 'libohoscrossmessages.so' 只能看到 stdlib 的 zip 类型,@CName 业务符号不会挂到 ArkTS 模块上,必须加 CMake NAPI 包装层;
  10. 运行期 dlopen 失败,多数是只拷了 KN so、没拷 libc++_shared.so;copyKnSo* 任务必须成对拷,CMake POST_BUILD 再拷一次到 NAPI 同目录;
  11. 真机安装报 9568347,是只带了 arm64;abiFilters 双写,两个 ABI 都出 so;
  12. NAPI 拿到的字符串以后内存泄漏,是因为 KN 返回的 const char* 在 Kotlin 堆上,必须 DisposeString(raw),不能 free();
  13. 消息里带逗号(「已保存到你的收藏,去看看吧」)时解析断在中间,是朴素 split(",") 的锅;MiniJson 用字符串状态机跳引号内分隔符;
  14. 根 .gitignore 的 /build/ 是根锚定模式,盖不住嵌套目录,example/nativeApp/build/ 与 example/harmonyApp/.idea/ 要单独追加规则,否则混进提交。

8.2 已知问题

  • 鸿蒙上不跑 Compose。composeMain 只是参考实现,ArkUI 负责重绘。改视觉改 Index.ets,改语义改 commonMain 引擎。
  • 无障碍超时是近似。上游在 Android 上调用 AccessibilityManager.calculateRecommendedTimeoutMillis,鸿蒙侧只保留一个 accessibilityScale 乘数(默认 1.0),宿主可按系统字体缩放自行传值。
  • Toast 不是系统 Toast。HarmonyOS 没有面向三方应用的系统 Toast API,Demo 复刻的是上游 iOS 的 UIKit 方案(黑色 75%、圆角 12、250ms 淡入淡出)。它在应用窗口内绘制,退到后台不会跟随。
  • 动作回调留在宿主。Lambda 过不了 C ABI,桥只回 index / label / style。
  • Alert 最多 5 个动作。协议用 action0…action4 打平,MAX_ACTIONS = 5 是硬约束;上游 Compose 侧没有这个限制。
  • Snackbar 文本最多 2 行,与上游 maxLines = 2 一致,超长文案建议改用 Alert。
  • 颜色只支持 ARGB。ArkUI 吃 #AARRGGBB,引擎不处理渐变与主题衍生色(tonal elevation 叠加由宿主自行近似)。
  • 本 fork 不发布 Maven。ohos 构建只产出 KLIB 与 so;上游的 vanniktech 发布配置随根构建一起剥离。

8.3 未来优化方向

  • 增加 updates 式状态推送(N-API 线程安全回表),替代「操作后读回」;
  • 把 metrics 里的动画曲线也交给引擎(目前只给时长,曲线在 ArkTS);
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充真机(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。

九、总结

9.1 核心难点回顾

上游 CrossMessages(Compose Host 三件套)
    → 拆源码集(commonMain 搬空,UI 移到 composeMain)
    → 抽引擎(排队 / 计时 / 暂停 / 动作 / 色板 / 度量,零依赖)
    → 工具链对齐 HarmonyOS Kotlin 2.2.21-1.0.0(Gradle 8.14.1)
    → 根构建替换(cmp_qrose 模式)
    → ohosArm64 / ohosX64 KLIB + 双 ABI 动态库
    → C ABI(@CName 单入口)+ 扁平 JSON + 宿主时钟
    → C++ N-API(DisposeString 纪律)
    → ArkTS JSON 解析 + 四 Tab 重绘
    → ArkUI 真机验收(排队 / 暂停 / 滑关 / 动作 / 五套配色)

9.2 封装层次

  • 引擎层:CrossSnackbarQueue / CrossToastQueue / CrossAlertQueue、CrossMessagePalette、CrossMessageMetrics;
  • 参考层:composeMain 里上游 Compose Host,留档不编译;
  • Native 层:双 ABI 动态库,通过 1 个 C ABI 输出 JSON;
  • N-API 层:参数检查、字符串转换、Native 内存释放;
  • ArkTS 层:页面状态、JSON 校验、手势、动画、日志与错误文本;
  • DevEco 层:CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 路线要跟着依赖走:库如果能真编译(依赖都有 ohos 制品),就别抽内核;Host 绑死 Compose 时,抽引擎是唯一不重打 Compose 发行版的做法。两条路线各有各的标准答案;
  2. 让时间成为参数:now 从宿主传入后,4000ms / 10000ms / 暂停剩余全部变成纯函数,JVM 上逐毫秒可测,真机上也不用担心主线程卡顿;
  3. 颜色和尺寸必须由引擎给:ArkTS 只负责画,不负责「大概多少 dp」。op=metrics + op=theme 让「换皮」和「语义」彻底分开,切深色也不会把正在看的卡片关掉。

9.4 适配成果

当前适配已完成:

  • CrossMessages 2.0.0-ohos.1 的纯 Kotlin 消息引擎(7 个文件、零第三方依赖)双 ohos 目标编译;
  • HarmonyOS Kotlin 2.2.21-1.0.0 + Gradle 8.14.1 的可复现构建;
  • 1 个 C ABI 符号(llvm-nm 验证双 ABI 全局 T)与 C++ N-API 包装层;
  • DevEco Stage ArkUI 四 Tab 验收页面(Snackbar / Toast / Alert / About,约 2000 行);
  • 11 项 JVM 交叉验证全绿 + 双 ABI so(arm64 ≈3.96 MiB、x86_64 ≈3.68 MiB);
  • 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

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

更多推荐