本文记录 Enro 接入 OpenHarmony 的完整过程,覆盖导航库盘点、真编译路线选型、依赖双线钉版、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、无头平台宿主、ArkUI 真机页面、HAP 构建和真机验收。

Enro 是基于 Compose Multiplatform 的导航库:页面即函数,@Serializable 的 NavigationKey 是页面签名,引擎统一管理容器、回退栈、结果通道、深链与状态恢复。本次适配与社区早期路线(qrose / haze 的"零依赖内核提取")有本质区别:compose.runtime / lifecycle / savedstate / coroutines / serialization 的 ohos 制品线在社区 Maven 上已经齐套,因此不需要提取内核——真实的导航引擎(EnroController、NavigationContainer、NavigationHandle、结果通道、路径解析、SavedState 序列化)的 commonMain 源码一行未改,原样编译成 OpenHarmony KLIB,一个完整的导航状态机运行在真机上,ArkUI 页面作为验收宿主。Compose UI 渲染层(enro-ui)按社区已审模式留盘隔离,等社区 CMP UI 制品与上游版本对齐后接回。

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

上游仓库: isaac-udy/Enro

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

Enro 为 Compose Multiplatform 应用提供"导航"这一件事的完整答案:@NavigationDestination 声明目的地,NavigationContainer 持有回退栈并按过滤器路由操作,NavigationKey.WithResult<T> 声明带结果返回的页面签名,EnroController 统一装配模块(绑定、序列化器、拦截器、路径、装饰器),深链走 NavigationPathBinding,进程死亡恢复走多态 SavedState 编码。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套导航能力,但上游构建(Kotlin 2.4.0 / Gradle 9.5.0 / AGP 9.2.1)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示按钮,却无法证明公共导航引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证导航引擎、Compose runtime 状态系统、lifecycle/viewmodel/savedstate 栈、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.4.0 / Gradle 9.5.0 / AGP 9.2.1 与鸿蒙工具链无法共存于一个 Gradle 构建;旧钉法 Kotlin 2.2.21-1.0.0 读不了 2.4 元数据。
依赖矩阵引擎依赖 compose.runtime / lifecycle / savedstate / coroutines / serialization 五件套,每一个都要有 ohos 变体才能解析;上游 Compose 1.11 与社区 ohos 线 1.9.2 还存在版本代差。
平台宿主缺失Enro 每个平台一个根宿主(desktop RootWindow / iOS EnroUIViewController / web EnroBrowserRoot),ohos 需要同位的新宿主,且不能依赖 Compose UI。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

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

1.2 库提供的能力

Demo 提供一个真实的导航会话,对应桥接的全部入口:

  • 容器路由:bootstrap 装配真实 EnroOhosHost,注册 main(接受除 modal 键外的一切)与 modal(只接受 SheetKey)双容器,findContainerForOperation 按过滤器把操作路由到正确的容器;
  • 回退栈操作:open / close / replace 全部经根 NavigationHandle 走引擎完整操作管线(拦截器、空栈拦截、结果登记),不走捷径;
  • 结果回传:completeFrom 打开 SearchKey : NavigationKey.WithResult<String>,complete 携带结果完成,结果进入引擎的 NavigationResultChannel.pendingResults;
  • 深链:openFromPath 把 /detail/42 经 PathRepository 解析成 DetailKey 再打开,pathFor 反向生成路径,与 Android intent 深链同一条代码路径;
  • 状态恢复:save 把回退栈经多态 SavedState 编码存成进程内令牌,clear 清空后 restore 让栈原样回来——与 rememberNavigationContainer 的 Saver 同一条代码路径;
  • 版本握手:enroVersion() 返回 fork 版本号,来自真实 Native 调用而非静态文字。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文(含 cause 链)。
op回显本次操作。
containers / activeContainer / top全部容器、回退栈条目({key, id, args})、活跃容器与栈顶。
results引擎 pendingResults 通道快照。
token / restored保存令牌与恢复结果。

引擎侧完整能力(操作管线、拦截器、ViewModel 作用域装饰器、路径匹配优先级、Metadata)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 168 项交叉验证用例,证明 ohos .so 里执行的导航逻辑没有漂移。

1.3 实现适配

