本文记录 Decompose 接入 OpenHarmony 的完整过程,覆盖导航组件库盘点、Essenty 依赖处置、真实引擎编译、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

Decompose 是基于 Essenty + kotlinx.serialization 的 Kotlin Multiplatform 生命周期感知业务组件库(BLoC):组件树 + 路由式导航(Child Stack / Slot / Pages / Panels / 通用 Children)+ 可插拔 UI,ComponentContext 聚合 Lifecycle(生命周期)、StateKeeper(状态保存)、InstanceKeeper(实例保留,对应 AndroidX ViewModel 语义)、BackHandler(返回处理)。本次适配与社区早期路线(qrose / haze / PreCompose / voyager 的"零依赖内核提取")有本质区别:社区 Maven 上已经出现完整的 HarmonyOS kotlinx 制品线(serialization-core / json 1.11.0-0.1.0-07,全部带 ohosArm64 / ohosX64 Gradle 变体),但 Decompose 的核心依赖 Essenty 没有任何 ohos 制品——因此本次采用"真实引擎原样编译 + 依赖源码 vendored"的混合路线:Decompose 引擎一个文件没动,Essenty 2.7.0-alpha01 从官方 sources jar 逐字进入本仓编译,一个带生命周期编排、状态保存恢复、返回键分发的完整导航引擎运行在真机上,ArkUI 页面作为验收宿主。

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

上游仓库: arkivanov/Decompose

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

Decompose 为 Kotlin Multiplatform 应用提供"业务逻辑组件树"这一件事的完整答案:DefaultComponentContext 一行组装四件套,childStack 工厂把 @Serializable 配置栈实例化为组件树,backStack 里的组件保留实例在后台继续存活,LifecycleRegistry 驱动整棵树的状态机,StateKeeper 在进程死亡后凭序列化快照恢复导航状态与组件状态,InstanceKeeper 决定 retained 实例跨配置变更的存活。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套组件化与导航能力,但上游构建(Kotlin 2.3.21 / Gradle 8.13 / arkivanov 自研 gradle-setup-plugin)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示按钮,却无法证明导航引擎、生命周期编排、状态保存恢复在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证引擎本体、vendored 依赖、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
依赖无制品commonMain 对 Essenty 是 api() 依赖(lifecycle / state-keeper / instance-keeper / back-handler),社区 Maven 上没有任何 Essenty ohos 变体。
工具链不一致上游 Kotlin 2.3.21 + 自研 gradle-setup-plugin(target 全集、bundle 层级、发布全挂它上面)与鸿蒙工具链无法共存于一个 Gradle 构建。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
构建拓扑复杂上游 16 个模块里 10 个依赖 AGP/Compose/iOS 工程,只有 3 个纯逻辑模块值得进 ohos 构建。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

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

1.2 库提供的能力

Demo 提供一个真实的组件世界会话,对应桥接的全部入口:

  • 生命周期(Lifecycle):LifecycleRegistry 全状态机驱动(INITIALIZED→CREATED→STARTED→RESUMED,反向回退与 DESTROYED),根与子组件回调成对滚动;
  • 计数与 Value:活跃子组件的 MutableValue 计数器,订阅 / 退订 observer,每次修改在日志中打印发射记录;
  • 导航栈(Child Stack):StackNavigation push Counter / push Info / replace 顶部 / pop / popToRoot / popToIndex,backStack 子组件保留实例与计数并进入停止态;
  • 返回键(BackHandler):BackDispatcher.back() 先弹栈、根部触发 exit 兜底回调;
  • 状态保存(StateKeeper / InstanceKeeper):保存 → 把计数改掉 → 恢复,栈与每个子组件的计数回到保存时刻;recreate(configChange) 决定 retained 实例是跨配置变更存活还是随进程死亡重建。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文。
rootState / worldId根生命周期状态与当前世界编号。
backEnabled / exitEnabled返回分发与 exit 兜底的当前可用性。
stack[]全栈视图:每层 config / lifecycle / count / active。
retainedId / retainedOriginretained 实例编号与来源(本世界 / 跨重建存活)。
savedPreview / savedMeta已保存栈的序列化配置预览与保存时刻。
log[]滚动事件日志(最近 60 条)。

引擎侧完整能力(Child Slot / Pages / Panels / 通用 Children、Items、Value operator 等)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 623 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的引擎没有漂移。

1.3 实现适配

