本文记录 kotlin-channel-event-bus 接入 OpenHarmony 的完整过程,覆盖事件总线库盘点、鸿蒙 coroutines 制品线选型、真实引擎编译、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、Flow 订阅语义的拉取式收敛、ArkUI 真机页面、HAP 构建和真机验收。

kotlin-channel-event-bus 是基于 kotlinx.coroutines 的 Kotlin Multiplatform 事件总线库:多 Key、多生产者、单消费者,每条总线由一个 Channel 承载——ChannelEvent.Key 标识总线,UNLIMITED(默认)或 CONFLATED 决定缓冲语义,send 同步入队,receiveAsFlow 返回冷流且同一时刻只允许一个收集者,closeKey 前可做存在性 / 收集中 / 非空三项校验,ChannelEventBusLogger 暴露全部内部生命周期。本次适配与社区早期路线(qrose / haze / PreCompose / voyager 的"零依赖内核提取")有本质区别:社区 Maven 上已经有完整的 HarmonyOS kotlinx 制品线(coroutines 1.11.0-0.1.0-07,带 ohosArm64 / ohosX64 Gradle 变体),因此不需要提取内核——真实的引擎(ChannelEventBus、ChannelEvent.Key、容量语义、发送选项、关闭校验、logger)被原样编译成 OpenHarmony KLIB,一个完整的多 Key 事件总线运行在真机上,ArkUI 页面作为验收宿主。它也是这个系列里第一个纯协程语义库:真正难的不是编译,是把 Flow 的订阅语义翻译成能过 C ABI 的形状。

项目地址: AtomGit/oh-tpc/kotlin-channel-event-bus.git

上游仓库: Kotlin-Multiplatform-Foundation/kotlin-channel-event-bus

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

kotlin-channel-event-bus 为 Kotlin Multiplatform 应用提供"往一个 Key 发事件、从同一个 Key 收事件"这一件事的完整答案:ChannelEventBus() 一行建总线,send(event, option) 全生命周期覆盖,receiveAsFlow(key) 让 UI 订阅事件,容量 UNLIMITED / CONFLATED 由 Key 声明,closeKey / close 带校验地收尾,ChannelEventBusLogger 让内部状态可观测。KMP 项目把这些能力全部放在 commonMain,由 Android、iOS、桌面共享。OpenHarmony 应用同样需要这套事件通信能力——页面间结果回传、多步骤流程串联、"只要最新值"的通知,都是事件总线的经典场景。

如果只把示例页面改成 ArkTS,页面可以显示按钮,却无法证明公共引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证事件引擎、coroutines 运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.0.21 / Gradle 8.11.1 不会生成 OpenHarmony 变体;鸿蒙工具链必须走社区 Maven。
依赖矩阵引擎依赖 kotlinx-coroutines-core,上游钉 1.9.0——社区 Maven 上 1.9.0 没有任何 ohos 变体,版本主体必须升到有制品的线;测试依赖 FlowExt 没有 ohos 制品。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
订阅语义跨不了桥receiveAsFlow 返回冷 Flow,只有协程能收集;C ABI 只能同步一来一回,"持续订阅 + 单收集者"没有对应物。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:事件引擎的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。总线语义完全由上游原版实现负责,ArkTS 只负责调用 Native、解析 JSON 和展示结果。

1.2 库提供的能力

Demo 提供一个真实的多 Key 事件总线会话,对应桥的全部入口:

  • 写入(send):bus.send(event, option) 把事件 trySend 进 Key 对应的 Channel;总线不存在时按选项新建(默认)/ 抛异常 / 忽略——三个分支全部可以在页面上触发;
  • 读取(receive):一次拉取 = 对 receiveAsFlow(key) 做一次带超时的冷收集,把当前缓冲的事件一次性带回;UNLIMITED 总线连发 3 条收 3 条,CONFLATED 总线连发 3 条只收最后 1 条;
  • 关闭(closeKey / close):单 Key 关闭可带校验(存在 / 未收集 / 已清空,任一不满足返回库的原始异常文本),或无校验强制关闭;全部关闭对应 bus.close();
  • 可观测(logs):ChannelEventBusLogger 被接进环形缓冲,页面上能读回 bus created / event sent / collection start / collection stop / bus closed 的真实序列;
  • 版本握手(version):cebVersion() 返回 fork 版本号,来自真实 Native 调用而非静态文字。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文(库的异常信息原样透传)。
op / key / capacity回显本次操作、目标总线与容量语义(UNLIMITED / CONFLATED)。
seq / count / eventsping 事件的自增序号;本次拉取的条数与事件数组。