维度要求
代码复用enro-annotations / enro-common / enro-runtime / enro-test 的全部 commonMain 文件原样编译,不提取、不重写;仅新增平台 actual(ohosMain ×10、jvmMain ×8)与无头宿主 EnroOhosHost。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 desktop/jvm 目标作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);Compose UI 渲染层(enro-ui)、Android 专用(enro-compat/enro-lint)、JS 专用(enro-html)、KSP 处理器、buildSrc 留盘不进构建。
桥接稳定使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律commonMain 声明上游 plain 精确版本(JVM 交叉验证与上游 CI 一致),ohos 编译配置 force 社区 OH 线;唯一无 ohos 制品的 urlencoder 以源码内置。
UI 完整单页四 Tab 覆盖全部桥接入口:导航栈、操作/结果、深链、保存恢复,附引擎日志区。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。Enro 的 UI 渲染层(enro-ui 的 NavigationDisplay)依赖 Compose UI/animation/navigationevent,当前无 ohos 可解析制品,按 cmp_qrose 的 composeMain 隔离模式留盘;ArkTS Demo 只是把桥暴露的导航状态画出来。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖矩阵、expect/actual 清单、OH 制品线逐个核验
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版、依赖双线
第 3 阶段:平台宿主       ── EnroOhosHost 无头宿主 + 七个 expect 的 ohosMain actual
第 4 阶段:桥协议建模     ── 14 个 op 的 JSON 契约、错误通道、demo 模块(双作用域序列化注册)
第 5 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 6 阶段:ArkUI 宿主     ── 四 Tab 验收页、启动握手、日志区
第 7 阶段:示例与验证     ── HAP、签名、真机安装运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
enro-annotations/                    纯 Kotlin 注解(进 ohos 构建,nativeMain actual 天然覆盖)
enro-common/                         keys/backstack/path 解析(进 ohos,+2 个 ohosMain actual)
enro-runtime/                        导航引擎 131 个 commonMain 文件(进 ohos,+10 个 ohosMain 文件)
enro-test/                           测试夹具 commonMain(进构建,供 JVM 交叉验证)
urlencoder/                          新增:上游 urlencoder-lib 1.6.0 源码内置(唯一依赖适配)
example/nativeApp/                   Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/                  DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md       鸿蒙适配说明(中英双份)
buildSrc.upstream/                   上游约定插件,改名留盘(buildSrc 无法从构建排除)
enro/ enro-ui/ enro-compat/ enro-html/ enro-lint/ enro-processor/
recipes/ tests/ .run/ .github/       上游遗留,留盘不进 settings.gradle.kts

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :urlencoder、:enro-annotations、:enro-common、:enro-runtime、:enro-test 与 :example:nativeApp,rootProject.name = "ohos_enro"。上游的 AGP classpath、dokka、vanniktech 发布、emulator.wtf 设备矩阵全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
JDK21(org.gradle.java.home 写进 gradle.properties)
OpenHarmony 目标ohosArm64 + ohosX64(desktop/jvm 目标保留作交叉验证)
Gradle9.5.0(沿用上游 wrapper;2.4.20 线支持 Gradle 9.x)
Compose(plain / OH)plain 1.11.1(JVM 验证)+ OH 1.9.2-OH.0.1.2-35(ohos force)
lifecycle / savedstateplain 2.11.0 / 1.4.0;OH 2.9.4-OH.0.1.2-37 / 1.3.3-OH.0.1.2-35
kotlinxcoroutines plain 1.10.2 / OH 1.11.0-0.1.0-07;serialization plain 1.11.0 / OH 1.11.0-0.1.0-07
skiko0.9.22.2-0.5.0-30(viewmodel-compose 的 ohos klib 传递依赖 compose.ui ohos 线)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.enro
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块commonMain 外部依赖处置
enro-annotations零依赖整体编译(nativeMain 的 NavigationDestination actual 经 fork 层级模板天然覆盖 ohos)
enro-commonserialization、urlencoder、@Stable/@Immutable 注解整体编译 + 2 个 ohosMain actual(className、serializerForNavigationKey)
enro-runtimecompose.runtime(+saveable)、lifecycle(viewmodel/-compose/runtime-compose)、savedstate(+compose,含 serialization API)、coroutines、serialization整体编译 + 10 个 ohosMain 文件
enro-testcompose.runtime、lifecycle、savedstatecommonMain 编入(JVM 交叉验证需要)
enro-uiCompose UI/animation/foundation/navigationevent留盘隔离(无 ohos 可解析制品)
enro / enro-compat / enro-html / enro-lint / enro-processorAndroid/JS/KSP 专属留盘隔离

expect/actual 全清单(本适配的手工工作量所在):

