开源鸿蒙平台 KMP 三方库「Decompose」适配全流程
本文记录
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/ohosX64Gradle 变体),但 Decompose 的核心依赖 Essenty 没有任何 ohos 制品——因此本次采用"真实引擎原样编译 + 依赖源码 vendored"的混合路线:Decompose 引擎一个文件没动,Essenty2.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):
StackNavigationpush 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 / retainedOrigin | retained 实例编号与来源(本世界 / 跨重建存活)。 |
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 Multiplatform | HarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向本机 Adoptium 路径) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 8.13(沿用上游 wrapper;仅 -all 换 -bin,本地缓存可达性差异) |
| kotlinx | serialization-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 product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.decompose |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 模块 | commonMain 文件 | 依赖形态 | 处置 |
|---|---|---|---|
decompose | 100+ | Essenty 四件套 api() + serialization-core,零 UI、零 AGP | 整体编译(jvm + 双 ohos) |
decompose-test-utils | 2 | 纯 common(TestComponentContext 等) | 整体编译(JVM 测试用) |
essenty(上游依赖) | 33 | serialization-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 方法 | 覆盖内核 |
|---|---|---|
DecomposeDispatch | decomposeDispatch(req) | lifecycle / counter / observe / stack / back / saveState / restoreState / recreate / reset / status 十类 op |
DecomposeVersion | decomposeVersion() | 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.toml | Kotlin 2.4.20-0.1.0-07 + serialization ohos 制品线钉版 + 双插件别名 |
build.gradle.kts(根) | 版本 3.6.0-beta01-ohos.1、全局禁用 Ohos*Test* 任务 |
essenty/build.gradle.kts | vendored 模块: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.kt | 2 个 C ABI、op 分发、全量状态回带、错误通道 |
nativeApp/.../DemoWorld.kt | 真实组件树:DefaultComponentContext + childStack + DemoChild |
nativeApp/.../JsonOut.kt | 手写 JSON 写出器 + MiniJson(扁平 JSON) |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 四 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) |
| Native | DecomposeDispatch / DecomposeVersion | C ABI JSON 导出 |
| N-API | decomposeDispatch(req) / decomposeVersion() | 向 ArkTS 暴露能力 |
| ArkTS | send(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.children | 183 | 通用子导航内核:生命周期编排、状态保存恢复、递归导航 |
router.stack | 91 | Child Stack:push/pop/replace 语义、backStack 保留、重复配置检测 |
router.panels / pages / items / slot | 206 | 其余导航模型(已编进 .so,桥未开入口) |
lifecycle | 46 | LifecycleRegistry 状态机与回调订阅 |
| 根包(Relay / Utils 等) | 43 | 基础设施,含 AbstractThreadingTest 线程安全 |
backhandler | 22 | BackDispatcher 优先级与 enabled 分发 |
value + value.operator | 32 | MutableValue 原子性与 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 功能用例
- 启动后版本握手行显示
3.6.0-beta01-ohos.1(来自decomposeVersion()真实 Native 调用,非静态文字); op:reset建世界,徽章显示world #1,日志出现world created: root=INITIALIZED retained=#1;- 生命周期 Tab 依次 CREATE / START / RESUME:徽章
root=变 CREATED→STARTED→RESUMED,[root]与[child Counter#0]的回调成对滚动; - 计数与 Value Tab:订阅 observer 后连按 +1 / +10,日志逐次出现
observer#1 (Counter#0) -> value=1/11/21/31/41(Value 发射证据,见效果图);退订后日志静默; - 导航 Tab:push Counter / push Info 后栈列表增长,
▶标记活跃层,backStack 层显示lifecycle=CREATED(停止态)且计数保留;replace 顶部时日志显示旧顶onDestroy (saving count=N); - 模拟返回键:栈深 >1 时弹顶层(
backDispatcher.back() -> handled=true);在根部再按,日志出现[root] onBack -> stack at root, exit requested; - 状态保存 Tab:保存后把计数改掉再 push,点"恢复保存的状态",栈与每层计数回到保存时刻,
savedPreview显示[{"type":"counter","id":0},...]; - recreate 配置变更:retained 编号不变、徽章显示
survived recreation;recreate 进程死亡:retained 编号 +1(this world),状态依然恢复——StateKeeper 与 InstanceKeeper 的语义差异一目了然; - 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 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:decompose/:essenty真正编译成ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- Kotlin 钉版由 serialization 制品线的 stdlib 元数据反推(
2.4.20-0.1.0-07),2.2.21 旧线读不了 2.4 元数据——版本主体写对、工具链线选错,解析照样失败; - vendored 文件名不能改:上游
Utils.java.kt改名Utils.kt会让 JVM facade 类UtilsKt与 commonMain 的Utils.kt重名,Duplicate JVM class name——文件名也是"ABI"的一部分; - serialization 1.11 新增
SealedSerializationApi:SerialDescriptor被标@SubclassOptInRequired,上游 Essenty 的PolymorphicSerializer实现它时必须补这个 opt-in(上游靠自家 gradle-setup-plugin开了InternalSerializationApi,但社区没记录这个 1.11 新标记); - 自定义 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")直连; - ohosMain 别重复继承来的 actual:
uniqueName已由dependsOn(nonWebMain)继承,再写一份就是Conflicting declarations——先接线、再补 actual、缺什么补什么; - ArkTS 严格模式禁裸对象字面量(
arkts-no-untyped-obj-literals,本机 DevEco 编译器比 qrose 审核时更严):所有桥请求从显式声明的类构造,@Builder参数全标量化;ForEach 的 keyGenerator 回调没有index参数就不能用index(Cannot find name 'index'); - BackCallback 构造不等于注册:
BackCallback(isEnabled=...)只是构造,不backHandler.register(callback)就永远不生效——本次由 JVM 冒烟检查在真机之前抓住; - essenty 语义两则:
destroy()不能从 INITIALIZED 直接调(必须先 create,Demo 的 DESTROY 按钮对 INITIALIZED 根是 no-op,上游语义如此);onStop 后Lifecycle.State回到CREATED(对齐 AndroidX,没有 STOPPED 枚举态),UI 文案写"停止态"、断言写CREATED; - 上游
.gitignore的/build是根锚定,盖不住essenty/build/等嵌套目录,class 文件差点进提交——改成build/; - Gradle wrapper
-all发行版本地无缓存且services.gradle.org不可达,换-bin(只差文档源码);DevEco 打开工程后生成的example/harmonyApp/.idea/与构建目录要单独追加 gitignore; build-profile.json5的signingConfigs条目缺material字段会被 hvigor schema 拒绝(00303038)——仓库内留空数组且 product 不引用签名,勾选自动签名后由 IDE 回填;证书绝不进入 AtomGit;- HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so + KN so + libc++)缺一不可; - 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 三条经验
- 先验证生态制品线再定适配路线,制品结论分依赖逐个下:serialization 有 ohos 变体、Essenty 一个没有——于是"真编译引擎 + vendored 依赖"的混合路线才是正确答案;路线选择必须基于当天的、逐依赖的制品可达性,而不是历史结论或整体印象;
- 工具链版本跟着依赖走,构建经验按工具链线区分: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 注入这类"上游构建插件的隐式行为"都要随源码一起显式补齐;
- 交叉验证是"内核没漂"的唯一可信证明,且要下探到 Demo 层:623 项
jvmTest跑上游原测试文件证明引擎没漂;28 项 DemoWorld 冒烟检查跑与 .so 同一份代码,把演示语义也钉死——本次正是在这一层提前抓住了 exit 回调漏注册的真 bug,真机一次通过。
9.4 适配成果
当前适配已完成:
- Decompose
3.6.0-beta01(fork3.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。
参考文档
更多推荐




所有评论(0)