维度要求
代码复用decompose 与 decompose-test-utils 的全部 commonMain 文件原样编译,不提取、不重写;Essenty 2.7.0-alpha01 从官方 sources jar 逐字 vendored(35 个文件,文件名都不改);全仓新增 Kotlin 源仅 4 个(3 个 ohos actual + 1 个 Essenty ohos serialize actual,合计约 90 行)。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);extensions-compose / extensions-android / jetpack-component-context / sample/*(Compose 与 Android 绑定)留盘不进构建。
桥接稳定使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律Essenty 版本与上游声明逐字一致(vendored 换形不换版);serialization 换线不换版主体(1.11.0 → 1.11.0-0.1.0-07);上游测试零改动全绿。
UI 完整单页覆盖全部桥接入口:四个 Tab + 状态徽章 + 事件日志。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。Decompose 本身不碰 UI(这正是它的设计),适配面天然最小;ArkTS Demo 只是把桥暴露的引擎能力画出来。

二、实现路线图

第 1 阶段:项目初始化     ── 模块与依赖边界盘点、Essenty 制品查重(无 ohos 变体)、确定 vendored 路线
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版、Essenty vendored 编译
第 3 阶段:桥协议建模     ── 演示世界(DemoWorld)JSON 契约、全量状态响应、错误通道
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 版本握手、四 Tab、栈可视化、事件日志
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
essenty/                               vendored Essenty 2.7.0-alpha01(进 ohos 构建,jvm + ohosArm64 + ohosX64)
  src/commonMain/                      lifecycle(6) + statekeeper(12) + instancekeeper(6) + backhandler(7) + utils(2)
  src/jvmMain/.../Utils.java.kt        JVM serialize actual(Zip 流水线,上游文件名原样保留)
  src/ohosMain/.../Utils.kt            新增:ohos serialize actual(对齐上游 nonJavaMain 变体)
decompose/                             上游导航引擎(进 ohos 构建,commonMain 一行未动)
  src/ohosMain/                        新增:Lock / CheckMainThread / PrintError 三个 actual
  src/nonWebMain/                      上游中间源集(uniqueName actual),jvm/ohos 共同复用
decompose-test-utils/                  上游测试工具(2 个文件,纯 common,JVM 交叉验证用)
example/nativeApp/                     Kotlin/Native 动态库(Bridge + DemoWorld + JsonOut/MiniJson)
example/harmonyApp/                    DevEco Stage、CMake N-API 和 ArkTS 四 Tab 页面
README.OpenHarmony.md / _CN.md         鸿蒙适配说明(中英双份)
README.upstream.md                     上游原文保存
sample/ docs/ tools/ extensions-*/     上游遗留(Compose/Android/iOS 示例与文档站),留盘不进构建

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :essenty、:decompose、:decompose-test-utils 与 :example:nativeApp,rootProject.name = "ohos_Decompose"。上游的自研 gradle-setup-plugin、AGP/Compose classpath、binary-compatibility-validator、dependency-guard、Maven Central 发布段全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
JDK21(org.gradle.java.home 指向本机 Adoptium 路径)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle8.13(沿用上游 wrapper;仅 -all 换 -bin,本地缓存可达性差异)
kotlinxserialization-core / serialization-json 1.11.0-0.1.0-07(ohos 制品线)
vendored 依赖Essenty 2.7.0-alpha01(lifecycle / state-keeper / instance-keeper / back-handler / utils-internal)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.decompose
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块commonMain 文件依赖形态处置
decompose100+Essenty 四件套 api() + serialization-core,零 UI、零 AGP整体编译(jvm + 双 ohos)
decompose-test-utils2纯 common(TestComponentContext 等)整体编译(JVM 测试用)
essenty(上游依赖)33serialization-core/json,posix actual源码 vendored(社区无 ohos 制品)
extensions-compose / -android / jetpack-component-context—Compose / AGP 绑定留盘不进构建
sample/*—Compose/React/Xcode 工程留盘不进构建

Decompose 全部 expect 声明只有 4 个:Lock(递归互斥)、checkMainThread()、KClass<*>.uniqueName、printError(exception)。ohos 侧的落点:前三个在 ohosMain 新增 actual(Lock 参照上游 linuxMain 的 pthread 实现,posix 同源逐字一致;checkMainThread 与 Linux 一样 no-op;printError 同款),uniqueName 直接复用上游 nonWebMain/Utils.nonJs.kt——ohos 通过 dependsOn(nonWebMain) 继承,不重复写(写了反而 Conflicting declarations)。

1.4 页面边界
标题区          DECOMPOSE · OPENHARMONY / 版本握手行(KMP 生命周期组件库 · Kotlin/Native ohosArm64/ohosX64)
状态徽章行      world #N · root=<状态> · back=<on/off> · exit=<on/off> · retained 实例 #N(来源)
Tab 切换        生命周期 / 计数与 Value / 导航栈 / 状态保存
生命周期区      CREATE / START / RESUME / PAUSE / STOP / DESTROY 六按钮 + RESET(新世界)
计数区          -1 / +1 / +10 / 清零 + 订阅 observer / 取消订阅(已订阅状态行)
导航区          push Counter / push Info / replace 顶部 / pop / 回到根 / 模拟返回键 + 栈列表
状态区          保存状态 / 恢复保存的状态 / recreate 配置变更 / recreate 进程死亡 + 已保存栈预览
日志区          事件日志(Lifecycle / Value / Router / Back 全部回调,最新在前)

页面启动时先调 decomposeVersion() 做握手,再 op:reset 建世界(根组件生命周期从 INITIALIZED 起步,由用户驱动——生命周期本身就是被演示的对象)。

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

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

先查社区 Maven(https://maven.eazytec-cloud.com)上有没有 ohos 的制品。逐项核对:

org.jetbrains.kotlinx:kotlinx-serialization-core:1.11.0-0.1.0-07
  variants: ohosArm64Api-published / ohosX64Api-published / jvmApi-published / jvmRuntime-published ✔
org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0-0.1.0-07
  variants: 同上(jvm + ohos 双线齐全)✔
com.arkivanov.essenty:{lifecycle, state-keeper, instance-keeper, back-handler}:2.7.0-alpha01
  variants: 仅 iosArm64 / jvm / js / linuxX64 等上游原生变体(eazytec 只是 Central 镜像)✘ 无 ohos

两个关键结论:其一,serialization 版本主体与上游声明逐字相同(1.11.0 覆盖上游的 1.6.3,从源码编译本就取最新稳定 API),只是仓库换线——commonMain 里一行依赖坐标都不用改语义;其二,Essenty 是死路:Maven Central 与社区镜像都没有 ohos 变体,也没有任何 -0.1.0 / OH 后缀的 Essenty 发行线。处置由此定型——不是抽内核,也不是放弃,而是 vendored 源码。

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

serialization 1.11.0-0.1.0-07 依赖 kotlin-stdlib 2.4.20-0.1.0-07(元数据 2.4 级),旧钉法 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 用的)读不了 2.4 元数据,编译期直接报:

Class 'kotlin.Unit' was compiled with an incompatible version of Kotlin.
The binary version of its metadata is 2.4.0, expected version is 2.3.0 or less.

工具链版本跟着依赖走:Kotlin 钉 2.4.20-0.1.0-07。额外好处:上游源码本来就是 Kotlin 2.3.21 编的,2.4.20 高于它,源码级零降级;Gradle 方面 2.4.20 线支持 8.x/9.x,上游 wrapper 8.13 原样沿用(唯一改动:gradle-8.13-all.zip 换 gradle-8.13-bin.zip,因为本机缓存与网络可达性差异,两者只差文档源码)。

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_Decompose"

include(":essenty")
include(":decompose")
include(":decompose-test-utils")
include(":example:nativeApp")
2.3 编译真实引擎(含 vendored Essenty)

先从 eazytec 镜像下载 Essenty 五个模块的 2.7.0-alpha01-sources.jar,解出 commonMain(+ state-keeper 的 javaMain / nonJavaMain),35 个文件逐字拷贝进 :essenty 模块——包名、文件名、内容一律不改(Utils.java.kt 这种文件名都原样保留,原因见踩坑表)。全仓唯一新增的 Essenty 源文件是 ohos serialize actual(对齐上游 nonJavaMain 变体):

// essenty/src/ohosMain/kotlin/com/arkivanov/essenty/statekeeper/Utils.kt
// Mirrors the upstream nonJavaMain actual used by iOS/Linux: ohos is a
// posix non-Java platform, so serialization goes through the plain JSON
// string path instead of the java.util.zip pipeline.
internal actual fun <T> T.serialize(strategy: SerializationStrategy<T>): ByteArray =
    essentyJson.encodeToString(serializer = strategy, value = this).encodeToByteArray()

internal actual fun <T> ByteArray.deserialize(strategy: DeserializationStrategy<T>): T =
    essentyJson.decodeFromString(deserializer = strategy, string = decodeToString())

essenty/build.gradle.kts 收敛为纯 KMP 模块:

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

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)

    jvm()          // 交叉验证用
    ohosArm64()
    ohosX64()

    sourceSets {
        all {
            // Upstream build enables these via its Gradle setup plugin. The
            // vendored PolymorphicSerializer implements a SerialDescriptor,
            // which since kotlinx.serialization 1.11 additionally requires
            // SealedSerializationApi from implementers.
            languageSettings.optIn("kotlinx.serialization.InternalSerializationApi")
            languageSettings.optIn("kotlinx.serialization.SealedSerializationApi")
        }

        commonMain.dependencies {
            api(libs.kotlinx.serialization.core)
            api(libs.kotlinx.serialization.json)
        }
    }
}

decompose/build.gradle.kts 同构,重点是源集接线——上游 kotlin.mpp.applyDefaultHierarchyTemplate=false,层级全部由自研插件声明,本仓恢复默认模板并重建 nonWebMain 中间源集:

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)

    jvm()       // 交叉验证
    ohosArm64()
    ohosX64()

    sourceSets {
        val commonMain by getting

        // Upstream intermediate source set (non-JS/Wasm platforms), reused
        // by the ohos targets instead of duplicating its single actual.
        val nonWebMain by creating { dependsOn(commonMain) }

        jvmMain { dependsOn(nonWebMain) }
        ohosMain { dependsOn(nonWebMain) }

        // The upstream nonWebTest threading tests only run on the JVM here;
        // pull their sources directly into jvmTest instead of re-creating
        // the intermediate test source set.
        jvmTest {
            kotlin.srcDir("src/nonWebTest/kotlin")
        }

        all {
            languageSettings {
                optIn("com.arkivanov.decompose.InternalDecomposeApi")
                optIn("com.arkivanov.decompose.ExperimentalDecomposeApi")
            }
        }

        commonMain.dependencies {
            api(project(":essenty"))
            api(libs.kotlinx.serialization.core)
        }

        commonTest.dependencies {
            implementation(kotlin("test"))
            implementation(project(":decompose-test-utils"))
            implementation(libs.kotlinx.coroutines.core)
        }
    }
}

实测:

./gradlew :essenty:compileKotlinOhosArm64 :essenty:compileKotlinOhosX64            # BUILD SUCCESSFUL
./gradlew :decompose:compileKotlinOhosArm64 :decompose:compileKotlinOhosX64        # BUILD SUCCESSFUL
./gradlew :decompose-test-utils:compileKotlinOhosArm64                             # BUILD SUCCESSFUL

引擎与 vendored 依赖的全部 commonMain 文件一个没动,新增源只有 4 个 ohos actual。

2.4 测试依赖的处置:ohos 侧只编译不测试

上游 commonTest 依赖 kotlinx-coroutines-core(1.9.0,Central 制品,无 ohos 变体需求——因为 ohos 测试任务根本不跑)。处置是两条纪律的组合:测试统一在 JVM 跑(jvmTest),根构建全局禁用 ohos 测试任务:

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

这样上游测试文件(含 coroutines 线程测试)零改动保留,ohos 侧只承担编译与链接。

第 3 阶段:桥协议建模

3.1 JSON 契约
com.arkivanov.decompose/**(上游引擎,原样编译)
com.arkivanov.essenty/**(vendored 依赖,原样编译)
        ↓ DefaultComponentContext / childStack / Value / BackDispatcher
DemoWorld.kt(演示世界:真实组件树)
        ↓ op 分发
Bridge.kt(@CName × 2,扁平 JSON 进出)
        ↓ UTF-8 JSON
N-API decomposeDispatch() / decomposeVersion()
        ↓
ArkUI Index.ets(四 Tab + 事件日志)

请求与响应:

请求 : {"op":"lifecycle","event":"resume"}
       {"op":"counter","action":"inc","value":10}
       {"op":"stack","action":"pushCounter"}
       {"op":"back"} / {"op":"saveState"} / {"op":"restoreState"}
       {"op":"recreate","configChange":"true"}
响应 : {"ok":true,"worldId":1,"rootState":"RESUMED","backEnabled":true,"exitEnabled":false,
        "stack":[{"config":"Counter#0","lifecycle":"RESUMED","count":41,"active":true}],
        "retainedId":1,"retainedOrigin":"this world","savedPreview":...,"log":[...]}
       {"ok":false,"error":"..."}

两个不对称设计:请求是扁平标量 JSON(MiniJson 只解析字符串/整数/布尔,继承自 cmp_qrose 审核模式);响应全量回带——每个 op 的响应都携带完整世界状态(徽章字段、栈视图、retained 信息)加最近 60 条日志,ArkTS 无本地状态机、重放即渲染,掉帧与重入都安全。

3.2 演示世界为什么这样建模

桥内的世界这样开:

val lifecycle = LifecycleRegistry()
val stateKeeper = StateKeeperDispatcher(savedState)
val instanceKeeper = inheritedInstanceKeeper ?: InstanceKeeperDispatcher()
val backDispatcher = BackDispatcher()

val context = DefaultComponentContext(
    lifecycle = lifecycle,
    stateKeeper = stateKeeper,
    instanceKeeper = instanceKeeper,
    backHandler = backDispatcher,
)

val navigation = StackNavigation<Config>()

val stack: Value<ChildStack<Config, DemoChild>> = context.childStack(
    source = navigation,
    serializer = Config.serializer(),
    initialStack = { listOf(Config.Counter(0)) },
    key = "DemoChildStack",
    handleBackButton = true,
    childFactory = { config, childContext -> DemoChild(childContext, config, log) },
)

Config 是 @Serializable sealed interface(Counter(id) / Info(page))——配置即协议:childStack 用它做栈的保存恢复,ArkTS 侧的 savedPreview 显示的就是它的序列化输出。每个 DemoChild 在 init 里 stateKeeper.register(KEY) { model.value }、构造时 stateKeeper.consume(KEY) ?: 0——这正是上游 counters 示例的状态保存模式,一个字没改语义。exit 兜底回调通过 stack.subscribe { exitCallback.isEnabled = it.backStack.isEmpty() } 与栈的返回回调形成互斥联动:栈非空时弹栈、栈空时 exit。

suspend 不存在于此桥——Decompose 引擎是非协程的(基于回调与 Reaktive 理念),桥全程同步一次一调用,天然不碰鸿蒙线 Dispatchers.Main 的已知问题。

3.3 错误通道

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

@CName("DecomposeDispatch")
fun decomposeDispatch(request: String): String = try {
    val json = MiniJson.parse(request)
    dispatch(json.str("op"), json)
} catch (t: Throwable) {
    JsonOut().bool("ok", false).str("error", t.message ?: t.toString()).build()
}

非法 op、世界已销毁、空栈 pop、无保存状态可恢复,全部走同一个 ok:false 通道,ArkTS 侧统一渲染到状态行。

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

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libdecompose_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohosdecompose.so)
  │ DecomposeDispatch / DecomposeVersion
  ▼
UTF-8 JSON + DisposeString

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

4.2 Kotlin/Native 导出函数
@CName("DecomposeDispatch")
fun decomposeDispatch(request: String): String   // 全部 op 一口进

@CName("DecomposeVersion")
fun decomposeVersion(): String = "3.6.0-beta01-ohos.1"  // 版本握手
C ABI 符号ArkTS 方法覆盖内核
DecomposeDispatchdecomposeDispatch(req)lifecycle / counter / observe / stack / back / saveState / restoreState / recreate / reset / status 十类 op
DecomposeVersiondecomposeVersion()fork 版本握手(真机版本行的数据来源)

example/nativeApp/build.gradle.kts 声明双 ABI sharedLib(baseName = "ohosdecompose"),并用 Gradle Copy 任务把 KN 产物与 libc++_shared.so 回填进 HAP 的 libs 目录(linkDebugShared* 任务 finalizedBy 拷贝),cxxShared() 从 ~/.konan/dependencies 定位对应 triple 的 C++ 运行时。

4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"decomposeDispatch", nullptr, NapiDecomposeDispatch, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"decomposeVersion", nullptr, NapiDecomposeVersion, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

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

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

add_library(decompose_napi SHARED napi_init.cpp)
target_link_libraries(decompose_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohosdecompose)

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

export const decomposeDispatch: (request: string) => string;
export const decomposeVersion: () => string;

第 5 阶段:ArkUI 宿主

5.1 版本握手和触发操作
aboutToAppear(): void {
  this.version = decomposeVersion();     // 版本握手:来自真实 Native 调用
  this.dispatch('reset', '', 0);         // 建世界
}

private send(request: string): void {
  const raw: string = decomposeDispatch(request);
  const parsed: DispatchResponse = JSON.parse(raw) as DispatchResponse;
  if (!parsed.ok) {
    this.errorText = parsed.error ?? 'unknown error';   // ok:false → 状态行
    return;
  }
  this.rootState = parsed.rootState ?? '-';
  this.stack = parsed.stack ?? [];
  this.log = (parsed.log ?? []).slice().reverse();      // 最新在前
  /* 其余 @State 全量刷新 */
}

