本文记录 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 MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(Temurin 21.0.12,org.gradle.java.home 钉死路径)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.11.1(沿用上游 wrapper;2.2.21 线与 8.11/8.14 同代兼容)
kotlinxcoroutines / coroutines-test 1.10.2-1.0.0(ohos 制品线,含 ohosArm64 / ohosX64 变体)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.krumb
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
模块commonMain 文件依赖形态处置
krumb-core8纯 Kotlin + coroutines(StateFlow / Mutex / delay),零 UI、零平台 import、零 expect整体编译(jvm + 双 ohos)
krumb-compose10androidx.compose.* 全量(runtime / animation / foundation / ui / icons)+ 手势迁入 src/composeMain,不进构建
krumb-material32compose 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 方法覆盖内核
KrumbShowkrumbShow(req)ToastController.show / 五类型快捷入口 / showCustom(含 builder 全字段)
KrumbSnapshotkrumbSnapshot()visible StateFlow + pendingSnapshot() + remainingMs 记账
KrumbActionkrumbAction(id)ToastAction.onClick 回调(Kotlin 侧执行)
KrumbPromisekrumbPromise(req)promise 状态机:loading → success/error + rearmTimer
KrumbDismiss / KrumbDismissAllkrumbDismiss(id) / krumbDismissAll()单关 / 清空(含 pending)
KrumbPause / KrumbResumekrumbPause(id) / krumbResume(id)定时器暂停 / 按剩余时长续走
KrumbVersionkrumbVersion()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.tomlKotlin 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.kt9 个 C ABI、快照记账、演示 Action 桩、MiniJson、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出 × 9、两段式字符串转换、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面(轮询 / 六锚点 / 手势 / 转场)
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)
NativeKrumbShow / Snapshot / Action / Promise / Dismiss / DismissAll / Pause / Resume / VersionC ABI JSON 导出 × 9
N-APIkrumbShow(req) / krumbSnapshot() / …向 ArkTS 暴露能力 × 9
ArkTSrefresh() / 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 项用例全部通过(上游原测试文件,零改动):

测试类用例数钉住什么
ConcurrencyTest1并发入队 / 驳回一致性
ToastControllerTest12控制器状态机:五类型入口、builder、dismiss / dismissAll、暂停恢复续走、update
ToastQueueTest11队列抢占矩阵:maxVisible、HIGH 抢占 LOW、同级 FIFO、pending 补位
PromiseTest3loading → 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 功能用例

  1. 启动后版本行显示 krumb-core 0.1.1-ohos.1 · Kotlin/Native + ArkTS shell(来自 krumbVersion() 真实 Native 调用,非静态文字);
  2. 点 Success:顶部中央滑入深色卡片,绿圆 ✓ 图标 + “Profile saved” + 绿色进度条 4s 匀速走完自动消失;
  3. 连点 Info / Error / Warning:三条堆叠(蓝 i / 红 ✕ / 橙 !),各色进度条并行倒数,先到期先消失;
  4. 屏上铺满后继续点击:黄标出现 N pending in queue,前面的消失后 pending 依次补位上屏;
  5. 铺 LOW 后点 High:最旧 LOW 被抢占顶掉,HIGH 立即上屏——抢占语义在真机可见;
  6. 点 Save + Undo 后点卡片上 Undo:自身关闭并弹 info “File restored”(回调在 Kotlin 侧执行);
  7. 点 Promise OK:loading(转圈、无进度条)→ 1.8s 后转绿 “Uploaded report-2026.bin (2.4 MB)”,进度条从头重挂;Promise FAIL 同理转红;
  8. 按住任意卡片:进度条停、出现 ⏸;松开:从剩余时长续走;
  9. 横滑卡片超 60vp:该卡片滑出消失;
  10. 六宫格切换 position:新 toast 落对应锚点(顶部三区向下排 / 底部三区向上排);
  11. 点 Loading:无限时长无进度条,只能手势滑掉或 Dismiss all;
  12. 点 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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :krumb-core 真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. org.gradle.java.home 只管 Gradle 进程内部 JVM,gradlew 启动器仍要 JAVA_HOME——Windows 上不设环境变量会报 JAVA_HOME is not set,退出码还是 0(管道吃掉了),别被假成功骗过;
  4. Kotlin 的 promise(block, …) 里 block 是首参,尾随 lambda 会被绑到 position 上,编译报"Argument type mismatch: Function0 vs ToastPosition"——多参挂起函数一律具名传参;
  5. delay(Int) 编不过,KN 的 delay 只收 Long / Duration——桥里 req.int() 出来的是 Int,记得 toLong();
  6. ArkTS 严格模式 arkts-no-untyped-obj-literals:对象字面量传给自定义方法的 Record 参数不被接受(12 处全报错),内联进 JSON.stringify(...) 即过(SDK 参数位置放行,qrose / KStore 同款惯用法);
  7. ArkTS 组件状态命名不能撞通用属性:@State position 与 CustomComponent 基类的 .position() 冲突报类型不兼容,改名 anchor(协议字段名不受影响);
  8. 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';
  9. KN 返回字符串必须 libohoskrumb_symbols()->DisposeString 归还运行时,N-API 层不能 free();napi_get_value_string_utf8 用两段式(量长再拷)绕 API 26 头文件 const 问题;
  10. CMake 的 rpath 在 Windows 上要防盘符混进 RUNPATH:-Wl,-rpath,\$ORIGIN 转义 + BUILD_WITH_INSTALL_RPATH TRUE + POST_BUILD 把三件套拷到同目录;
  11. DevEco 打开的是 example/harmonyApp 子目录而不是仓库根(根是 Gradle 工程,两套工程互不相干);HAP 打包用 DevEco 自带 JBR,系统 JDK 8 会让 hvigorw assembleHap 半路报不相干的错;
  12. 签名材料由 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.ui ohos 制品与 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 三条经验

  1. 适配边界由库的依赖剖面决定:krumb 引擎半身零 expect、纯 coroutines,ohos 侧一行源码都不用补——比 KStore(8 行 actual)还干净;UI 半身站在 CMP 1.10.3 上,社区 1.9.2 接不住——隔离而非移植。"真编译到哪一层"这个问题,答案永远在依赖矩阵里,不在意愿里;
  2. 钉版由依赖制品的元数据反推:coroutines 1.10 线配 Kotlin 2.2.21(本仓)、1.11 线配 2.4.20(KStore 仓),两条线的 Gradle 经验也不同(2.2 线 8.x 沿用、2.4 线 9.x 沿用)——历史结论不能跨线套用,每次适配先查制品再定工具链;
  3. 有状态引擎的正确桥法是"命令 + 快照":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。

参考文档

Logo

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

更多推荐