开源鸿蒙平台 CMP 三方库「krumb」适配全流程
本文记录
krumb接入 OpenHarmony 的完整过程,覆盖 Toast 引擎与 Compose UI 的依赖边界盘点、鸿蒙工具链钉版、真实引擎编译、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
krumb是 Compose Multiplatform 生态的 Toast / 通知库——“CMP 界的 sonner”:一行式全局 API(Toaster.success("…")可从 ViewModel、协程、点击回调任意处调用)、优先级抢占队列(maxVisible = 3溢出排队、HIGH抢占屏上最旧LOW)、按类型默认时长与无限时长 Loading、Promise 模式(loading → 自动转 success / error 并重挂定时器)、句柄在线更新、Action 回调与自定义内容注册表。它分成两半:krumb-core是纯 Kotlin + 协程的引擎(队列、控制器、定时器、门面),krumb-compose/krumb-material3是自绘 Compose UI(ToasterHost宿主、弹簧动画、手势)。本次适配把边界划在两半之间:引擎原样编译成 OpenHarmony KLIB——上游commonMain一行不改、全库零 expect/actual、ohos 侧零新增源文件;Compose UI 因社区 CMP 工具链(1.9.2 / Kotlin 2.2.21)与上游(CMP 1.10.3 / Kotlin 2.2.20)对不齐而隔离到不参与编译的composeMain目录,Toast 外观由 ArkTS 复刻、行为全部来自引擎真身。9 个 C ABI 符号把引擎能力交给 ArkTS,一个可交互的 Toast 演示场跑在真机上。
项目地址: AtomGit/oh-tpc/krumb.git
上游仓库: NadeemIqbal/krumb
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
krumb 为 Compose Multiplatform 应用解决"任意处弹通知"这一件事:不穿 SnackbarHostState、不 hoist 状态,MaterialTheme { Material3ToasterHost { App() } } 包一次根,之后 Toaster.success("Profile saved") 全局生效。屏上行为全部由 krumb-core 驱动:ToastController 持优先级队列与 visible: StateFlow<List<Toast>>,入队 / 抢占 / 定时消失 / 暂停恢复(按剩余时长续走)都在协程里完成;ToastHandle.update(message, type) 支持在线换文案换类型,promise 把 loading → 结果的状态机收敛成一个挂起函数。KMP 项目把这些放在 commonMain,Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套通知引擎,但上游构建(Kotlin 2.2.20 / CMP 1.10.3 / AGP 8.7.3)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以显示按钮,却无法证明通知引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证队列引擎、协程运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.2.20 / CMP 1.10.3 与鸿蒙工具链(HarmonyOS Kotlin 2.2.21-1.0.0 / 社区 CMP 1.9.2)无法共存于一个 Gradle 构建;ToasterHost 一族 Composable 编不进 ohos。 |
| 依赖矩阵 | 引擎依赖 kotlinx-coroutines-core,必须有 ohos 变体才能解析(社区 fork 1.10.2-1.0.0 已就位)。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象与 StateFlow 订阅,需要 C ABI、C++ N-API 和 JSON。 |
| 队列语义跨不了桥 | 优先级抢占、暂停续走是 Kotlin 侧概念,C ABI 只能同步一来一回。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:引擎的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。队列语义完全由上游原版实现负责,ArkTS 只负责轮询快照、渲染卡片和转发交互。
1.2 库提供的能力
Demo 提供一个真实的 Toast 会话,对应桥接的全部入口:
- 五类型 + 自定义:
success/error/info/warning/loading(无限时长、无进度条)与custom(内容键由 ArkTS 壳渲染自定义布局); - 优先级队列:
low/normal/high快速连点,屏上最多 3 条、超出进 pending(黄标显示数量)、HIGH抢占最旧LOW、同级 FIFO 补位; - Action 回调:
actionLabel+actionId,点按钮回调在 Kotlin 侧执行(Undo 关闭自身并弹 info;Retry 关闭后先 warning 再延迟 success); - Promise 模式:loading → 1.8s 后转 success / error,换文案换色、定时器重挂(4s / 6s);
- 暂停 / 恢复:按住卡片暂停(进度条停、出现 ⏸)、松开按剩余时长续走;
- 手势关闭:横滑超过 60vp 调
dismiss; - 程序化控制:
dismiss(id)单关、dismissAll清空(含 pending); - 版本握手:
krumbVersion()返回 fork 版本号,来自真实 Native 调用而非静态文字。
快照响应包含:
| 字段 | 作用 |
|---|---|
visible[] | 当前屏上 toast 全量:id / message / type / durationMs / remainingMs / paused / position / priority / actionLabel / custom / customKey。 |
pending | 溢出队列数量(引擎 pendingSnapshot())。 |
maxVisible / version | 队列容量与版本握手。 |
引擎侧完整能力(队列抢占、定时器、暂停续走、promise 状态机、句柄更新、Toaster 门面)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 27 项 jvmTest 交叉验证用例(上游原测试文件零改动),证明 ohos .so 里执行的队列逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | krumb-core 全部 commonMain 文件原样编译,不提取、不重写、零 expect/actual,ohos 侧零新增源文件;krumb-compose / krumb-material3 源码迁入 composeMain 隔离(非注册源集,Gradle 不编译,目录即休眠)。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 sample/*(android / desktop / web / ios 样例)留盘不进构建。 |
| 桥接稳定 | 9 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;状态经快照轮询而非回调订阅;异常永远不跨 FFI。 |
| 依赖纪律 | Kotlin 钉 2.2.21-1.0.0(与上游 2.2.20 同代)、coroutines 换 ohos 制品线 1.10.2-1.0.0(版本主体与上游 1.10.1 同代),上游测试零改动全绿。 |
| UI 完整 | 单页覆盖全部桥接入口:五类型、Action、Promise、优先级、六位置选择器、手势与版本握手。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。
krumb的 UI 半身(ToasterHost)留在composeMain等工具链对齐;ArkTS Demo 用引擎快照驱动卡片渲染,是引擎行为的验收宿主,不是上游样式的复刻承诺。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认 coroutines ohos 制品线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Compose 模块隔离
第 3 阶段:桥协议建模 ── 9 个导出的 JSON 契约、快照结构、remainingMs 记账与错误通道
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 快照轮询、六锚点卡片、进度条、手势、转场与 ArkTS 严格模式排雷
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
krumb-core/ 上游引擎(进 ohos 构建,jvm + ohosArm64 + ohosX64,零新增源码)
krumb-compose/ 上游 Compose UI(src/composeMain 隔离,留盘不进 settings)
krumb-material3/ 上游 Material3 宿主(src/composeMain 隔离,留盘不进 settings)
sample/ 上游四平台样例(android/desktop/web/ios),留盘不进构建
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
doc/ 本文章与真机效果图
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :krumb-core 与 :example:nativeApp,rootProject.name = "ohos_krumb"。上游的 vanniktech 发布插件、AGP classpath、compose 编译器插件、Maven Central 发布段全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(Temurin 21.0.12,org.gradle.java.home 钉死路径) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.11.1(沿用上游 wrapper;2.2.21 线与 8.11/8.14 同代兼容) |
| kotlinx | coroutines / coroutines-test 1.10.2-1.0.0(ohos 制品线,含 ohosArm64 / ohosX64 变体) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.krumb |
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
| 模块 | commonMain 文件 | 依赖形态 | 处置 |
|---|---|---|---|
krumb-core | 8 | 纯 Kotlin + coroutines(StateFlow / Mutex / delay),零 UI、零平台 import、零 expect | 整体编译(jvm + 双 ohos) |
krumb-compose | 10 | androidx.compose.* 全量(runtime / animation / foundation / ui / icons)+ 手势 | 迁入 src/composeMain,不进构建 |
krumb-material3 | 2 | compose material3 主题 | 迁入 src/composeMain,不进构建 |
expect 声明全库 0 处——这是这批 CMP 库里少见的干净底子:ToastController 的调度直接用 Dispatchers.Default(全平台可用),时长用 kotlin.time,ID 用 kotlin.random,没有一行平台分支。这意味着 ohos 侧连一个 actual 都不用补(对比 KStore 还需要 8 行 dispatcher actual),适配面全部落在构建脚本与桥接层。唯一要处理的"共享类型"问题是反向的:上游测试与 UI 都在 commonTest / commonMain 里引用 Compose,所以隔离动作做在 UI 模块上而不是引擎上。
krumb-compose 值得单独说明:它的全部价值是 ToasterHost 一族 Composable,而这些类型站在 androidx.compose.ui 上。社区 CMP 1.9.2 的 ohos 制品与上游 CMP 1.10.3 的 API 面对不齐(动画 / 手势 / 图标族差异),硬接等于把 UI 层重写一遍还引入漂移——所以隔离而非移植。composeMain 不是注册源集,Gradle 无视该目录;等社区工具链对齐后改回 commonMain、恢复 settings 与插件即可整体接回,ArkTS 壳降级为 fallback。
1.4 页面边界
标题区 🍞 KRUMB × OPENHARMONY / 版本握手行(krumb-core 0.1.1-ohos.1 · Kotlin/Native + ArkTS shell)
位置区 六宫格选择器(↖ ↑ ↗ / ↙ ↓ ↘),切换后新 toast 落对应锚点
类型组 Success / Error / Info / Warning / Loading (infinite) / Custom content
动作组 Save + Undo / Sync fail + Retry(Action 回调在 Kotlin 侧执行)
Promise 组 Promise OK / Promise FAIL(loading → 结果转换 + 定时器重挂)
优先级组 Low / Normal / High (preempts Low) / Dismiss all
状态行 pending 黄标(N pending in queue)
提示行 tap toast = pause / resume · horizontal swipe = dismiss
浮动层 六个锚点 ToastArea(顶部三区向下排、底部三区向上排,空容器穿透点击)
页面启动时先调 krumbVersion() 做握手并渲染版本行,再启动 120ms 快照轮询。
第 2 阶段:目标与依赖打通
2.1 制品线验证(本适配的成败手)
先确认社区 Maven(https://maven.eazytec-cloud.com)上有没有 ohos 的 coroutines 制品。本机 Gradle 缓存里已经躺着完整的解析结果:
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2-1.0.0
variants: ohosArm64 / ohosX64 / jvm 齐全(coroutines-test 同线)
org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.21-1.0.0
提供 ohosArm64() / ohosX64() target DSL
两个关键结论:其一,1.10.2-1.0.0 与上游声明的 1.10.1 同代(1.10 线),krumb-core 的 commonMain 里一行依赖坐标都不用改,只换版本号后缀;其二,coroutines 1.10 线基于 Kotlin 2.2.x stdlib 构建,与 HarmonyOS Kotlin 2.2.21-1.0.0 元数据互相可读——不像 KStore 那条 1.11 线必须把工具链升到 2.4.20。krumb 上游本来就是 Kotlin 2.2.20,2.2.21 与之同代,源码级兼容成本为零,Gradle 8.11.1 wrapper 原样沿用(qrose 用 8.14.1 同代先例)。
2.2 工具链钉版(为什么是 2.2.21-1.0.0)
社区已批准的钉法:HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 用的同一条线)。coroutines 1.10.2-1.0.0 的 stdlib 元数据是 2.2 系,2.2.21 读得动;反过来官方 Maven Central 的 Kotlin 没有ohos target,配置期直接报 Unresolved reference: ohosArm64——所以 eazytec 双仓必须置顶:
pluginManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
google()
mavenCentral()
}
}
rootProject.name = "ohos_krumb"
include(":krumb-core")
include(":example:nativeApp")
gradle.properties 同步钉死 JDK 与 KN 分发渠道:
org.gradle.java.home=C:\\Program Files\\Eclipse Adoptium\\jdk-21.0.12.101-hotspot
version=0.1.1-ohos.1
kotlin.native.ignoreDisabledTargets=true
kotlin.native.distribution.downloadFromMaven=true
2.3 编译真实引擎
krumb-core/build.gradle.kts 收敛为纯 KMP 模块(android / iOS / wasm 全部退位):
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {
implementation(libs.kotlinx.coroutines.core) // 1.10.2-1.0.0
}
commonTest.dependencies {
implementation(kotlin("test"))
implementation(libs.kotlinx.coroutines.test)
}
}
}
实测:
.\gradlew.bat :krumb-core:compileKotlinOhosArm64 :krumb-core:compileKotlinOhosX64 # BUILD SUCCESSFUL
引擎的全部 8 个 commonMain 文件一个没动,ohos 侧零新增源文件。krumb-compose / krumb-material3 的迁移只动目录名:src/commonMain → src/composeMain、src/commonTest → src/composeTest(git 识别为 100% rename),两模块移出 settings,构建脚本原样留盘不评估。
2.4 测试依赖的处置
commonTest 依赖对 ohos 测试编译同样可见,根构建全局禁用 ohos 测试任务(与 KStore / qrose 同款纪律):
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
这样上游测试文件零改动保留,测试统一在 JVM 跑,ohos 侧只承担编译与链接。
第 3 阶段:桥协议建模
3.1 JSON 契约
io.krumb.core/**(上游引擎,原样编译)
↓ ToastController / 队列 / 定时器 / promise
Bridge.kt(@CName × 9,扁平 JSON 进出)
↓ UTF-8 JSON
N-API krumbShow / krumbSnapshot / krumbAction / krumbPromise / …
↓ 120ms 轮询
ArkUI Index.ets(六锚点卡片渲染)
请求与响应:
krumbShow 请求 : {"type":"success","message":"Profile saved","durationMs":0,
"priority":"normal","position":"topCenter",
"actionLabel":"Undo","actionId":"undo","customKey":"promo"}
durationMs: 0/缺省=按类型默认(4s/5s/6s/无限),-1=无限
krumbShow 响应 : {"ok":true,"id":"t_<hex>_<hex>"}
krumbSnapshot : {"ok":true,"version":"0.1.1-ohos.1","maxVisible":3,"pending":1,
"visible":[{id,message,type,durationMs,remainingMs,paused,
position,priority,actionLabel,custom,customKey}]}
krumbPromise : {"delayMs":1800,"succeed":true,"loading":"…","okText":"…","failText":"…"}
错误通道 : {"ok":false,"error":"…"}(Kotlin 异常永不跨 FFI)
三个不对称设计:show 是命令(入队即返回 id,观感走快照),snapshot 是状态(全量拉取,ArkTS 侧 diff 渲染),action 是事件(按 toast id 触发 Kotlin 侧闭包)。命令 / 状态 / 事件三通道分离,桥上不需要为每种交互发明新签名。
3.2 快照轮询而不是回调订阅
引擎的 visible 是 StateFlow<List<Toast>>,但跨语言回调要处理注册 / 反注册与线程亲和,C 签名也过不了挂起函数。桥的选择:ArkTS 每 120ms 调一次 krumbSnapshot() 全量拉取。StateFlow.value 读取线程安全,轮询天然合并渲染帧;对 4s 级 toast,120ms 的观测粒度对观感无影响。pending 计数由桥内一个收集 visible 的协程顺带刷新(队列只在 visible 变化时变动),快照函数本身保持非阻塞。
3.3 remainingMs 记账与错误通道
timerStartedAt 是控制器私有,桥用「toast 首次出现在 visible 的单调时刻」记账:remainingMs = duration - elapsed;暂停时冻结当前值,恢复时把记账基线回拨 (total - remaining),进度条跨暂停窗连续。无限时长(loading / custom)统一 durationMs = -1,ArkTS 不画进度条。
每个导出整体 try/catch,非法 JSON、未知类型、缺字段、内部异常全部走同一个 ok:false 通道,ArkTS 侧统一忽略——轮询周期内下一个快照自愈。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libkrumb_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohoskrumb.so)
│ KrumbShow / KrumbSnapshot / KrumbAction / KrumbPromise / KrumbDismiss(All) / KrumbPause / KrumbResume / KrumbVersion
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制队列语义,所以选择 C ABI + JSON。
4.2 Kotlin/Native 导出函数
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
KrumbShow | krumbShow(req) | ToastController.show / 五类型快捷入口 / showCustom(含 builder 全字段) |
KrumbSnapshot | krumbSnapshot() | visible StateFlow + pendingSnapshot() + remainingMs 记账 |
KrumbAction | krumbAction(id) | ToastAction.onClick 回调(Kotlin 侧执行) |
KrumbPromise | krumbPromise(req) | promise 状态机:loading → success/error + rearmTimer |
KrumbDismiss / KrumbDismissAll | krumbDismiss(id) / krumbDismissAll() | 单关 / 清空(含 pending) |
KrumbPause / KrumbResume | krumbPause(id) / krumbResume(id) | 定时器暂停 / 按剩余时长续走 |
KrumbVersion | krumbVersion() | fork 版本握手(真机版本行的数据来源) |
桥内还有一段演示桩(runDemoAction):undo 关闭自身并弹 info、retry 关闭后先 warning 再延迟 1.5s success、open 弹仓库链接——这是"ViewModel"的角色,证明 Action 回调里可以编排任意 Kotlin 逻辑。
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"krumbVersion", nullptr, NapiKrumbVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"krumbShow", nullptr, NapiKrumbShow, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
/* …snapshot / action / promise / dismiss / dismissAll / pause / resume 同构 … */
};
字符串参数统一两段式转换(先 napi_get_value_string_utf8 量长,再 std::vector<char> 拷贝——API 26 头文件 const 问题的社区解法);KN 返回的字符串必须经 libohoskrumb_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohoskrumb_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与九个业务符号),不拖整份生成头。
4.4 CMake 和类型声明
add_library(ohoskrumb SHARED IMPORTED)
set_target_properties(ohoskrumb PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(krumb_napi SHARED napi_init.cpp)
target_link_options(krumb_napi PRIVATE
"-Wl,--no-undefined"
"-Wl,-rpath,\$ORIGIN")
target_link_libraries(krumb_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohoskrumb)
OHOS_ARCH 为空或 libohoskrumb.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见——$ORIGIN 转义写法还防了 Windows 盘符混进 RUNPATH。
export const krumbVersion: () => string;
export const krumbShow: (request: string) => string;
export const krumbSnapshot: () => string;
export const krumbAction: (id: string) => string;
export const krumbPromise: (request: string) => string;
export const krumbDismiss: (id: string) => void;
export const krumbDismissAll: () => void;
export const krumbPause: (id: string) => void;
export const krumbResume: (id: string) => void;
第 5 阶段:ArkUI 宿主
5.1 快照轮询和状态渲染
aboutToAppear(): void {
this.version = krumbVersion(); // 版本握手:来自真实 Native 调用
this.refresh();
this.timer = setInterval(() => { this.refresh(); }, 120);
}
refresh(): void {
try {
const snap = JSON.parse(krumbSnapshot()) as Snapshot;
if (snap.ok) {
this.toasts = snap.visible; // @State 全量替换,ArkUI 按 key diff
this.pending = snap.pending;
}
} catch (e) { /* 一个坏快照不能打断演示 */ }
}
ForEach 以 toast id 为 key,配合 TransitionEffect.OPACITY.combine(translate) 出入场转场(顶部区从 -24vp 滑入、底部区从 +24vp),200ms FastOutSlowIn 近似上游 spring。
5.2 六锚点布局与手势
页面根部一个 Stack,六个全屏 ToastArea(TopStart / Top / TopEnd / BottomStart / Bottom / BottomEnd 对齐)叠在控制面板之上,空容器 hitTestBehavior(HitTestMode.Transparent) 穿透点击、只有真实卡片接手势。卡片本体:深色圆角卡片(#111827)+ 类型色圆形图标(✓ ✕ ! i,色值取上游 KrumbDefaults:绿 #1F8A4C / 红 #C0392B / 橙 #D68910 / 蓝 #2563EB)+ 文案 + Action 按钮 + 线性进度条(remainingMs / durationMs 换算百分比);loading 型换 LoadingProgress 且无进度条;custom 型走 CustomBody(🍞 图标 + customKey 文案 + Star 按钮——对应上游 CustomContentRegistry 的"内容键由宿主渲染"语义)。
交互三件套全接真桥:按住卡片 krumbPause(id) / 松开 krumbResume(id)(⏸ 标记随快照出现消失);横滑超过 60vp krumbDismiss(id);点 Action 按钮 krumbAction(id)。
5.3 ArkTS 严格模式排雷(两处编译期拦截)
第一处,对象字面量必须显式定型:show(param: Record<string, Object>) 这种自定义方法参数在 arkts-no-untyped-obj-literals 检查下不被接受,12 处调用点全部报错。解法是把字面量内联进 JSON.stringify(...)(SDK 声明参数位置,检查器放行——qrose / KStore 审核版同款惯用法):
this.show(JSON.stringify({ type: 'success', message: 'Profile saved', position: this.anchor }));
第二处,状态属性名与通用属性冲突:@State position 撞上 CustomComponent 基类继承的 .position() 通用属性,编译器报类型不兼容。改名 anchor(桥接协议里的 JSON 字段名 position 不受影响,仍在请求串里)。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohoskrumb)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ └── Bridge.kt @CName × 9 + MiniJson(扁平 JSON,继承自 cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.krumb、versionName 0.1.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 导出 × 9、参数检查、DisposeString
├── src/main/cpp/include/libohoskrumb_api.h
├── src/main/cpp/types/libkrumb_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 验收页面
copyKnSoArm64 / copyKnSoX64 被 finalizedBy 挂在 link 任务后,除了 libohoskrumb.so 还会从 ~/.konan/dependencies/<toolchain>/lib/<triple>/ 把 libc++_shared.so 一起拷进 entry/libs/<abi>/——每个 ABI 三件套缺一不可。
6.2 构建、签名和安装
.\gradlew.bat :krumb-core:jvmTest # 27 项交叉验证
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组、product 引用 default 占位)。
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.krumb -m entry
四、完整代码对照
4.1 整体架构
io.krumb.core/**(上游引擎原样编译)
│ ToastController / ToastQueue / Toaster / ToastHandle / promise
Bridge.kt + MiniJson
│ KrumbShow / Snapshot / Action / Promise / Dismiss(All) / Pause / Resume / Version
libkrumb_napi.so
│ krumbShow / krumbSnapshot / krumbAction / …(× 9)
Index.ets
│ 120ms 轮询 + JSON.parse + @State diff
▼
ArkUI 真机页面(六锚点 Toast 演示场)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_krumb、compose/sample 移出 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 + coroutines ohos 制品线钉版(仅 1 插件 2 库别名) |
build.gradle.kts(根) | 版本 0.1.1-ohos.1、全局禁用 Ohos*Test* 任务 |
krumb-core/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,引擎零源码改动 |
krumb-compose/、krumb-material3/ | composeMain 隔离(git 100% rename),脚本留盘不评估 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 9 个 C ABI、快照记账、演示 Action 桩、MiniJson、错误通道 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出 × 9、两段式字符串转换、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 验收页面(轮询 / 六锚点 / 手势 / 转场) |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(16 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | Toaster.success/error/info/warning/loading | 一行式全局入口(桥内直连 ToastController 同款方法) |
| 引擎 | ToastController.show { action(…) duration position priority } | builder 全字段(KrumbShow 请求的 Kotlin 侧落点) |
| 引擎 | showCustom(customContentKey) | 自定义内容注册表接缝(ArkTS 按 customKey 渲染) |
| 引擎 | promise(block, loading, success, error) | loading → 结果状态机 + rearmTimer(KrumbPromise 全程真跑) |
| 引擎 | visible: StateFlow<List<Toast>> | 屏上状态源(KrumbSnapshot 的数据来源) |
| 引擎 | pause(id) / resume(id) | 定时器暂停 / 剩余时长续走 |
| 引擎 | pendingSnapshot() | 溢出队列快照(pending 黄标) |
| 引擎 | ToastHandle.update / dismiss | 在线更新 / 句柄关闭(promise 内部即用 update) |
| Native | KrumbShow / Snapshot / Action / Promise / Dismiss / DismissAll / Pause / Resume / Version | C ABI JSON 导出 × 9 |
| N-API | krumbShow(req) / krumbSnapshot() / … | 向 ArkTS 暴露能力 × 9 |
| ArkTS | refresh() / show(req) | 轮询快照 / 触发命令并立即拉一次 |
4.4 ArkTS 与 Kotlin 的边界
// ArkTS:弹一条带 Undo 的成功通知(等价上游 Toaster.show("Document saved") { action("Undo") { … } })
this.show(JSON.stringify({
type: 'success', message: 'Document saved',
actionLabel: 'Undo', actionId: 'undo', position: this.anchor
}));
// Kotlin:桥侧把请求落进真实引擎,Action 闭包在 Kotlin 侧执行
var selfId = ""
val handle = controller.show(req.str("message")) {
type = parseType(req.str("type"))
duration = parseDuration(req.int("durationMs", 0), type)
if (actionLabel.isNotBlank()) action(actionLabel) { runDemoAction(actionId, selfId) }
/* …priority / position… */
}
selfId = handle.id
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象、StateFlow 订阅或未校验的动态结构。
五、关键决策说明
决策 1:引擎真编译 + Compose UI 隔离,而不是整库移植或纯 ArkTS 重写
krumb 的价值一半在引擎(任意处可调、优先级抢占、promise、句柄更新),一半在 Compose UI(ToasterHost 自绘)。引擎半身是纯 Kotlin + 协程,ohos 制品线(coroutines 1.10.2-1.0.0)已就位,真编译零成本——而且全库零 expect,连 actual 都不用补。UI 半身站在 CMP 1.10.3 上,社区工具链 1.9.2 接不住,硬移植等于重写。所以走"引擎真身 + UI 壳":队列语义由上游单测锁定,ArkTS 只画卡片。反向的纯 ArkTS 重写(自己写个鸿蒙 toast 轮子)被否决——那不是适配,是另起炉灶,行为语义没有上游测试背书。
决策 2:Kotlin 钉 2.2.21-1.0.0,Gradle 8.11.1 原样沿用
coroutines 1.10 线基于 Kotlin 2.2.x stdlib,2.2.21 读得动;krumb 上游本来就是 2.2.20,同代零降级。对比 KStore 那条线:coroutines 1.11 依赖 stdlib 2.4.20 元数据,必须把工具链一起升——两条线的经验不能互相套用,钉版永远由依赖制品的元数据反推。Gradle 方面 2.2.21 线与 8.11/8.14 同代(qrose 用 8.14.1),上游 wrapper 8.11.1 实测直接可用,一行没改。
决策 3:根构建替换(cmp_qrose 模式),compose 模块与 sample 留盘
上游根构建(vanniktech 发布 / AGP / compose 编译器 / Maven Central)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。krumb-compose / krumb-material3 迁入 composeMain(非注册源集,目录即休眠,git 记录为 100% rename),sample/* 留盘——它们的存在方式只有这一种,等社区 compose.ui ohos 制品与 CMP 1.10+ 对齐后改回目录名即可整体接回。
决策 4:快照轮询 + 桥侧记账,而不是跨语言回调
visible 的订阅推不过 C 签名,注册式回调要管生命周期与线程亲和。轮询 120ms 对 4s 级 toast 观感无影响、天然合并渲染帧、坏快照下一拍自愈。进度条的 remainingMs 由桥侧单调钟记账(引擎的 timerStartedAt 是私有),暂停冻结 / 恢复回拨基线,误差小于一个轮询周期——这是明确声明并写进文档的近似,不是假装精确。
决策 5:jvm 交叉验证 + 测试零改动
引擎逻辑在 commonMain,JVM 与 ohos 跑同一份代码;27 项 jvmTest 跑的就是上游原测试文件(零改动),是"队列语义没漂"的闸门。coroutines-test 没有 ohos 制品也没关系——测试统一 JVM 跑 + 根构建禁用 Ohos*Test*,交叉验证的有效性建立在测试文件一个字节不动之上。
六、测试与验证
6.1 测试环境
本次验证使用 Windows x64、JDK 21(Temurin 21.0.12)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
.\gradlew.bat :krumb-core:jvmTest
27 项用例全部通过(上游原测试文件,零改动):
| 测试类 | 用例数 | 钉住什么 |
|---|---|---|
ConcurrencyTest | 1 | 并发入队 / 驳回一致性 |
ToastControllerTest | 12 | 控制器状态机:五类型入口、builder、dismiss / dismissAll、暂停恢复续走、update |
ToastQueueTest | 11 | 队列抢占矩阵:maxVisible、HIGH 抢占 LOW、同级 FIFO、pending 补位 |
PromiseTest | 3 | loading → success / error 转换、取消路径、定时器重挂 |
依赖换线没有 API 漂移:coroutines 1.10.1 → 1.10.2-1.0.0(ohos 线)后版本主体同代,上游测试照跑全绿。
6.3 HAP 验证
:krumb-core:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL(copyKnSo* 自动回填 entry/libs)
llvm-nm -D libohoskrumb.so: KrumbVersion / Show / Snapshot / Action / Promise /
Dismiss / DismissAll / Pause / Resume / libohoskrumb_symbols
共 10 个全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libkrumb_napi.so)
CompileArkTS: finished(Record→JSON.stringify 内联 + anchor 改名后一次通过)
PackageHap: finished
install bundle successfully
start ability successfully
KN so 未 strip 的 debugShared arm64 约 5.8MB、x86_64 约 5.3MB(含 Kotlin/Native 运行时与 coroutines);HAP 内双 ABI 各三件套(libkrumb_napi.so / libohoskrumb.so / libc++_shared.so)。
6.4 功能用例
- 启动后版本行显示
krumb-core 0.1.1-ohos.1 · Kotlin/Native + ArkTS shell(来自krumbVersion()真实 Native 调用,非静态文字); - 点 Success:顶部中央滑入深色卡片,绿圆 ✓ 图标 + “Profile saved” + 绿色进度条 4s 匀速走完自动消失;
- 连点 Info / Error / Warning:三条堆叠(蓝 i / 红 ✕ / 橙 !),各色进度条并行倒数,先到期先消失;
- 屏上铺满后继续点击:黄标出现
N pending in queue,前面的消失后 pending 依次补位上屏; - 铺 LOW 后点 High:最旧 LOW 被抢占顶掉,HIGH 立即上屏——抢占语义在真机可见;
- 点 Save + Undo 后点卡片上 Undo:自身关闭并弹 info “File restored”(回调在 Kotlin 侧执行);
- 点 Promise OK:loading(转圈、无进度条)→ 1.8s 后转绿 “Uploaded report-2026.bin (2.4 MB)”,进度条从头重挂;Promise FAIL 同理转红;
- 按住任意卡片:进度条停、出现 ⏸;松开:从剩余时长续走;
- 横滑卡片超 60vp:该卡片滑出消失;
- 六宫格切换 position:新 toast 落对应锚点(顶部三区向下排 / 底部三区向上排);
- 点 Loading:无限时长无进度条,只能手势滑掉或 Dismiss all;
- 点 Custom content:卡片内 ArkTS 自定义布局(🍞 + customKey 文案 + Star 按钮),点 Star 弹仓库链接 info。
6.5 验证结论
队列引擎、协程运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的 Toast 会话:版本握手、五类型、优先级抢占、pending 补位、Action 回调、Promise 状态机、暂停续走与手势关闭全部按引擎语义运转(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/krumb.git
cd krumb
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
.\gradlew.bat :krumb-core:compileKotlinOhosArm64 :krumb-core:compileKotlinOhosX64 # klib
.\gradlew.bat :krumb-core:jvmTest # 27 项交叉验证
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# DevEco Studio 打开 example/harmonyApp(不是仓库根),签名后 Run 或:
hdc install -r entry\build\default\outputs\default\entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.krumb -m entry
hdc shell hilog | grep Krumb
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:krumb-core真正编译成ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;org.gradle.java.home只管 Gradle 进程内部 JVM,gradlew启动器仍要JAVA_HOME——Windows 上不设环境变量会报JAVA_HOME is not set,退出码还是 0(管道吃掉了),别被假成功骗过;- Kotlin 的
promise(block, …)里block是首参,尾随 lambda 会被绑到position上,编译报"Argument type mismatch: Function0 vs ToastPosition"——多参挂起函数一律具名传参; delay(Int)编不过,KN 的delay只收Long/Duration——桥里req.int()出来的是 Int,记得toLong();- ArkTS 严格模式
arkts-no-untyped-obj-literals:对象字面量传给自定义方法的Record参数不被接受(12 处全报错),内联进JSON.stringify(...)即过(SDK 参数位置放行,qrose / KStore 同款惯用法); - ArkTS 组件状态命名不能撞通用属性:
@State position与CustomComponent基类的.position()冲突报类型不兼容,改名anchor(协议字段名不受影响); entry/build-profile.json5的abiFilters必须双 ABI(arm64-v8a+x86_64),缺 x64 模拟器装不上;每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可,缺 KN so 安装报9568347,缺 NAPI so ArkTS 报does not provide an export named 'krumbShow';- KN 返回字符串必须
libohoskrumb_symbols()->DisposeString归还运行时,N-API 层不能free();napi_get_value_string_utf8用两段式(量长再拷)绕 API 26 头文件 const 问题; - CMake 的 rpath 在 Windows 上要防盘符混进 RUNPATH:
-Wl,-rpath,\$ORIGIN转义 +BUILD_WITH_INSTALL_RPATH TRUE+ POST_BUILD 把三件套拷到同目录; - DevEco 打开的是
example/harmonyApp子目录而不是仓库根(根是 Gradle 工程,两套工程互不相干);HAP 打包用 DevEco 自带 JBR,系统 JDK 8 会让hvigorw assembleHap半路报不相干的错; - 签名材料由 DevEco 自动写入
build-profile.json5,仓库内保持signingConfigs空数组 + product 引用default占位,证书绝不进入 AtomGit。
8.2 已知问题
- 鸿蒙侧 Toast 外观是 ArkTS 复刻(深色卡片 + 线性进度条 + 转场近似),上游
KrumbStyle的样式旋钮(圆角 / 间距 / 图标着色 / spring 参数)未透出; - 进度条数值是桥侧单调钟近似,toast 从 pending 转正的时刻与引擎
armTimer有毫秒级偏差(小于轮询周期 120ms),暂停 / 恢复由桥补账保持连续; krumb-compose/krumb-material3隔离在composeMain(休眠非删除),ToasterHost一族 Composable 待社区compose.uiohos 制品与 CMP 1.10+ 对齐后接回,届时 ArkTS 壳降级为 fallback;updates式订阅不跨 C ABI:桥是快照轮询,推送式状态变化(N-API 线程安全回表)留作演进;- android / iOS / wasm 目标剥离(源文件留在树里),上游 CI workflow 编的是已移出构建的目标,属遗留物随仓库保留;
- 无 ohpm /
.har分发:产物是 so 三件套 + HAP demo,接入方复制example/nativeApp桥模式; - HAP 安装必须使用与 bundle name
org.terminator.ohos.krumb匹配的签名。
8.3 未来优化方向
- 增加快照版本号(引擎侧
visible收集时 bump),ArkTS 空闲时降频轮询,替代固定 120ms; - 桥协议补
maxVisible参数化与显式update入口(对应ToastHandle.update的直达暴露); - 等社区 CMP 工具链对齐后接回
composeMain,ArkTS 壳转为 fallback 路径; - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。
九、总结
9.1 核心难点回顾
上游 krumb-core(8 文件 commonMain,零 expect)
→ 验证 coroutines ohos 制品线(1.10.2-1.0.0)
→ 工具链对齐 Kotlin 2.2.21-1.0.0(Gradle 8.11.1 沿用)
→ 根构建替换(cmp_qrose 模式,compose/sample 移出)
→ composeMain 隔离(git 100% rename,休眠待接回)
→ ohosArm64 / ohosX64 KLIB(引擎零源码改动)
→ C ABI(@CName × 9)+ 快照轮询 + remainingMs 记账
→ C++ N-API(DisposeString 纪律)
→ ArkTS 严格模式排雷(字面量定型 / anchor 改名)
→ ArkUI 真机演示场(五类型 / 抢占 / Promise / 手势 / 六位置)
9.2 封装层次
- 引擎层:上游原版
ToastController、优先级队列、定时器、暂停续走、promise 状态机、Toaster门面; - Native 层:生成双 ABI 动态库并通过 9 个 C ABI 输入输出 JSON;
- N-API 层:完成参数检查、两段式字符串转换和 Native 内存释放;
- ArkTS 层:管理快照轮询、六锚点渲染、进度条换算、手势分发与错误忽略;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 适配边界由库的依赖剖面决定:krumb 引擎半身零 expect、纯 coroutines,ohos 侧一行源码都不用补——比 KStore(8 行 actual)还干净;UI 半身站在 CMP 1.10.3 上,社区 1.9.2 接不住——隔离而非移植。"真编译到哪一层"这个问题,答案永远在依赖矩阵里,不在意愿里;
- 钉版由依赖制品的元数据反推:coroutines 1.10 线配 Kotlin 2.2.21(本仓)、1.11 线配 2.4.20(KStore 仓),两条线的 Gradle 经验也不同(2.2 线 8.x 沿用、2.4 线 9.x 沿用)——历史结论不能跨线套用,每次适配先查制品再定工具链;
- 有状态引擎的正确桥法是"命令 + 快照":Toast 不像存储库一次一调用,它有持续演化的屏上状态(进度、暂停、补位)。命令(show/dismiss/pause)与状态(snapshot 全量)分离、轮询而非回调,让 ArkTS 侧零状态机——所有语义留在有上游单测背书的 Kotlin 侧,这是"内核没漂"在 UI 类库上的表达方式。
9.4 适配成果
当前适配已完成:
- krumb
0.1.1真实引擎的双 ABI Kotlin/Native 编译(上游commonMain零改动、全库零 expect、ohos 侧零新增源文件); - HarmonyOS coroutines 制品线在 Toast 引擎上的完整落地(
1.10.2-1.0.0,与上游 1.10.1 同代换线); - 9 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证 10 个全局 T 符号); - DevEco Stage ArkUI 演示场(五类型、优先级抢占、pending 补位、Action 回调、Promise 状态机、暂停续走、手势关闭、六位置锚点、版本握手);
- 27 项 JVM 交叉验证全绿(上游原测试文件零改动)+ HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证(效果图见第七节);
- 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)