引擎侧完整能力(线程安全的 SynchronizedHashMap 守护、收集标记、单收集者约束、关闭三校验、六种 logger 回调)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 11 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的总线逻辑没有漂移。

1.3 实现适配

维度要求
代码复用channel-event-bus 的全部 commonMain 文件原样编译,不提取、不重写;库内零 expect/actual,适配面比 KStore(8 行 actual)还小——全仓唯一新增 Kotlin 源文件在 example/nativeApp 桥里。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);上游构建文件存 .upstream 副本;sample/、docs/、kotlin-js-store/ 留盘不进构建。
桥接稳定使用 2 个 C ABI、6 个 JSON op 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律coroutines 从上游 1.9.0 升到 ohos 制品线 1.11.0-0.1.0-07(无 1.9.0 ohos 制品,本仓唯一"换线且升版"处),上游测试零改动全绿兜底。
UI 完整单页覆盖全部桥接入口:双 Key 切换、载荷输入、发送选项、拉取、关闭校验、内部日志和版本握手。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。kotlin-channel-event-bus 本身不碰 UI,适配面天然最小;ArkTS Demo 只是把桥暴露的总线会话画出来。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认 coroutines ohos 制品线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版、coroutines 升线
第 3 阶段:桥协议建模     ── 6 个 JSON op、Ping/Pong 双事件类型、订阅收敛为拉取、错误通道
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 三 Tab 页面、发送/拉取/关闭、内部日志和版本握手
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
channel-event-bus/                   上游事件引擎(进 ohos 构建,jvm + ohosArm64 + ohosX64)
  src/commonMain/.../                7 个文件:ChannelEvent / ChannelEventBus / Capacity /
                                     Exception / Logger / Option / Validation(零改动)
  src/commonTest/ + src/jvmTest/     上游测试(零改动,JVM 交叉验证闸门)
example/nativeApp/                   Kotlin/Native 动态库(Bridge + Events + MiniJson)
example/harmonyApp/                  DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md       鸿蒙适配说明(中英双份)
README.upstream.md / *.upstream      上游 README 与构建文件原文保存
sample/ docs/ kotlin-js-store/       上游示例与文档站,留盘不进 settings

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :channel-event-bus 与 :example:nativeApp,rootProject.name = "ohos_channeleventbus"。上游的 detekt / spotless / kover / dokka / vanniktech 发布插件、binary-compatibility validator、AGP classpath 全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
JDK21(org.gradle.java.home 指向本机 JDK 21)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle9.6.1(上游 wrapper 8.11.1 升级;2.4.20 线支持 Gradle 9.x)
kotlinxcoroutines / coroutines-test 1.11.0-0.1.0-07(ohos 制品线)
测试补充io.github.hoc081098:FlowExt:1.0.0(上游 commonTest 依赖,仅 JVM 解析)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.channeleventbus
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块commonMain 文件依赖形态处置
channel-event-bus7纯 Kotlin + coroutines-core,零 UI、零平台源码集、零 expect/actual整体编译(jvm + 双 ohos)
sample/standalone-androidApp—AGP、Compose、ViewModel留盘不进构建
sample/standalone-composeMultiplatform—AGP、Compose Multiplatform留盘不进构建

盘点结论直接决定路线:上游把 Android / iOS / 桌面 / Web 全部目标跑在同一份 commonMain 上,没有一处 expect——意味着鸿蒙侧连一个 actual 都不用补。KStore 需要补 8 行 dispatcher actual,本仓的库内新增源码是 0 行;整个 fork 的新增 Kotlin 源文件只存在于 example/nativeApp 的桥接层。挡住这类库适配的从来不是源码,是两件事:coroutines 有没有 ohos 制品(第 2 阶段验证),以及 Flow 怎么过桥(第 3 阶段解决)。

值得单独说明的是库内唯一一处协程内部 API:ChannelEventBusImpl 用 kotlinx.coroutines.internal.SynchronizedObject + synchronized 守护 _busMap(@OptIn(InternalCoroutinesApi::class))。这是 1.9.0 的源码,升到 1.11.0-0.1.0-07 线后原样可用——双 ohos 目标编译通过即是最直接的证据。

1.4 页面边界
标题区          KOTLIN-CHANNEL-EVENT-BUS · OPENHARMONY / 副标题(KMP 事件总线库 · Kotlin/Native ohosArm64/ohosX64)
Tab 切换        事件收发 / 总线日志 / 关于
Key 区          ping · UNLIMITED / pong · CONFLATED 双选片(每条 Key 一条独立总线)
载荷区          事件内容输入框(默认 hello from ArkTS)
选项区          send 不存在行为三选(新建/抛异常/忽略)+ closeKey 校验二选(无校验/ALL)
操作区          发送(深绿) / 接收(拉取)(蓝) / 关闭当前 Key(红) / 关闭全部
状态行          最近一次操作结果
事件区          最近收到的事件列表(最新在前,上限 20 条)
日志区          桥调用日志(→ 请求 / ← 响应)+ 引擎内部 logger 记录
关于区          版本握手、工具链版本、架构分层、验收要点、仓库与社区链接

