开源鸿蒙平台 KMP 三方库「Enro」适配全流程

本文详述开源导航库Enro适配OpenHarmony平台的完整技术路径,涵盖KMP多平台构建、Kotlin/Native双ABI编译、C ABI/N-API桥接层设计、无头平台宿主(EnroOhosHost)实现、ArkTS页面集成及HAP真机验证。重点突破依赖双线钉版、JSON契约建模、SavedState跨平台序列化、三层桥接架构等关键技术难点,并通过161项JVM交叉测试与真机功能闭环验证导航引擎逻辑一致性。

本文记录 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。

参考文档

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值