ArkTS 严格模式(arkts-no-untyped-obj-literals)禁止裸对象字面量,所有请求从显式声明的类构造(DispatchRequest / DispatchIndexRequest / DispatchFlagRequest),@Builder 按钮参数全标量化——这是本仓相对早期已审仓库新增的合规点。

5.2 页面交互

四个 Tab 自上而下:生命周期 Tab 是六色状态机按钮(CREATE 深绿 / START 青 / RESUME 蓝 / PAUSE 琥珀 / STOP 深琥珀 / DESTROY 红)+ RESET 灰;计数 Tab 是 -1 琥珀 / +1 深绿 / +10 青 / 清零灰与订阅蓝 / 退订灰;导航 Tab 是 push×2 / replace 紫 / pop / 回到根 / 模拟返回键红,下方栈列表每层显示 config / lifecycle / count,backStack 项带"切到此层"按钮(popToIndex);状态保存 Tab 是保存绿 / 恢复蓝 / recreate 配置变更紫 / recreate 进程死亡红,下方显示已保存栈的序列化预览。日志区深色背景、最新在前、随每次操作全量刷新。关于 retained 实例,徽章行直接显示 retained 实例 #N(survived recreation / this world)。

5.3 对比度和错误边界

破坏性操作(DESTROY、模拟返回键、recreate 进程死亡)统一红色系;状态推进用色与语义对齐;ok:false 时错误原文进状态行与 hilog。Native 调用本身抛错(理论不该发生)同样被 try/catch 收进状态行。busy 标志防连点重入。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohosdecompose)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/org/terminator/ohos/decompose/
│       ├── Bridge.kt                         @CName × 2 + op 分发 + 状态回带
│       ├── DemoWorld.kt                      演示世界:真实组件树 + Config + DemoChild
│       └── JsonOut.kt                        手写 JSON 写出器 + MiniJson(cmp_qrose 审核模式)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.decompose、versionName 3.6.0-beta01-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/libohosdecompose_api.h
        ├── src/main/cpp/types/libdecompose_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 四 Tab 验收页面