页面启动时先调 cebVersion() 做握手,再经 version op 取工具链版本。

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

2.1 制品线验证(本适配的成败手)

先查社区 Maven(https://maven.eazytec-cloud.com)上 coroutines 的可用版本。maven-metadata.xml 给出两个关键结论:

kotlinx-coroutines-core  ohos 线从 1.10.2-0.3.0-04 起步,最新 1.11.0-0.1.0-07
                          (.module 元数据带 ohosArm64Api-published / ohosX64Api-published)
1.9.0(上游钉的版本)      没有任何 ohos 变体

与 KStore 的"换线不换版"不同,本仓必须换线且升版:版本主体从 1.9.0 升到 1.11.0,后缀钉 -0.1.0-07。选最新线而不是 1.10.2-x 的理由:1.11.0-0.1.0-07 依赖 stdlib 2.4.20-0.1.0-07,与 KStore 已过审的工具链完全同线,本机 Konan 已预下载该发行版,不需要再引入一条未经验证的组合。这条偏离用上游测试兜底:测试文件零改动,在 1.11.0 线上 JVM 全绿(见 6.2 节)。

测试依赖 FlowExt(上游作者自己的库)在 Maven Central 上只有 js/jvm/native/apple 变体、没有 ohos 变体,处置见 2.4 节。

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

社区此前批准的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 用的)。但 coroutines 1.11.0-0.1.0-07 依赖 kotlin-stdlib 2.4.20-0.1.0-07(元数据 2.4.0),Kotlin 2.2.21 只能读到 2.3.0,编译期会直接报 incompatible metadata。工具链版本跟着依赖走:Kotlin 钉 2.4.20-0.1.0-07,Gradle 从上游 8.11.1 升到 9.6.1(2.4.20 线支持 Gradle 9.x,KStore / compottie 先例;只有 2.2.21 旧线才必须降 8.14.1,两条工具链线的经验不能互相套用)。

settings.gradle.kts 把 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()
    }
}

rootProject.name = "ohos_channeleventbus"

include(":channel-event-bus")
include(":example:nativeApp")

版本目录收敛为三条线:

[versions]
kotlin = "2.4.20-0.1.0-07"
coroutines = "1.11.0-0.1.0-07"
flowExt = "1.0.0"
2.3 编译真实引擎

channel-event-bus/build.gradle.kts 收敛为纯 KMP 模块(上游文件存 .upstream):

plugins {
    alias(libs.plugins.kotlin.multiplatform)
}

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)

    jvm()
    ohosArm64()
    ohosX64()

    sourceSets {
        commonMain.dependencies {
            api(libs.coroutines.core)
        }
        commonTest.dependencies {
            implementation(kotlin("test"))
            implementation(libs.coroutines.test)
            implementation(libs.flow.ext)
        }
    }
}

实测:

./gradlew :channel-event-bus:compileKotlinOhosArm64 :channel-event-bus:compileKotlinOhosX64   # BUILD SUCCESSFUL

引擎的 7 个 commonMain 文件一个没动,库内新增源码 0 行。

2.4 测试依赖的处置:FlowExt 没有 ohos 制品

commonTest 的依赖表对 ohos 测试编译同样可见,FlowExt 解析不到 ohos 变体就会卡死 compileTestKotlinOhosArm64。处置是两条纪律的组合:测试统一在 JVM 跑(jvmTest),根构建全局禁用 ohos 测试任务:

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

这样上游测试文件零改动保留,ohos 侧只承担编译与链接——与 KStore 对 turbine 的处置同款。

第 3 阶段:桥协议建模