声明ohosMain actual 处置
platformNavigationModule空模块(desktop/web 同款)
EnroLogprintln + 环形缓冲(给桥观测)
callSiteClassName / stableClassName / serializerForNavigationKey照抄 ios/desktop actual
findRootNavigationContext明确报错(无头宿主无组合根)
rememberShouldRemoveViewModelStoreCallback{ true }
EnroViewModelFactory照抄 desktop
handleNavigationOperationForPlatformreturn false(web 同款)
1.4 页面边界
标题区          ENRO · OPENHARMONY / 版本握手行(导航引擎 · Kotlin/Native ohosArm64/ohosX64 · 版本号)
Tab 1 导航栈    main/modal 双容器卡片,条目(序号. Key / args / #短id),active 标记
Tab 2 操作/结果 Detail 打开(id/title 输入)、Settings、Sheet(modal)、关闭顶部、替换顶部;
                结果回传区:completeFrom → complete(值输入)→ results 列表
Tab 3 深链      路径输入(默认 /detail/42)解析打开;pathFor 反向生成
Tab 4 保存恢复  save → clear → restore 三步按钮 + token 显示
Footer          状态行 + 引擎日志区(最近 8 行,[boot] 启动结果常驻)

页面启动时先调 enroVersion() 做握手,再 dispatch bootstrap 并把启动结果永久写入日志区。

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

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

先查社区 Maven(https://maven.eazytec-cloud.com)上 ohos 制品的覆盖面。用 .module 元数据逐个验证双 ohos 变体(ohosArm64ApiElements + ohosX64ApiElements):

org.jetbrains.compose.runtime:runtime:1.9.2-OH.0.1.2-35        ✓ 双 ohos(无 jvm)
org.jetbrains.compose.runtime:runtime-saveable:1.9.2-0.3.0-07  ✓ 双 ohos
org.jetbrains.compose.runtime:runtime-annotation:1.9.2-OH.0.1.2-35 ✓ 双 ohos + jvm
org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.9.4-OH.0.1.2-37 ✓
org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.9.4-OH.0.1.2-33 ✓
org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.9.4-OH.0.1.2-33 ✓
org.jetbrains.androidx.savedstate:savedstate(-compose):1.3.3-OH.0.1.2-35 ✓
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0-0.1.0-07 ✓
org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0-0.1.0-07 ✓
org.jetbrains.skiko:skiko:0.9.22.2-0.5.0-30 ✓

三个关键结论:其一,导航引擎需要的五件套 ohos 线齐套,这是"真编译整库"成立的前提;其二,savedstate OH 线的主制品 自带 androidx.savedstate.serialization.* 全套 API(经 sources jar 核验),上游以独立 artifact 引用的编码器/解码器/配置无需额外坐标;其三,OH 制品的 klib 里有独立 -ohosarm64/-ohosx64 模块(根模块 .module 的 available-at 指向),变体选择由 Gradle 属性匹配完成。

2.2 双线钉版(plain 声明 + ohos force)

上游 plain 制品没有 ohos klib,OH 重建制品大多没有 jvm 变体——同一个坐标要同时喂饱两个目标体系,只能在配置层面分线:

// commonMain 声明上游 plain 精确版本(JVM 交叉验证与上游 CI 版本组一致)
implementation(libs.compose.runtime)          // 1.11.1
implementation(libs.lifecycle.viewmodel)      // 2.11.0
implementation(libs.savedstate)               // 1.4.0

// 根构建:ohos 编译配置 force 到 OH 线
configurations.configureEach {
    if (name.contains("ohos", ignoreCase = true)) {
        resolutionStrategy.force(
            "org.jetbrains.compose.runtime:runtime:1.9.2-OH.0.1.2-35",
            "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.9.4-OH.0.1.2-37",
            "org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0-0.1.0-07",
            // …完整 12 项见仓库 build.gradle.kts
        )
    }
}

两个例外:runtime-annotation 的 plain 1.9.x 从未发布,而 OH 版自带 jvm 变体,直接在 toml 声明 OH 版通吃两个目标;skiko 只在 ohos 侧需要(viewmodel-compose 的 ohos klib 传递依赖 compose.ui ohos 线)。settings.gradle.kts 把 eazytec 双仓写在最前,另加其 maven-central 成员仓与 mavenCentral() 兜底(该网络环境下 repo1.maven.org 整体不可达,repo.maven.apache.org 可用)。

2.3 编译真实引擎

enro-runtime/build.gradle.kts 收敛为纯 KMP 模块(multiplatform + serialization + compose 编译器插件,上游同款 opt-in):

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)
    jvm()
    ohosArm64()
    ohosX64()
    sourceSets {
        commonMain.dependencies {
            api(project(":enro-annotations"))
            api(project(":enro-common"))
            implementation(libs.compose.runtime)
            implementation(libs.compose.runtime.saveable)
            api(libs.lifecycle.viewmodel)
            implementation(libs.lifecycle.viewmodel.compose)
            implementation(libs.lifecycle.runtime.compose)
            implementation(libs.savedstate)
            implementation(libs.savedstate.compose)
            implementation(libs.serialization.json)
            implementation(libs.coroutines.core)
            implementation(libs.urlencoder)
        }
        commonTest.dependencies {
            implementation(project(":enro-test"))
            implementation(kotlin("test"))
            implementation(libs.coroutines.test)
        }
    }
}

实测:

./gradlew :enro-runtime:compileKotlinOhosArm64 :enro-runtime:compileKotlinOhosX64   # BUILD SUCCESSFUL

引擎的全部 commonMain 文件一个没动,新增的只有 ohosMain 平台层(见第 3 阶段)。

2.4 测试依赖的处置

commonTest 声明的依赖对 ohos 测试编译同样可见,处置是两条纪律的组合:测试统一在 JVM 跑(desktopTest/jvmTest),根构建全局禁用 ohos 测试任务:

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

两个测试期补充:enro-test 必须应用 compose 编译器插件(否则 navigationDestinationContent 的 @Composable 调用点签名不匹配,运行期 NoSuchMethodError);jvmTest 需要 kotlin-reflect(serializerForNavigationKey 的反射默认查找)。另有两处 expect/actual 在测试源集(platformHasQualifiedClassNames、RobolectricHostTest),从 desktopTest 复制到 jvmTest 即可,测试文件本身零改动。