6.2 构建、签名和安装
./gradlew :decompose:jvmTest                                    # 623 项交叉验证
./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,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填带 material 的完整配置)。

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

四、完整代码对照

4.1 整体架构

com.arkivanov.essenty/**(vendored 2.7.0-alpha01,原样编译)
    │ Lifecycle / StateKeeper / InstanceKeeper / BackHandler
com.arkivanov.decompose/**(上游引擎原样编译)
    │ ComponentContext / childStack / Value / BackDispatcher
DemoWorld.kt(演示世界:上游 counters 示例的桥接版)
    │ DecomposeDispatch / DecomposeVersion
libdecompose_napi.so
    │ decomposeDispatch / decomposeVersion
Index.ets
    │ JSON parse + @State 全量刷新
    ▼
ArkUI 真机页面(四 Tab + 事件日志)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_Decompose、Compose/Android 模块移出
gradle/libs.versions.tomlKotlin 2.4.20-0.1.0-07 + serialization ohos 制品线钉版 + 双插件别名
build.gradle.kts(根)版本 3.6.0-beta01-ohos.1、全局禁用 Ohos*Test* 任务
essenty/build.gradle.ktsvendored 模块:jvm + ohosArm64 + ohosX64,serialization 双 opt-in
essenty/src/**(35 文件)上游 Essenty 2.7.0-alpha01 逐字拷贝 + 1 个新增 ohos actual
decompose/build.gradle.kts纯 KMP:jvm + 双 ohos,nonWebMain 重建 + nonWebTest srcDir 并入 jvmTest
decompose/src/ohosMain/**(3 文件)Lock(pthread)/ CheckMainThread / PrintError actual
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt2 个 C ABI、op 分发、全量状态回带、错误通道
nativeApp/.../DemoWorld.kt真实组件树:DefaultComponentContext + childStack + DemoChild
nativeApp/.../JsonOut.kt手写 JSON 写出器 + MiniJson(扁平 JSON)
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 四 Tab 验收页面(显式请求类,ArkTS 严格模式合规)
README.OpenHarmony_CN.md鸿蒙适配主文档(20 节,5000+ 字)

4.3 关键 API 对照

层次API作用
引擎DefaultComponentContext(lifecycle, stateKeeper, instanceKeeper, backHandler)四件套组装(reset op 的世界构造)
引擎LifecycleRegistry.create/start/resume/pause/stop/destroy根状态机全驱动(lifecycle op)
引擎StackNavigation<C>.navigate { transformer }配置栈纯函数变换(stack op 的全部导航)
引擎ComponentContext.childStack(source, serializer, initialStack, handleBackButton, childFactory)演示世界的栈由上游工厂创建
引擎ChildStack.active / backStack响应里的栈视图直接读上游数据结构
引擎MutableValue / Value.subscribe / Cancellation.cancel计数与 observer(counter / observe op)
引擎BackDispatcher.back() / BackCallback(isEnabled)返回分发与 exit 兜底(back op)
引擎StateKeeperDispatcher.save() / (savedState) / register / consume状态树快照与恢复(saveState / restoreState op)
引擎InstanceKeeper.retainedInstance(key) / destroy()retained 实例存活语义(recreate op)
NativeDecomposeDispatch / DecomposeVersionC ABI JSON 导出
N-APIdecomposeDispatch(req) / decomposeVersion()向 ArkTS 暴露能力
ArkTSsend(request)触发操作并全量刷新 UI

4.4 ArkTS 与 Kotlin 的边界

const raw: string = decomposeDispatch(
  JSON.stringify(new DispatchRequest('stack', 'pushCounter', 0)));
const res: DispatchResponse = JSON.parse(raw) as DispatchResponse;
// res.stack 末尾出现新层,res.log 头部是旧 child onPause/onStop + 新 child onStart/onResume
when (op) {
    "stack" -> aliveWorld().navigate(json.str("action"), json.int("index", 0))
    "back" -> aliveWorld().back()
    // ...
}

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

五、关键决策说明

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

早期仓库抽内核,是因为 ohos 上没有它们依赖的 kotlinx 制品。本次先验证制品线:serialization 有 ohos 变体(障碍已消失),Essenty 没有——再抽内核等于把生命周期编排、状态保存恢复手抄一遍,引入漂移风险。真编译让 Decompose 引擎零改动;Essenty 从官方 sources jar 逐字 vendored(35 文件,唯一新增是 8 行 ohos serialize actual,对齐上游 nonJavaMain 变体)。“依赖有制品就换线、没有就 vendor 源码、任何时候都不手抄引擎”——这是本次适配的方法论。

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

serialization 1.11.0-0.1.0-07 基于 stdlib 2.4.20-0.1.0-07 构建,旧钉法 2.2.21 读不了 2.4 元数据;上游本来就是 Kotlin 2.3.21,2.4.20 高于它零降级。Gradle 方面 2.4.20 线支持 8.x/9.x(KStore 9.6.1 先例),上游 wrapper 8.13 原样保留——只有 2.2.21 旧线才必须降 8.14.1,两条工具链线的经验不能互相套用。

决策 3:根构建替换(cmp_qrose 模式),Compose/Android 模块留盘

上游根构建(自研 gradle-setup-plugin / AGP / Compose classpath / binary-compatibility-validator / dependency-guard / Sonatype 发布)对鸿蒙没有保留价值,整个替换为 33 行的极简 ohos 构建。extensions-compose 等 UI 绑定模块与 sample/* 留盘不进 settings.gradle.kts——Decompose 的 UI 本来就可插拔,鸿蒙侧的 ArkTS 宿主就是"又一个 UI"。

决策 4:桥同步一次一调用 + 响应全量回带

Value 的持续推送与订阅式语义过不了 C 签名(KStore 同款结论),桥是同步一次一调用;但每个响应都携带完整世界状态与滚动日志,ArkTS 无本地状态机,操作后回读即观察。演示世界的 reset/destroy/recreate 自由度让"进程死亡恢复"这类语义可以被当场演出来——生命周期本身就是被演示的对象。

决策 5:jvmTest 交叉验证 + DemoWorld 冒烟检查,双层闸门

引擎逻辑在 commonMain,JVM 与 ohos 跑同一份代码;623 项 jvmTest 跑的就是上游原测试文件(含 nonWebTest 线程测试),是"内核没漂"的闸门。在此之上,DemoWorld 的业务语义(28 项检查:生命周期全状态机、Value 发射、backStack 保留实例、返回键 exit 兜底、save/restore、retained 语义)也在 JVM 上跑——与打进 .so 的是同一份 DemoWorld.kt。本次适配正是靠这层冒烟在真机之前抓住了 exit 回调漏 register 的真 bug。

六、测试与验证

6.1 测试环境

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

6.2 JVM 交叉验证

./gradlew :decompose:jvmTest

623 项用例全部通过(上游原测试文件,零改动),按包分组:

测试包用例数钉住什么
router.children183通用子导航内核:生命周期编排、状态保存恢复、递归导航
router.stack91Child Stack:push/pop/replace 语义、backStack 保留、重复配置检测
router.panels / pages / items / slot206其余导航模型(已编进 .so,桥未开入口)
lifecycle46LifecycleRegistry 状态机与回调订阅
根包(Relay / Utils 等)43基础设施,含 AbstractThreadingTest 线程安全
backhandler22BackDispatcher 优先级与 enabled 分发
value + value.operator32MutableValue 原子性与 map 等操作符

依赖换线(Essenty vendored + serialization -0.1.0-07 线)后这套测试照跑全绿——这正是"换形不换版、引擎没漂"的直接证据。另有 DemoWorld 28 项 JVM 冒烟检查(与 .so 同一份代码)覆盖演示世界的全部业务语义。

6.3 HAP 验证

:essenty / :decompose / :decompose-test-utils compileKotlinOhos{Arm64,X64}: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / OhosX64: BUILD SUCCESSFUL(三件套回填 entry/libs)
llvm-nm -D: DecomposeDispatch / DecomposeVersion / libohosdecompose_symbols 均为全局 T 符号(双 ABI)
BuildNativeWithCmake: finished(双 ABI 编出 libdecompose_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

libohosdecompose.so debugShared 约 7.2 MB(arm64)/ 6.8 MB(x86_64),含 Kotlin/Native 运行时、Decompose 引擎、vendored Essenty 与 serialization;HAP 打包时 strip。

6.4 功能用例

  1. 启动后版本握手行显示 3.6.0-beta01-ohos.1(来自 decomposeVersion() 真实 Native 调用,非静态文字);
  2. op:reset 建世界,徽章显示 world #1,日志出现 world created: root=INITIALIZED retained=#1;
  3. 生命周期 Tab 依次 CREATE / START / RESUME:徽章 root= 变 CREATED→STARTED→RESUMED,[root] 与 [child Counter#0] 的回调成对滚动;
  4. 计数与 Value Tab:订阅 observer 后连按 +1 / +10,日志逐次出现 observer#1 (Counter#0) -> value=1/11/21/31/41(Value 发射证据,见效果图);退订后日志静默;
  5. 导航 Tab:push Counter / push Info 后栈列表增长,▶ 标记活跃层,backStack 层显示 lifecycle=CREATED(停止态)且计数保留;replace 顶部时日志显示旧顶 onDestroy (saving count=N);
  6. 模拟返回键:栈深 >1 时弹顶层(backDispatcher.back() -> handled=true);在根部再按,日志出现 [root] onBack -> stack at root, exit requested;
  7. 状态保存 Tab:保存后把计数改掉再 push,点"恢复保存的状态",栈与每层计数回到保存时刻,savedPreview 显示 [{"type":"counter","id":0},...];
  8. recreate 配置变更:retained 编号不变、徽章显示 survived recreation;recreate 进程死亡:retained 编号 +1(this world),状态依然恢复——StateKeeper 与 InstanceKeeper 的语义差异一目了然;
  9. PAUSE 后徽章 root=STARTED,DESTROY 后任何操作回 ok:false 提示 world destroyed,RESET 复活。

6.5 验证结论

导航引擎、生命周期编排、状态保存恢复、vendored 依赖、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的组件世界会话:版本握手、生命周期状态机、Value 发射、栈导航与返回分发、状态保存恢复闭环(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/Decompose.git
cd Decompose
export JAVA_HOME="/path/to/jdk-21"
./gradlew :decompose:jvmTest                                            # 623 项交叉验证
./gradlew :essenty:compileKotlinOhosArm64 :essenty:compileKotlinOhosX64 # vendored 依赖 klib
./gradlew :decompose:compileKotlinOhosArm64 :decompose:compileKotlinOhosX64
./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.decompose -m entry
hdc shell hilog | grep DecomposeNapi

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :decompose / :essenty 真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Kotlin 钉版由 serialization 制品线的 stdlib 元数据反推(2.4.20-0.1.0-07),2.2.21 旧线读不了 2.4 元数据——版本主体写对、工具链线选错,解析照样失败;
  4. vendored 文件名不能改:上游 Utils.java.kt 改名 Utils.kt 会让 JVM facade 类 UtilsKt 与 commonMain 的 Utils.kt 重名,Duplicate JVM class name——文件名也是"ABI"的一部分;
  5. serialization 1.11 新增 SealedSerializationApi:SerialDescriptor 被标 @SubclassOptInRequired,上游 Essenty 的 PolymorphicSerializer 实现它时必须补这个 opt-in(上游靠自家 gradle-setup-plugin开了 InternalSerializationApi,但社区没记录这个 1.11 新标记);
  6. 自定义 nonWebTest 中间源集炸 expect/actual:jvmTest dependsOn nonWebTest dependsOn nonWebMain 的接线会把 commonMain 拖进测试编译独立校验,报 The 'expect' declaration 'Lock' has no 'actual' declaration in module '<commonMain> for JVM';处置是 main 侧保留 nonWebMain,test 侧不建中间源集,jvmTest.kotlin.srcDir("src/nonWebTest/kotlin") 直连;
  7. ohosMain 别重复继承来的 actual:uniqueName 已由 dependsOn(nonWebMain) 继承,再写一份就是 Conflicting declarations——先接线、再补 actual、缺什么补什么;
  8. ArkTS 严格模式禁裸对象字面量(arkts-no-untyped-obj-literals,本机 DevEco 编译器比 qrose 审核时更严):所有桥请求从显式声明的类构造,@Builder 参数全标量化;ForEach 的 keyGenerator 回调没有 index 参数就不能用 index(Cannot find name 'index');
  9. BackCallback 构造不等于注册:BackCallback(isEnabled=...) 只是构造,不 backHandler.register(callback) 就永远不生效——本次由 JVM 冒烟检查在真机之前抓住;
  10. essenty 语义两则:destroy() 不能从 INITIALIZED 直接调(必须先 create,Demo 的 DESTROY 按钮对 INITIALIZED 根是 no-op,上游语义如此);onStop 后 Lifecycle.State 回到 CREATED(对齐 AndroidX,没有 STOPPED 枚举态),UI 文案写"停止态"、断言写 CREATED;
  11. 上游 .gitignore 的 /build 是根锚定,盖不住 essenty/build/ 等嵌套目录,class 文件差点进提交——改成 build/;
  12. Gradle wrapper -all 发行版本地无缓存且 services.gradle.org 不可达,换 -bin(只差文档源码);DevEco 打开工程后生成的 example/harmonyApp/.idea/ 与构建目录要单独追加 gitignore;
  13. build-profile.json5 的 signingConfigs 条目缺 material 字段会被 hvigor schema 拒绝(00303038)——仓库内留空数组且 product 不引用签名,勾选自动签名后由 IDE 回填;证书绝不进入 AtomGit;
  14. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(NAPI so + KN so + libc++)缺一不可;
  15. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free()。

8.2 已知问题

  • extensions-compose / extensions-compose-experimental / extensions-android / jetpack-component-context 未适配(依赖 Compose 与 Android),源码留盘不进构建,等社区 CMP 制品线与上游对齐后接回;
  • Child Pages / Panels / Slot / 通用 Children 已随 commonMain 编进 .so,但桥只暴露了 Child Stack 的驱动入口;
  • Essenty vendored 意味着上游 Decompose 升版若动 Essenty 版本,需要重新下载对应 sources jar 覆盖(diff 确认只有上游改动);
  • 桥是同步一次一调用,Value 持续推送不跨 C ABI,ArkTS 侧以操作后回读观察;
  • android / js / wasmJs / apple / linux 目标剥离(源文件留在树里),发布、binary-compatibility-validator、dependency-guard 一并剥离,本 fork 不发 Maven Central;
  • checkMainThread() 在 ohos 是 no-op(与上游 Linux 一致):桥可能被任意 ArkTS 线程调用;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.decompose 匹配的签名。

8.3 未来优化方向

  • 桥补 childSlot / childPages 驱动入口,覆盖其余导航模型(引擎已就位,只差桥层 op 与 ArkTS 控件);
  • Value 推送:评估 N-API 线程安全回表(tsfn),替代操作后回读;
  • Essenty ohos 制品化后撤掉 vendored 模块、换依赖坐标,回归单仓纯依赖形态;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建三道闸门;
  • 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本;
  • 跟随上游 Decompose 稳定版(3.6.0 正式版)做一次完整 re-vendor + 全量回归。

九、总结

9.1 核心难点回顾

上游 decompose + decompose-test-utils(commonMain 原样)
    → 验证依赖制品线(serialization 有 / Essenty 无)
    → Essenty 2.7.0-alpha01 源码 vendored(35 文件 + 1 个 ohos actual)
    → 工具链对齐 Kotlin 2.4.20-0.1.0-07(Gradle 8.13 沿用)
    → 根构建替换(cmp_qrose 模式,serialization 插件模块级补回)
    → ohosArm64 / ohosX64 KLIB + 3 个 ohos actual + nonWebMain 复用
    → C ABI(@CName × 2)+ 全量状态 JSON 响应
    → C++ N-API(DisposeString 纪律)
    → ArkTS 四 Tab 解析渲染(显式请求类,严格模式合规)
    → ArkUI 真机四 Tab 验收(生命周期 / Value / 导航 / 状态保存)

9.2 封装层次

  • 引擎层:上游原版 ComponentContext、childStack、Value、BackDispatcher 导航内核;
  • 依赖层:vendored Essenty LifecycleRegistry / StateKeeperDispatcher / InstanceKeeperDispatcher / BackDispatcher 四件套;
  • Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理页面状态、JSON 解析、操作分发、日志与错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 先验证生态制品线再定适配路线,制品结论分依赖逐个下:serialization 有 ohos 变体、Essenty 一个没有——于是"真编译引擎 + vendored 依赖"的混合路线才是正确答案;路线选择必须基于当天的、逐依赖的制品可达性,而不是历史结论或整体印象;
  2. 工具链版本跟着依赖走,构建经验按工具链线区分:Kotlin 钉版由 serialization 制品线的 stdlib 元数据反推(2.4.20-0.1.0-07);Gradle 经验分线(2.4.20 线留 8.x/9.x,2.2.21 旧线降 8.14.1),vendor 的文件名、opt-in 注入这类"上游构建插件的隐式行为"都要随源码一起显式补齐;
  3. 交叉验证是"内核没漂"的唯一可信证明,且要下探到 Demo 层:623 项 jvmTest 跑上游原测试文件证明引擎没漂;28 项 DemoWorld 冒烟检查跑与 .so 同一份代码,把演示语义也钉死——本次正是在这一层提前抓住了 exit 回调漏注册的真 bug,真机一次通过。

9.4 适配成果

当前适配已完成:

  • Decompose 3.6.0-beta01(fork 3.6.0-beta01-ohos.1)真实引擎的双 ABI Kotlin/Native 编译(上游 commonMain 文件零重写);
  • Essenty 2.7.0-alpha01 源码 vendored 编译(35 个文件与官方 sources jar 逐字一致,仅新增 8 行 ohos serialize actual);
  • HarmonyOS kotlinx 制品线落地(serialization-core / json 1.11.0-0.1.0-07);
  • 2 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号,双 ABI);
  • DevEco Stage ArkUI 四 Tab 验收页面(生命周期、计数与 Value、导航栈、状态保存、事件日志);
  • 623 项 JVM 交叉验证全绿(上游原测试文件)+ 28 项 DemoWorld 冒烟检查全过 + HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证;
  • AtomGit 项目文档、OpenHarmony README(中英双份,中文 5000+ 字)和本地效果图。

适配过程 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

一站式 AI 云服务平台

更多推荐