开源鸿蒙平台 CMP 三方库「CrossMessages」适配全流程
本文记录
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,四个平台共享同一份调用代码。
它真正值钱的不是「画一个黑条」,而是三件容易被忽略的事:
- 挂起式 API。
showSnackbar(...)返回CrossSnackbarResult,调用方直接知道用户点了动作还是超时消失;showAlert(...)返回CrossAlertResult,拿到的是被点的那颗按钮的 index 与内容。没有回调、没有事件总线,协程挂起就是答案。 - 真正的排队。三个 HostState 内部都是
Mutex+suspendCancellableCoroutine,同一时刻只有一条消息在屏上,重复调用按顺序排队,不会互相顶掉。连续三次show的结果是「一条一条播」,不是「叠成一坨」。 - 可测的数值规则。
Short = 4000ms/Long = 10000ms、Toast2000 / 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)。 |
actionFired | Snackbar 动作是否真的触发(只有卡上有 actionLabel 才为 true)。 |
result | Alert 的结果:Dismissed 或 ActionPerformed(index, label, style)。 |
reason | queued / 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 Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | Temurin 21(org.gradle.java.home 指向本机 JDK 21) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(上游 wrapper 9.7.0 降下来) |
| 引擎依赖 | 零(无 coroutines、无 serialization、无 Compose) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.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 个 drawable | androidx.compose.* + Material 3 | 留档,不参与任何编译 |
library/src/commonTest | 1 | 只用 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.kt | snackbar/CrossSnackbarQueue.kt | Mutex 排队、current/pending、暂停、动作语义 |
snackbar/CrossSnackbarVisuals.kt | snackbar/CrossSnackbar.kt | 数据模型:TextAlign → 枚举、DrawableResource → Named(name)、Color → ARGB |
snackbar/CrossSnackbarDefaults.kt | core/CrossMessagePalette.kt + core/CrossMessageMetrics.kt | M3 baseline 色板 + 全套 dp/ms 度量 |
toast/CrossToastHostState.kt + CrossToastHost.kt | toast/CrossToast.kt | 2000/3500ms、排队、CrossToastDuration |
alert/CrossAlertHostState.kt + CrossAlertHost.kt | alert/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 + 宿主时钟
三个理由:
- 没有序列化依赖。ohos 线上不引
kotlinx-serialization,klib 体积与编译时间都省;MiniJson用字符串状态机跳引号内逗号,标题里有逗号、冒号也不会把对象切坏。qrose 与 compose-stacked-snackbar 都是同一套写法。 - 协议可读。抓一次 hilog 就能看出宿主传了什么、引擎回了什么,联调不用猜二进制布局。
- 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 方法 | 覆盖内核 |
|---|---|---|
CrossMessagesCommand | crossMessagesCommand(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.toml | Kotlin 2.2.21-1.0.0 单行目录 |
build.gradle.kts(根) | 版本 2.0.0-ohos.1、全局禁用 Ohos*Test* 任务 |
gradle.properties | JDK 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.kt | Toast 队列 / 2000-3500ms |
library/src/commonMain/.../alert/CrossAlert.kt | Alert 队列 / 动作合成 / index 回传 |
library/src/commonMain/.../core/CrossMessagePalette.kt | M3 baseline 明暗色板 + 语义槽解析 |
library/src/commonMain/.../core/CrossMessageMetrics.kt | 全部 dp/ms 常量(与 Compose 逐条对应) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 1 个 C ABI、MiniJson、错误通道 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 四 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) | 五套语义色,明暗各一套 |
| 引擎 | CrossMessageMetrics | elevation / margin / padding / 350ms / 4000ms … |
| Native | CrossMessagesCommand | C ABI JSON 单入口 |
| N-API | crossMessagesCommand(req) | 向 ArkTS 暴露能力 |
| ArkTS | snackbarCall(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 |
| JDK | Temurin 21.0.12 |
| Gradle | 8.14.1(wrapper) |
| Kotlin/Native | HarmonyOS 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 | 第二条入队,第一条离开后晋升 |
shortDurationDismissesAtFourSeconds | Short = 4000ms,3999ms 不关 |
longDurationDismissesAtTenSeconds | Long = 10000ms |
indefiniteNeverTimesOut | Indefinite 永不超时 |
promotedCardRestartsItsClock | 换卡重新锚定 shownAt,不是接着上一条计时 |
actionOnlyFiresWhenLabelPresent | 无 actionLabel 时 actionFired = false |
pauseFreezesRemainingTime | 1000ms 处暂停,9000ms 时剩余仍是 3000ms |
styleSelectsTheSamePaletteAsCompose | Neutral / Error 容器色与 Compose 一致 |
darkPaletteRecolorsTheLiveCard | 切深色重着色,但不改 deadlineAt |
clearDropsQueuedCardsToo | reset 同时清空当前与排队 |
引擎与 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 功能用例
- 启动后 About 页显示引擎
2.0.0-ohos.1/ 上游2.0.0(来自真实 Native 调用,非静态文字); - Snackbar 页点 Show(Success / Short / Bottom):绿色卡片从底部滑入,350ms 到位,4s 后自动消失;
- 连续点 Queue 3:一次只显示一条,右上
pendingCount = 2 → 1 → 0,逐条播放不重叠; - 切
Indefinite再 Show:不自动消失,点 ✕ 才走; - 按住卡片 2s 后松手:进度条停住再继续,总停留时间明显变长;
- 左右滑动卡片超过阈值:卡片滑出并关闭,底部响应出现
dismiss; - 打开 Action 开关、填「撤销」再 Show,点动作:卡片关闭,响应
actionFired: true; - 切
Top:卡片贴顶滑入,不遮挡状态栏内容; - 五套 style 逐个点:容器/文字/图标颜色按色板表变化,深色模式下取暗色列;
- Toast 页 Show:底部黑色半透明气泡,2s 消失;切 Long 约 3.5s;
- Alert 页「取消+删除」预设 Show → 点删除:对话框关闭,响应为
ActionPerformed index:1 style:Destructive; - 关闭
dismissible后 Show,点对话框外部:对话框不关,页面提示not-dismissible; - 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 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:library真正编译成ohosArm64/ohosX64KLIB,并让:example:nativeApp链接出双 ABI 的.so; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- 上游
libs.versions.toml里的 AGP / Compose 别名必须在配置期之前删掉,留着就会去google()解析 AGP,而 AGP 会要 Android SDK; - Gradle 9.7 wrapper 跑不动 HarmonyOS Kotlin 2.2.21,必须整体降到 8.14.1(
jar/bat/sh一起换,只改 properties 不够); CrossSnackbarDuration的枚举项叫Long,会把kotlin.Long遮住,写Long.MAX_VALUE编译不过,必须写kotlin.Long.MAX_VALUE;- 上游把
Resource当图标类型,引擎改成data class Named,when (icon)立刻报不穷尽,要补is Named ->分支; - 色板解析第一版写在
companion object里,看不见实例属性(dark标记),改成实例方法palette.colors(style)才通; - 引擎第一版「上一条一消失,第二条立刻上屏」,测试却断言「第二条是 current」失败——上游
Mutex是排队语义,第二条在第一条结束前只能queued;改测试而不是改引擎; - ArkTS
import 'libohoscrossmessages.so'只能看到 stdlib 的 zip 类型,@CName业务符号不会挂到 ArkTS 模块上,必须加 CMake NAPI 包装层; - 运行期
dlopen失败,多数是只拷了 KN so、没拷libc++_shared.so;copyKnSo*任务必须成对拷,CMake POST_BUILD 再拷一次到 NAPI 同目录; - 真机安装报
9568347,是只带了 arm64;abiFilters双写,两个 ABI 都出 so; - NAPI 拿到的字符串以后内存泄漏,是因为 KN 返回的
const char*在 Kotlin 堆上,必须DisposeString(raw),不能free(); - 消息里带逗号(「已保存到你的收藏,去看看吧」)时解析断在中间,是朴素
split(",")的锅;MiniJson用字符串状态机跳引号内分隔符; - 根
.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 三条经验
- 路线要跟着依赖走:库如果能真编译(依赖都有 ohos 制品),就别抽内核;Host 绑死 Compose 时,抽引擎是唯一不重打 Compose 发行版的做法。两条路线各有各的标准答案;
- 让时间成为参数:
now从宿主传入后,4000ms / 10000ms / 暂停剩余全部变成纯函数,JVM 上逐毫秒可测,真机上也不用担心主线程卡顿; - 颜色和尺寸必须由引擎给: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。
参考文档
更多推荐



所有评论(0)