第 3 阶段:平台宿主(EnroOhosHost)

Enro 的平台集成模式是"每个平台一个根宿主":desktop 的 RootWindow、iOS 的 EnroUIViewController、web 的 EnroBrowserRoot。ohos 的新宿主 EnroOhosHost 是无头的,位于 enro-runtime/src/ohosMain——与上述宿主平级:

public class EnroOhosHost(
    public val isDebug: Boolean = false,
    builder: NavigationModule.BuilderScope.() -> Unit = {},
) : LifecycleOwner, ViewModelStoreOwner {

    override val viewModelStore: ViewModelStore = ViewModelStore()
    private val lifecycleRegistry = LifecycleRegistry(this).apply {
        currentState = Lifecycle.State.RESUMED
    }

    public val controller: EnroController =
        internalCreateEnroController(isDebug = isDebug, builder = builder)  // 库内 internal,宿主必须住在 enro-runtime 里

    private val wrappedViewModelStoreOwner = EnroWrappedViewModelStoreOwner(
        controller = controller, viewModelStoreOwner = this, savedStateRegistryOwner = null,
    )

    public val rootContext: RootContext = RootContext(
        id = "EnroOhosHost(GenericOhosKey)@${Uuid.random()}",
        parent = this, controller = controller,
        lifecycleOwner = this,
        viewModelStoreOwner = wrappedViewModelStoreOwner,
        defaultViewModelProviderFactory = wrappedViewModelStoreOwner,
        activeChildId = mutableStateOf(null),
    )

    public val navigation: NavigationHandle<NavigationKey> = run {
        controller.install(this)
        val holder = wrappedViewModelStoreOwner.getOrCreateNavigationHandleHolder {
            RootNavigationHandle(GenericOhosKey.asInstance(), createSavedStateHandle())
        }
        (holder.navigationHandle as RootNavigationHandle<NavigationKey>).bindContext(rootContext)
        holder.navigationHandle as NavigationHandle<NavigationKey>
    }
    // addContainer / open / closeTop / replaceTop / completeFrom / completeTop /
    // saveContainerState / restoreContainerState / keyFromPath / pathFromKey / flushSnapshots …
}

宿主必须放在 enro-runtime 内部(ohosMain 源集)而不是桥模块:它需要 internalCreateEnroController、rootContextRegistry、getOrCreateNavigationHandleHolder、EnroWrappedViewModelStoreOwner 等 internal API——与 iOS 的 EnroUIViewController 住在 iosMain 完全同构。操作入口全部经 navigation.execute(...) 走引擎完整操作管线,不走捷径;flushSnapshots() 在无 UI 快照管理器的环境下手动 Snapshot.sendApplyNotifications()。

真机上踩过的关键一坑:宿主不能实现 HasDefaultViewModelProviderFactory——一旦实现,EnroWrappedViewModelStoreOwner 会走 delegate 路径拿宿主自己的空 CreationExtras,导致 createSavedStateHandle() 抛 CreationExtras must have a value for SAVED_STATE_REGISTRY_OWNER_KEY。删掉该接口,wrapped 自动注入 UnboundedSavedStateRegistryOwner(web 平台同款 fallback),真机 bootstrap 立即转绿。

第 4 阶段:桥协议建模