3.1 JSON 契约
com.hoc081098.channeleventbus/**(上游引擎,原样编译)
        ↓ send / receiveAsFlow / closeKey / close / logger
Bridge.kt(@CName × 2,扁平 JSON 进出)
        ↓ UTF-8 JSON
N-API cebDispatch() / cebVersion()
        ↓
ArkUI Index.ets(三 Tab 滚动页)

请求与响应:

发送   : {"op":"send","key":"ping","value":"hello","when":"create"}
响应   : {"ok":true,"op":"send","key":"ping","when":"create","capacity":"UNLIMITED","seq":1}

拉取   : {"op":"receive","key":"ping","timeout":80}
响应   : {"ok":true,"op":"receive","key":"ping","capacity":"UNLIMITED","count":2,
          "events":[{"seq":1,"message":"hello"},{"seq":2,"message":"hello"}]}

关闭Key : {"op":"closeKey","key":"ping","validations":"all"}
响应   : {"ok":true,...} 或 {"ok":false,"error":"Bus by key=... is not empty, ..."}
全部关 : {"op":"close"}        日志: {"op":"logs","max":30}      版本: {"op":"version"}
3.2 载荷类型为什么固定 Ping / Pong 两种

桥内事件类型这样定:

class PingEvent(val seq: Int, val message: String) : ChannelEvent<PingEvent> {
    override val key get() = Key
    companion object Key : ChannelEventKey<PingEvent>(PingEvent::class)          // UNLIMITED(默认)
}

class PongEvent(val message: String) : ChannelEvent<PongEvent> {
    override val key get() = Key
    companion object Key : ChannelEventKey<PongEvent>(PongEvent::class, ChannelEventBusCapacity.CONFLATED)
}

两个设计约束决定了这个形状。其一,ChannelEvent.Key 的等价性由 eventClass + capacity 决定——同 class 同容量的两个 Key 是同一条总线,所以不同 Key 必须用不同 class(或不同容量),不能靠"同类型多实例"区分。其二,C ABI 传不了 Kotlin 类型系统,桥只能为有限的事件类型做映射;演示两种容量语义各配一个类型,seq 由 AtomicInt 自增、message 由页面输入,toJson() 序列化过桥。业务接入自定义事件时,照 Events.kt 加类、在 Bridge.kt 的分支里注册 Key 即可。

3.3 receive 为什么是"拉取"而不是"订阅"(本适配的技术核心)

receiveAsFlow 的 Flow 只有在总线被 close 时才正常完结;冷流 + 单收集者的语义在 C ABI 上没有对应物。桥的处理:

val events = ArrayList<String>(8)
runBlocking {
    withTimeoutOrNull(timeout.toLong()) {          // timeout 10..500ms,默认 80
        when (name) {
            "ping" -> bus.receiveAsFlow(PingEvent.Key).collect { events.add(it.toJson()) }
            "pong" -> bus.receiveAsFlow(PongEvent.Key).collect { events.add(it.toJson()) }
        }
    }
}

一次 receive = 一次带超时的冷收集:把当前缓冲的事件全部收走,超时后取消。取消会走进 receiveAsFlow 内部的 catch——markAsNotCollecting 后重抛,与"收集协程被取消"走完全相同的生命周期路径;下一次 receive 再开新的收集。这样设计换来三个可验收的语义:

  • UNLIMITED 总线:连发 3 条,一次拉取返回 3 条(缓冲语义可见);
  • CONFLATED 总线:连发 3 条,一次拉取只返回最后 1 条(覆盖语义可见);
  • 两次拉取之间总线处于"未收集"状态,closeKey(ALL) 的 BusIsNotEmpty / BusDoesNotExist 校验可以真实触发。

这是 KStore 适配中"updates Flow 不跨 C ABI,操作后读回"纪律的推广:订阅式语义收敛成拉取式语义,且收敛过程不丢任何一条可验收的行为。suspend 统一 runBlocking 收进同步 C ABI——鸿蒙线 Dispatchers.Main 的跨线程派发有已知问题(resaca 仓适配时记录过),桥全程不碰 Main。

3.4 错误通道

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

@CName("CebDispatch")
fun cebDispatch(request: String): String = try {
    /* op 分发 */
} catch (t: Throwable) {
    """{"ok":false,"op":"dispatch","error":${jsonString(t.message ?: t.toString())}}"""
}

非法 op、未知 key、BusDoesNotExist / BusIsNotEmpty / BusIsCollecting 等库异常,全部走同一个 ok:false 通道,库的异常文本原样透传,ArkTS 侧统一渲染——页面看到的错误就是引擎吐的错误。

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

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

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复刻总线语义,所以选择 C ABI + JSON。

4.2 Kotlin/Native 导出函数
@CName("CebDispatch")
fun cebDispatch(request: String): String   // send / receive / closeKey / close / logs / version 一口进

@CName("CebVersion")
fun cebVersion(): String = "0.1.1-ohos.1"  // 版本握手
C ABI 符号ArkTS 方法覆盖内核
CebDispatchcebDispatch(req)send(event, option) + receiveAsFlow(拉取式)+ closeKey(validations) + close + logger 读回
CebVersioncebVersion()fork 版本握手(真机版本行的数据来源)
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"cebDispatch", nullptr, NapiCebDispatch, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"cebVersion", nullptr, NapiCebVersion, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr}};

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

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

add_library(ceb_napi SHARED napi_init.cpp)
target_link_libraries(ceb_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohosceb)

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

export const cebDispatch: (request: string) => string;
export const cebVersion: () => string;

第 5 阶段:ArkUI 宿主

5.1 握手和触发操作
aboutToAppear(): void {
  this.handshake = cebVersion();               // 版本握手:来自真实 Native 调用
  const req = new CebRequest();
  req.op = 'version';
  const res = this.dispatch(req);              // 顺带取工具链版本
  this.kotlinLine = res.kotlin ?? '';
  this.coroutinesLine = res.coroutines ?? '';
}

private send(): void {
  const req = new CebRequest();                // 显式类,ArkTS 禁止未类型化对象字面量
  req.op = 'send';
  req.key = this.busKey;
  req.value = this.message;
  req.when = this.whenOption;
  this.apply(this.dispatch(req));              // @State 刷新状态行 + 日志
}
5.2 页面交互

事件收发 Tab 自上而下:Key 双选片(带容量标注)、载荷输入框、send 行为三选、发送(深绿)/接收(拉取)(蓝)双主按钮、closeKey 校验二选、关闭当前 Key(红)/关闭全部(灰字红)双按钮、状态行与最近事件列表(最新在前,首条绿底高亮)。总线日志 Tab 分两段:桥调用日志(→ 请求 / ← 响应,带 [HH:mm:ss] 时间戳,失败条目红字红底)和引擎内部 logger 记录(青字绿底,logs op 读回)。关于 Tab 展示版本握手行、工具链版本、ArkTS → NAPI → C ABI → Kotlin 引擎 → kotlinx-coroutines Channel 分层说明、五条验收要点与仓库 / 社区链接。

5.3 ArkTS 严格模式的三个坑(真实踩过)

首版页面在 DevEco 里编译报了 10 个错误,全部来自 ArkTS 严格模式,值得记录:

错误处置
arkts-no-untyped-obj-literals ×6:this.dispatch({op:...}) 这类对象字面量被禁定义显式 CebRequest 类(字段带默认值),6 个调用点全部 new CebRequest() + 逐字段赋值;未赋值字段(空串/0)桥侧有默认值兜底
Property 'key' ... not assignable to ... 'CustomComponent':@State key 与组件基类属性冲突改名 @State busKey,所有引用同步更新
Property 'fontSize' does not exist on type 'ColumnAttribute' ×2fontSize/fontColor 只存在于文本组件,从 Column 容器移到内部每个 Text 上

修复只动 ArkTS 页面,不涉及任何 .so 与 Kotlin 源码——这也是桥式架构的好处:Native 层与 UI 层可以独立修复、独立回归。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohosceb)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/org/terminator/ohos/channeleventbus/
│       ├── Bridge.kt                         @CName × 2 + 6 个 op + 环形日志 logger
│       ├── Events.kt                         PingEvent(UNLIMITED) / PongEvent(CONFLATED)
│       └── MiniJson.kt                       扁平 JSON(继承自 cmp_qrose 审核模式)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.channeleventbus、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 导出、参数检查、DisposeString
        ├── src/main/cpp/include/libohosceb_api.h
        ├── src/main/cpp/types/libceb_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 验收页面(三 Tab)
6.2 构建、签名和安装
./gradlew :channel-event-bus:jvmTest                                            # 11 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

# DevEco Studio 打开 example/harmonyApp(不要打开仓库根目录)
# File → Project Structure → Signing Configs 勾选自动生成签名
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon

签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填带 material 的完整配置,提交前用 git update-index --skip-worktree 保护)。

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.channeleventbus -m entry

四、完整代码对照

4.1 整体架构