4.1 JSON 契约
dev.enro/**(上游引擎 + ohosMain 宿主,原样编译)
        ↓ EnroOhosHost:controller / RootContext / containers / operations
Bridge.kt(@CName × 2,扁平 JSON 进出)
        ↓ UTF-8 JSON
N-API enroDispatch(request) / enroVersion()
        ↓
ArkUI Index.ets(四 Tab 单页)

14 个 op:bootstrap / open / close / replace / completeFrom / complete / openFromPath / pathFor / state / save / restore / clear / results / log。请求扁平 JSON,响应 {"ok":true,"op":...} 或 {"ok":false,"error":"..."}。

Demo 键与模块(dev.enro.platform.ohos.demo)用注解处理器生成代码调用的同一套公共 DSL 装配:

public fun createDemoModule(): NavigationModule = createNavigationModule {
    serializersModule(demoSerializersModule)   // 5 个 @Serializable 键
    path(NavigationPathBinding(
        keyType = DetailKey::class,
        pattern = "detail/{id}",
        deserialize = { DetailKey(id = require("id").toInt(), title = "From path") },
        serialize = { set("id", it.id.toString()) },
    ))
}

序列化器做 NavigationKey + Any 双作用域注册:2.4.20 线序列化插件对 @Polymorphic 泛型字段(NavigationKey.Instance.key)的 decode 在 Any 作用域查找子类序列化器,双注册让设备端 SavedState 往返完整工作(详见 8.1 第 8 条)。

4.2 错误通道

每个导出整体 try/catch,bootstrap 阶段还细分"宿主构造失败"与"容器注册失败"并携带 cause 链:

@CName("EnroDispatch")
fun enroDispatch(request: String): String = try {
    dispatch(MiniJson.parse(request))
} catch (t: Throwable) {
    """{"ok":false,"error":${jsonString("${t::class.simpleName}: ${t.message}")}}"""
}

非法 JSON、未知 op、未知键、bootstrap 前调用、保存令牌不存在,全部走同一个 ok:false 通道,ArkTS 侧统一渲染。启动握手结果由页面写入常驻日志区([boot] dispatch bootstrap -> ...),排查真机问题时不再被后续操作覆盖。

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

5.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libenro_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohosenro.so)
  │ EnroDispatch / EnroVersion
  ▼
UTF-8 JSON + DisposeString

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

5.2 Kotlin/Native 导出函数
@CName("EnroVersion")
fun enroVersion(): String = "3.0.0-beta07-ohos.1"

@CName("EnroDispatch")
fun enroDispatch(request: String): String
C ABI 符号ArkTS 方法覆盖内核
EnroDispatchenroDispatch(req)bootstrap / open / close / replace / completeFrom / complete / openFromPath / pathFor / state / save / restore / clear / results / log
EnroVersionenroVersion()fork 版本握手(真机版本行的数据来源)

llvm-nm -D libohosenro.so 实测三者(EnroDispatch / EnroVersion / libohosenro_symbols)均为全局 T 符号。

5.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"enroDispatch", nullptr, NapiEnroDispatch, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"enroVersion", nullptr, NapiEnroVersion, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

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

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

add_library(enro_napi SHARED napi_init.cpp)
target_link_libraries(enro_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohosenro)

OHOS_ARCH 为空或 libohosenro.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。index.d.ts 只导出两个函数签名,类型包经 oh-package.json5 的 file:./src/main/cpp/types/libenro_napi 进入 ArkTS。

第 6 阶段:ArkUI 宿主

6.1 注入握手和触发操作
aboutToAppear(): void {
  this.version = enroVersion();                  // 版本握手:真实 Native 调用
  const bootstrapReq: OpRequest = { op: 'bootstrap' };
  this.dispatch('bootstrap', bootstrapReq);
  const bootLine: string = `[boot] dispatch bootstrap -> ${this.status}`;
  this.logLines = this.logLines.concat([bootLine]);  // 启动结果常驻日志区
}

private dispatch(op: string, body: OpRequest): void {
  const request: OpRequest = { op: op, key: body.key, id: body.id, /* … */ };
  const res = JSON.parse(enroDispatch(JSON.stringify(request))) as DispatchOk;
  if (res.ok) { this.applyState(res); this.status = `${op} 成功`; }
  else { this.status = `${op} 失败: ${res.error ?? 'unknown'}`; }
}

ArkTS 严格模式(arkts-no-untyped-obj-literals)要求所有对象字面量对应显式接口——OpRequest 接口承载全部请求字段;struct 的 build 是保留方法名、tabIndex 撞内置属性,自定义方法与状态变量命名都要避开。

6.2 页面交互

四 Tab 自上而下:导航栈(双容器卡片 + 条目列表,栈顶高亮);操作/结果(Detail 参数输入与五类操作按钮、结果回传区带值输入与 results 列表);深链(路径输入解析 + 反向生成);保存恢复(三步按钮 + token 显示)。Footer 状态行 + 最近 8 行引擎日志(EnroLog 的 ohos actual 带环形缓冲,经 op=log 拉取)。

6.3 对比度和错误边界

主操作深绿、辅助蓝、破坏性(关闭/清空)红,语义对齐;ok:false 时错误原文(含异常类名与 cause 链)进状态行与日志区。Native 调用本身抛错同样被 try/catch 收进日志区。

第 7 阶段:示例与验证

7.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohosenro)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/org/terminator/ohos/enro/
│       └── Bridge.kt                         @CName × 2 + MiniJson(继承 cmp_qrose 桥模式)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.enro、versionName 3.0.0-beta07-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/libohosenro_api.h
        ├── src/main/cpp/types/libenro_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 四 Tab 验收页面
7.2 构建、签名和安装
./gradlew :enro-common:desktopTest :enro-runtime:jvmTest              # 交叉验证
./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,不进入仓库。

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

四、完整代码对照

4.1 整体架构

dev.enro/**(上游引擎原样编译)
    │ EnroController / NavigationContainer / NavigationHandle / 结果通道 / 路径
EnroOhosHost + 七个 ohosMain actual(enro-runtime/src/ohosMain)
    │ RootContext / RootNavigationHandle / 容器注册 / 操作入口 / SavedState 编解码
dev.enro.platform.ohos.demo(演示键与模块,双作用域序列化注册)
    │ createDemoModule() = 注解处理器生成的同款公共 DSL
Bridge.kt + MiniJson(example/nativeApp)
    │ EnroDispatch / EnroVersion
libenro_napi.so
    │ enroDispatch / enroVersion
Index.ets
    │ JSON parse + @State 更新
    ▼
ArkUI 真机页面(四 Tab 单页)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec 置前、ohos_enro、隔离模块移出
gradle/libs.versions.toml双线钉版(plain 上游精确版 + OH 线版本 + fork Kotlin)
build.gradle.kts(根)版本、urlencoder 依赖替换、ohos 配置 force OH 线、禁用 Ohos*Test*
enro-*/build.gradle.kts ×4纯 KMP:desktop/jvm + 双 ohos 目标、上游同款 opt-in
enro-runtime/src/ohosMain(10 文件)七个 expect 的 actual + EnroPlatformOhos + EnroOhosHost + demo 模块
enro-runtime/src/jvmMain(8 文件)/ jvmTest(2 文件)JVM 交叉验证目标的 actual 补位
enro-common/src/ohosMain(2 文件)className / serializerForNavigationKey actual
urlencoder/上游 1.6.0 源码内置(剥 JVM 注解)+ 许可说明
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt2 个 C ABI、14 个 op、MiniJson、分阶段错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 四 Tab 验收页面
README.OpenHarmony_CN.md鸿蒙适配主文档(17 节)

4.3 关键 API 对照

层次API作用
引擎internalCreateEnroController(builder)默认模块 + 平台模块 + 业务模块装配(生成代码同款入口)
宿主EnroOhosHost.addContainer(name, filter, backstack)容器注册为根上下文子节点,过滤器决定路由
宿主navigation.execute(Open/Close/Complete…)全部操作走引擎完整管线(拦截器、空栈拦截、结果登记)
引擎NavigationKey.WithResult<R> + completeFrom / complete(result)结果回传进 pendingResults
引擎getNavigationKeyFromPath / getPathFromNavigationKey深链双向
引擎encodeToSavedState / decodeFromSavedState(多态 Instance)回退栈序列化(与 Saver 同源)
NativeEnroDispatch / EnroVersionC ABI JSON 导出
N-APIenroDispatch(req) / enroVersion()向 ArkTS 暴露能力
ArkTSdispatch(op, body)触发操作并刷新 UI

4.4 ArkTS 与 Kotlin 的边界

const req: OpRequest = { key: 'detail', id: this.detailId, title: this.detailTitle };
const raw = enroDispatch(JSON.stringify(req));
const res = JSON.parse(raw) as DispatchOk;
// res.ok === true;res.containers = [{name:'main', size:2, entries:[…]}]
"open" -> withHost { h ->
    h.open(buildKey(req))          // buildKey 把 JSON 映射成 @Serializable 的 NavigationKey
    ok("open") { stateJson(h) }    // 容器/条目/活跃容器/结果快照
}

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

五、关键决策说明

决策 1:真编译整个导航引擎而不是提取零依赖内核

早期仓库抽内核,是因为 ohos 上没有它们依赖的制品。本次适配先验证制品线(compose.runtime / lifecycle / savedstate / coroutines / serialization 全部存在双 ohos 变体),障碍已消失——再抽内核等于把导航状态机手抄一遍,反而引入漂移风险。真编译让操作管线、拦截器、结果通道、路径匹配优先级全部是上游原件。

决策 2:工具链升到 Kotlin 2.4.20-0.1.0-07,Gradle 沿用 9.5.0

上游源码按 Kotlin 2.4.0 编写,OH 制品线依赖 stdlib 元数据最高到 2.4 代,旧钉法 2.2.21 读不了 2.4 元数据;2.4.20 与上游同代零降级。Gradle 方面 2.4.20 线支持 9.x,上游 wrapper 9.5.0 原样保留——只有 2.2.21 旧线才必须降 8.14.1,两条工具链线的经验不能互相套用。

决策 3:依赖双线钉版(plain 声明 + ohos force),urlencoder 源码内置

单坐标单声明无法同时满足"JVM 验证与上游 CI 版本组一致"和"ohos 编译用社区 klib",于是 commonMain 声明 plain、ohos 配置 force OH。唯一无 ohos 制品的 urlencoder 以源码内置(其源码 100% commonMain,仅剥 @JvmOverloads/@JvmStatic),根构建用依赖替换把原坐标映射到内置模块,库的依赖声明保持与上游逐字一致。

决策 4:无头平台宿主住进 enro-runtime 的 ohosMain

宿主需要库内 internal API(internalCreateEnroController、rootContextRegistry、getOrCreateNavigationHandleHolder),与 iOS EnroUIViewController 住 iosMain 同构。桥模块只保留 @CName 出口与 JSON 协议,不碰 internal。demo 键与模块同样住 ohosMain(用注解处理器生成代码的同款公共 DSL 装配),桥经 enro-runtime klib 的公共面消费。

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

引擎逻辑在 commonMain,JVM 与 ohos 编译同一份代码;168 项用例跑的就是上游原测试文件。desktopMain 因 navigationevent 无 jvm 可解析制品而隔离,JVM 目标由 jvm() + jvmMain actual(逐字镜像 desktop)补位——测试文件一个字节不动,交叉验证的有效性才成立。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x86_64)、JDK 21、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。

6.2 JVM 交叉验证