com.hoc081098.channeleventbus/**(上游引擎原样编译)
    │ ChannelEventBus / ChannelEvent.Key / 容量 / 选项 / 校验 / Logger
Bridge.kt + Events.kt + MiniJson.kt
    │ CebDispatch / CebVersion
libceb_napi.so
    │ cebDispatch / cebVersion
Index.ets
    │ JSON parse + @State 更新
    ▼
ArkUI 真机页面(三 Tab 滚动)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_channeleventbus、sample 移出
gradle/libs.versions.tomlKotlin 2.4.20-0.1.0-07 + coroutines 1.11.0-0.1.0-07 + FlowExt 钉版
build.gradle.kts(根)版本 0.1.1-ohos.1、全局禁用 Ohos*Test* 任务
channel-event-bus/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64(上游文件存 .upstream)
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt2 个 C ABI、6 个 op、runBlocking + withTimeoutOrNull 拉取、环形日志
nativeApp/.../Events.ktPingEvent / PongEvent 演示事件(双容量语义)
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面(三 Tab)
README.OpenHarmony_CN.md鸿蒙适配主文档(17 节)

4.3 关键 API 对照

层次API作用
引擎ChannelEventBus(logger)建总线实例,logger 可观测内部生命周期
引擎send(event, option)同步 trySend;总线不存在时按 OptionWhenSendingToBusDoesNotExist 三选
引擎receiveAsFlow(key)冷 Flow、单收集者;桥上收敛为带超时的拉取
引擎closeKey(key, validations)关闭前校验(存在 / 未收集 / 已清空)
引擎close()全部关闭,无校验
引擎ChannelEventBusCapacityUNLIMITED(默认缓冲全部)/ CONFLATED(只留最新)
NativeCebDispatch / CebVersionC ABI JSON 导出
N-APIcebDispatch(req) / cebVersion()向 ArkTS 暴露能力
ArkTSsend() / receive()触发操作并刷新 UI

4.4 ArkTS 与 Kotlin 的边界

const req = new CebRequest();
req.op = 'send';
req.key = 'ping';
req.value = 'refresh-list';
req.when = 'create';
const res = JSON.parse(cebDispatch(JSON.stringify(req))) as CebResponse;
// res.ok === true, res.seq === 1

const pull = new CebRequest();
pull.op = 'receive';
pull.key = 'ping';
pull.timeout = 80;
const recv = JSON.parse(cebDispatch(JSON.stringify(pull))) as CebResponse;
// recv.count === 1, recv.events[0] === {"seq":1,"message":"refresh-list"}
bus.send(PingEvent(seq, value), option)                    // trySend 进 UNLIMITED Channel
runBlocking {
    withTimeoutOrNull(80) {
        bus.receiveAsFlow(PingEvent.Key).collect { events.add(it.toJson()) }
    }
}                                                          // 超时取消 → markAsNotCollecting

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

五、关键决策说明

决策 1:真编译整库而不是提取零依赖内核

早期仓库抽内核,是因为 ohos 上没有它们依赖的 kotlinx 制品。本次适配先验证制品线(coroutines 1.11.0-0.1.0-07 存在 ohos 双变体),障碍已消失——再抽内核等于把约 290 行的 ChannelEventBusImpl(SynchronizedHashMap 守护、收集标记、关闭校验)手抄一遍,漂移风险反而更大。本仓库内零 expect/actual,真编译让多 Key 隔离、容量语义、单收集者约束全部是上游原件。

决策 2:coroutines 1.9.0 → 1.11.0-0.1.0-07,Kotlin 钉 2.4.20-0.1.0-07

KStore 的纪律是"换线不换版",本仓只能走一半:1.9.0 在社区 Maven 上没有任何 ohos 变体,版本主体必须升。选 1.11.0-0.1.0-07 是因为它反推的工具链(Kotlin 2.4.20-0.1.0-07、Gradle 9.6.1)与 KStore 完全同线,是已过审组合。偏离的代价用上游测试兜底:测试文件零改动、JVM 全绿(含使用 InternalCoroutinesApi 的 SynchronizedObject 路径),即"升版不换语义"的证据。

决策 3:根构建替换(cmp_qrose 模式),上游构建存 .upstream

上游根构建(detekt / spotless / kover / dokka / vanniktech / binary-compatibility validator / AGP classpath)对鸿蒙没有保留价值,整个替换为极简 ohos 构建,原文件保存为 .upstream 副本 5 份 + README.upstream.md。sample/ 两个 Android 示例与 docs/、kotlin-js-store/ 留盘不进 settings.gradle.kts——它们的存在方式只有这一种。

决策 4:订阅收敛为拉取(本系列的方法论增量)

Flow 过不了 C ABI,但"拉取"能:一次 receive 就是一次带超时的冷收集,取消路径与真实协程取消完全一致。这个收敛保住了三条可验收语义(UNLIMITED 缓冲、CONFLATED 覆盖、关闭校验在"未收集"状态下可触发),是 KStore"updates 不跨桥、操作后读回"纪律在事件类库上的推广。代价是拉取窗口(10…500ms)同步占用调用线程,Demo 量级无感,生产化方向是 N-API 异步回调。

决策 5:jvmTest 交叉验证 + 测试零改动

引擎逻辑在 commonMain,JVM 与 ohos 跑同一份代码;jvmTest 跑的就是上游原测试文件,是"内核没漂"的闸门。FlowExt 没有 ohos 制品,处置不是改测试,而是"测试统一 JVM + 根构建禁用 Ohos*Test*"——测试文件一个字节不动,交叉验证的有效性才成立。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x64)、JDK 21、Gradle 9.6.1、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与 HarmonyOS 设备(arm64)。

6.2 JVM 交叉验证

./gradlew :channel-event-bus:jvmTest

11 项用例全部通过(上游原测试文件,零改动;1 项为平台专属跳过):

测试类钉住什么
ChannelEventBusTestsend/receive 状态机、多 Key 隔离、容量语义、单收集者、关闭校验
ChannelEventTestChannelEvent.Key 等价性(eventClass + capacity)、channelEventKeyOf
ChannelEventBusJvmTestJVM 平台专属行为(跳过项在此类)

依赖换线升版没有 API 漂移:coroutines 1.9.0 → 1.11.0-0.1.0-07 后上游测试照跑全绿——这正是"升版不换语义"的直接证据。

6.3 链接与 HAP 验证

:channel-event-bus:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL(KLIB)
:channel-event-bus:jvmTest: 11 项全绿
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: CebDispatch / CebVersion / libohosceb_symbols 均为全局 T 符号(双 ABI)
CompileArkTS: finished(修复 3 类严格模式错误后)
PackageHap: finished
BUILD SUCCESSFUL / install bundle successfully / start ability successfully

产物:libohosceb.so arm64 约 5.0MB、x86_64 约 4.5MB(debugShared 未 strip,含 Kotlin/Native 运行时与 coroutines),配 libc++_shared.so 约 1.8MB;arm64 .so 动态符号表共 399 个全局 T 符号。

6.4 功能用例

  1. 启动后「关于」Tab 出现版本握手 0.1.1-ohos.1 与工具链版本行(来自真实 Native 调用,非静态文字);
  2. 选 ping(UNLIMITED)连发 2 条,点「接收(拉取)」返回 2 条事件,seq 连续递增,状态行显示 收到 2 条事件(ping · UNLIMITED)——缓冲语义成立(见效果图);
  3. 选 pong(CONFLATED)连发 3 条再拉取,只返回最后 1 条——覆盖语义成立;
  4. 关闭 Key 后把 send 行为切到「不存在则抛异常」再发送,返回 ok:false 且 error 为库的 BusDoesNotExist 原文;切回「新建」再发则自动重建总线;
  5. ping 有未消费事件时用「严格校验(ALL)」关闭,返回 BusIsNotEmpty;先拉取清空再关则成功;
  6. 「总线日志」Tab 刷新后,bus created / event sent / collection start / collection stop 序列与操作一一对应——ChannelEventBusLogger 的真实回调。

6.5 验证结论

事件引擎、coroutines 运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在 HarmonyOS 设备上成功安装并进入前台,页面展示了真实的总线会话:版本握手、双容量语义的发送与拉取、发送选项与关闭校验的错误通道、内部生命周期日志(见下节效果图)。

七、运行效果

7.1 运行效果图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/kotlin-channel-event-bus.git
cd kotlin-channel-event-bus
export JAVA_HOME="/path/to/jdk-21"
./gradlew :channel-event-bus:compileKotlinOhosArm64 :channel-event-bus:compileKotlinOhosX64   # klib
./gradlew :channel-event-bus:jvmTest                                                          # 11 项交叉验证
./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.channeleventbus -m entry
hdc shell hilog | grep -E "CebNapi|CebDemo"

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :channel-event-bus 真正编译成 ohosArm64 / ohosX64 产物;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. coroutines 版本主体写对、后缀写错照样解析失败:必须钉 1.11.0-0.1.0-07,1.9.0 无 ohos 变体;
  4. Kotlin 钉版由 coroutines 制品线的 stdlib 元数据反推(2.4.20-0.1.0-07),qrose 时代的 2.2.21 钉法读不了 2.4 元数据;Gradle 经验按工具链线区分(2.4.20 线走 9.x);
  5. ArkTS 严格模式三连:未类型化对象字面量被禁(要用显式 CebRequest 类)、@State key 与 CustomComponent 基类属性冲突(改名 busKey)、fontSize/fontColor 不能挂在 Column 容器上(要挂到内部 Text)——共 10 个编译错误,全部页面侧修复,Native 层零改动;
  6. FlowExt 没有 ohos 制品:测试统一 JVM 跑 + 根构建全局禁用 Ohos*Test* 任务,上游测试文件零改动;
  7. 鸿蒙线 Dispatchers.Main 跨线程派发有已知问题(resaca 记录),桥用 runBlocking 收 suspend,全程不碰 Main;
  8. 根 .gitignore 的 build/ 是非锚定模式能盖住嵌套,但 DevEco 的 oh_modules / .hvigor / entry/libs / .cxx 要单独追加,另把 DevEco 自动生成的 .clang-tidy / .clangd 一并忽略,保持与审核标准仓库同构;
  9. build-profile.json5 的 signingConfigs 条目缺 material 字段会被 hvigor schema 拒绝——仓库内留空数组且 product 不引用签名,勾选自动签名后由 IDE 回填;
  10. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可;
  11. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  12. 签名材料由 DevEco 自动写入 build-profile.json5,提交前确认暂存内容里 signingConfigs 仍为空数组,证书绝不进入 AtomGit。

8.2 已知问题

  • 持续订阅不跨 C ABI:桥是同步一次一调用,事件到达即推送(N-API 线程安全函数)不在此版,ArkTS 侧以拉取观察状态;
  • 桥内事件类型固定 PingEvent / PongEvent 两种演示类型;ChannelEvent.Key 等价性由 eventClass + capacity 决定,业务扩展务必用独立 class 声明新 Key;
  • coroutines 版本主体从上游 1.9.0 升至 1.11.0(无 1.9.0 ohos 制品),以 JVM 交叉验证全绿作为无漂移证据;
  • 拉取窗口(默认 80ms)同步占用调用线程,Demo 量级无感,生产化需异步化;
  • ChannelEventBusException.SendException.BusDoesNotExist 继承自 CloseException 是上游源码现状,桥按原文透传错误信息;
  • js / wasmJs / apple / linux / windows / androidNative 目标剥离(源文件留在树里),发布、dokka、kover 一并剥离,本 fork 不发 Maven Central;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.channeleventbus 匹配的签名。

8.3 未来优化方向

  • 用 N-API 线程安全函数把 receive 异步化(回调推送),去掉同步等待窗口;
  • 桥支持注册自定义事件类型(key → serializer 注册表),业务侧不改 Kotlin 也能扩展;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查(llvm-nm 断言 3 个符号)和 HAP 构建;
  • 上游 0.1.1 正式发布后对齐版本号主体。

九、总结

9.1 核心难点回顾

上游 channel-event-bus(7 个 commonMain 文件,零 expect/actual)
    → 验证 coroutines ohos 制品线(1.9.0 无制品 → 升 1.11.0-0.1.0-07)
    → 工具链对齐 Kotlin 2.4.20-0.1.0-07(Gradle 8.11.1 → 9.6.1)
    → 根构建替换(cmp_qrose 模式,上游构建存 .upstream)
    → ohosArm64 / ohosX64 KLIB + 库内零新增源码
    → C ABI(@CName × 2)+ 6 个 JSON op
    → receiveAsFlow 收敛为带超时的拉取(订阅语义过桥)
    → C++ N-API(DisposeString 纪律)
    → ArkTS JSON 解析(严格模式三坑修复)
    → ArkUI 真机三 Tab 验收(双容量语义 / 选项 / 校验 / 日志)

9.2 封装层次

  • 引擎层:上游原版 ChannelEventBus、多 Key Channel、容量语义、单收集者冷流、线程安全守护;
  • Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理页面状态、显式请求类构造、操作分发、日志与错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 制品线决定路线,但制品线没有你要的版本时,用"升版 + 交叉验证"打开局面:KStore 恰好同版本可换线,本仓的 1.9.0 无 ohos 制品——先查 maven-metadata.xml 认清现实,选与已过审工具链同线的制品版本,再用零改动的上游测试全绿兜底语义;路线选择必须基于当天的制品可达性;
  2. 工具链版本跟着依赖走:Kotlin 钉版由 coroutines 制品线的 stdlib 元数据反推(2.4.20-0.1.0-07),Gradle 经验按工具链线区分(2.4.20 线走 9.x,2.2.21 线降 8.14.1),两条线的经验不能互相套用;
  3. 协程语义库的适配核心是"语义过桥"而不是编译:本仓编译面是系列里最小的(零 expect/actual、零新增库内源码),真正的工作量在把"冷流 + 单收集者 + 持续订阅"收敛成"带超时的拉取",且收敛过程保住全部可验收行为(缓冲、覆盖、校验、生命周期日志)——桥的形状由库的语义决定,不是反过来。

9.4 适配成果

当前适配已完成:

  • kotlin-channel-event-bus 0.1.1-SNAPSHOT 真实引擎的双 ABI Kotlin/Native 编译(上游 commonMain 文件零重写,库内新增源码 0 行);
  • HarmonyOS coroutines 制品线在事件总线库上的完整落地(1.11.0-0.1.0-07,从上游 1.9.0 升线,JVM 交叉验证兜底);
  • 2 个 C ABI 符号、6 个 JSON op 和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • 订阅语义的拉取式收敛(带超时冷收集,取消路径与协程取消一致);
  • DevEco Stage ArkUI 三 Tab 验收页面(双 Key 容量语义、发送选项、关闭校验、内部日志、版本握手);
  • 11 项 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

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

更多推荐