./gradlew :enro-common:desktopTest :enro-runtime:jvmTest
模块结果
enro-common desktopTest全绿
enro-runtime jvmTest168 用例,161 通过

161 项覆盖:容器状态机(Open/Close/Complete/Replace/SideEffect)、拦截器管线与聚合操作、空栈行为、多容器路由(含 fromChildrenOnly)、结果通道、路径解析与优先级、Metadata、ViewModel 作用域。9 项失败全部集中在序列化边界用例(见 8.1 第 8 条),与 ohos 交付无关且有桥侧规避。

6.3 HAP 验证

:enro-runtime:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
:example:nativeApp:linkDebugSharedOhosArm64 / OhosX64: BUILD SUCCESSFUL
llvm-nm -D: EnroDispatch / EnroVersion / libohosenro_symbols 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libenro_napi.so)
CompileArkTS: finished
PackageHap: finished
install bundle successfully
start ability successfully

libohosenro.so arm64 约 32.4MB / x86_64 约 31.5MB(debug 未 strip,含 Kotlin/Native 运行时 + Compose runtime + 导航引擎),libc++_shared.so 各约 1.8MB。

6.4 功能用例

  1. 启动后标题下出现 导航引擎 · Kotlin/Native ohosArm64/ohosX64 · 3.0.0-beta07-ohos.1(真实 Native 调用,非静态文字);
  2. 日志区首行 [boot] dispatch bootstrap -> bootstrap 成功;
  3. 导航栈 Tab 出现 main (1) · active 与 1. Home #短id 条目,modal (0) 空容器;
  4. 打开 Detail(id/title 可改)→ main 栈增长,条目带 id=7 title=Item 参数;关闭顶部 → 回退;替换顶部为 Settings → 栈顶换人;
  5. 打开 Sheet → modal 容器出现条目(双容器路由生效),关闭后回到 main;
  6. completeFrom 打开 Search → complete 携带值 → 结果区出现 … → Completed(String: selected-42)(引擎 pendingResults);
  7. 深链 /detail/42 解析打开并显示解析出的 key;pathFor 反向生成 detail/9;
  8. 开数页后 save(拿到 token)→ clear(main 栈空)→ restore → 栈原样回来(SavedState 编解码闭环);
  9. 每步操作的引擎日志(EnroLog 环形缓冲)可经日志区刷新观察。

6.5 验证结论

导航引擎、Compose runtime 状态系统、lifecycle/viewmodel/savedstate 栈、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的导航会话:版本握手、双容器路由、回退栈操作、结果回传、深链解析与状态恢复闭环(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/Enro.git
cd Enro
export JAVA_HOME="/path/to/jdk-21"
./gradlew :enro-runtime:compileKotlinOhosArm64 :enro-runtime:compileKotlinOhosX64  # klib
./gradlew :enro-common:desktopTest :enro-runtime:jvmTest                         # 交叉验证
./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.enro -m entry
hdc shell hilog | grep -i enro

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让引擎真正编译成 ohosArm64 / ohosX64 klib 并在设备上执行;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前;上游把 toml 放仓库根目录时,替换 settings 不能丢掉显式 versionCatalogs 声明(或统一挪到 gradle/ 标准位置走自动导入);
  3. buildSrc 无法从构建排除,上游约定插件又依赖已删除的 toml 条目——改名 buildSrc.upstream 留盘;
  4. org.jetbrains.compose.runtime:runtime-annotation 无 plain 1.9.x 可解析,而 OH 版自带 jvm 变体——直接声明 OH 版通吃;
  5. OH 线 viewmodel-compose 的 klib 传递依赖 compose.ui ohos → skiko,plain skiko 无 ohos 变体——force OH 线 skiko;
  6. navigationevent 全线无 jvm 可解析制品——desktopMain 隔离,JVM 目标改 jvm() + jvmMain actual(逐字镜像 desktop);
  7. enro-test 不应用 compose 编译器插件时,@Composable 调用点运行期 NoSuchMethodError——插件补回;serializerForNavigationKey 反射默认查找需要 jvmTest 侧 kotlin-reflect;
  8. 9 项序列化边界测试失败的根因:2.4.20 线序列化插件对 @Polymorphic 泛型字段的 decode 在 Any 作用域查找子类序列化器(25 行最小复现定位;换 plain Kotlin 2.4.20 复测同样失败,排除 fork 因素;上游这些用例钉的正是 kotlinx #3022 周边边界)。规避:demo 序列化器 NavigationKey + Any 双作用域注册,设备端 SavedState 往返完整工作;
  9. 本世代 kotlinx.serialization 1.11 中 SerializersModule 位于 kotlinx.serialization.modules 包——import 路径写错会表现为成串的 Unresolved reference,与依赖缺失高度混淆;
  10. 宿主多实现 HasDefaultViewModelProviderFactory 会覆盖 EnroWrappedViewModelStoreOwner 的 extras 注入,真机 bootstrap 抛 SAVED_STATE_REGISTRY_OWNER_KEY 缺失——宿主只保留 LifecycleOwner + ViewModelStoreOwner;
  11. Kotlin/Native 无 synchronized / String.format——桥单线程免锁,十六进制转义手写;
  12. ArkTS 严格模式:对象字面量必须对应显式接口(arkts-no-untyped-obj-literals)、build 是 struct 保留方法名、tabIndex 撞内置属性、旧 SDK 的 FlexOptions 无 spaceBetween;
  13. 配置缓存可能重放改名前的构建错误,排障时加 --no-configuration-cache;
  14. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可。

8.2 已知问题

  • enro-ui(Compose UI 渲染层)未编入:Compose UI/animation/navigationevent 无 ohos 可解析制品,当前 Demo 用 ArkTS 呈现容器状态,等社区 CMP UI ohos 制品与上游版本对齐后接回才是完全体;
  • 9 项序列化边界测试在 JVM 失败(8.1 第 8 条),属工具链代差,桥侧已规避,上游 kotlinx 修复后应转绿;
  • KSP 处理器未编入:鸿蒙 App 侧生成 installNavigationController 需要处理器支持 ohos 平台分支;当前由桥用同一套公共 DSL 手工装配模块替代;
  • 桥是同步一次一调用:订阅式推送(backstackFlow)不跨 C ABI,ArkTS 侧以操作后读回观察状态;
  • debug so 约 32MB(含 KN 运行时 + Compose runtime + 引擎),release strip 后会显著缩小;
  • Android / js / wasm / ios 目标剥离(源文件留在树里),发布、dokka、kover 一并剥离,本 fork 不发 Maven Central;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.enro 匹配的签名。

8.3 未来优化方向

  • 等社区 compose.ui ohos 制品与上游 Compose 版本对齐后,把 enro-ui 接回,用 ComposeArkUIViewController 跑真正的 NavigationDisplay;
  • 给 enro-processor 增加 ohos 平台分支(ResolverPlatform 新枚举 + UIAbility 引用参数),让鸿蒙 App 侧也能走注解处理器生成装配;
  • 状态变化推送(N-API 线程安全回表)替代操作后读回;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。

九、总结

9.1 核心难点回顾

上游 enro-annotations + enro-common + enro-runtime + enro-test(全部 commonMain)
    → 验证 OH 制品线(compose.runtime / lifecycle / savedstate / coroutines / serialization)
    → 工具链对齐 Kotlin 2.4.20-0.1.0-07(Gradle 9.5 沿用)
    → 根构建替换(cmp_qrose 模式,双线钉版:plain 声明 + ohos force)
    → ohosArm64 / ohosX64 KLIB + 七个 expect 的 ohos actual
    → EnroOhosHost 无头平台宿主(RootContext / RootNavigationHandle / 容器注册)
    → C ABI(@CName × 2)+ 14 op JSON 契约 + demo 模块双作用域注册
    → C++ N-API(DisposeString 纪律)
    → ArkTS 四 Tab 解析
    → ArkUI 真机单页验收(容器路由 / 回退栈 / 结果回传 / 深链 / 保存恢复)

9.2 封装层次

  • 引擎层:上游原版 EnroController、NavigationContainer 过滤与路由、NavigationHandle 操作管线、结果通道与路径解析;
  • 平台层:ohosMain 的七个 actual + EnroOhosHost(自持 lifecycle/viewmodelStore,装配 RootContext 与根 handle,容器注册与操作入口);
  • Native 层:双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理页面状态、JSON 校验、操作分发、日志与错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 先验证生态制品线再定适配路线:导航类库的正确答案由"引擎依赖的每个制品是否有双 ohos 变体"决定——齐套就真编译整库,只补平台宿主;路线选择必须基于当天的制品可达性,而不是历史结论;
  2. 工具链版本跟着依赖走:Kotlin 钉版由制品线的 stdlib 元数据反推(2.4.20-0.1.0-07 与上游 2.4.0 同代,零源码降级),Gradle 经验按工具链线区分(2.4.20 线留 9.x,2.2.21 线降 8.14.1);
  3. 交叉验证是"内核没漂"的唯一可信证明:168 项用例跑上游原测试文件,测试零改动才让"引擎没漂"有说服力;9 项失败要定位到最小复现并给出桥侧规避,而不是绕过或改测试。导航库没有渲染面,Demo 的验收重点天然落在"容器路由正确、结果真的回来、状态恢复原样"这三条闭环上。

9.4 适配成果

当前适配已完成:

  • Enro 3.0.0-beta07 真实导航引擎的双 ABI Kotlin/Native 编译(上游 commonMain 文件零重写,仅新增平台 actual 与无头宿主);
  • 依赖双线钉版在导航库上的完整落地(plain 上游精确版供 JVM 验证,OH 线供 ohos 编译,urlencoder 源码内置);
  • 2 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • DevEco Stage ArkUI 四 Tab 验收页面(容器路由、回退栈操作、结果回传、深链、保存恢复、引擎日志);
  • 161 项 JVM 交叉验证通过(上游原测试文件,9 项序列化边界失败已定位并规避)+ 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

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

更多推荐