开源鸿蒙平台 KMP 三方库「Odyssey」适配全流程
本文记录
Odyssey接入 OpenHarmony 的完整过程,覆盖导航库的 Compose 依赖度盘点、鸿蒙工具链钉版、状态机与渲染层分离手术、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 四 Tab 真机页面、HAP 构建和真机验收。
Odyssey是 AlexGladkov 维护的声明式 Compose Multiplatform 导航库:push/present/ backstack、嵌套 Flow 子控制器、多栈 Tab(MultiStack,每个 Tab 独立栈)、ModalController(底部抽屉 / Alert)、LaunchFlag(singleNewTask / singleInstance / clearPrevious)、深链与 Breadcrumb 面包屑。它与社区已适配的存储类库(KStore,纯引擎零 UI)是相反的适配剖面:Odyssey 的全部价值长在 Compose UI 上,状态机(RootController/MultiStackRootController/ModalController,StateFlow 驱动)却被@Composable类型包裹。本次适配把这条边界切开——21 个 Compose 渲染文件隔离进不参与编译的src/composeMain留档,纯 Kotlin 导航状态机原样编译成 OpenHarmony KLIB,ArkUI 四 Tab Demo 通过单入口 JSON 桥驱动同一份状态机完成真机验收。
项目地址: AtomGit/oh-tpc/Odyssey.git
上游仓库: AlexGladkov/Odyssey
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
Odyssey 在 CMP 应用里提供的不是"又一个路由表",而是"把导航做成可观测的状态机":根栈 push / popBackStack / backToScreen,present 弹出带独立栈的 Flow,多栈 TabHost 每个子栈各自进化,模态栈由 ModalController 管理且根 pop 先收顶层模态,LaunchFlag 改变入栈策略,深链按 odyssey://screen/params 落到指定屏,每次转移发出 Breadcrumb 事件。这些能力全部写在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套导航语义,但上游构建(Kotlin 1.9.22 / Compose 1.6.0 / Gradle 8.6 / AGP 8.2.2,九个 target 经 buildSrc 约定插件集中声明)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面能画出来,却无法证明导航状态机在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证状态机、coroutines 运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 渲染层强绑定 | setNavigationContent / Navigator / ModalNavigator 全部 androidx.compose.*;上游 Compose 1.6 与社区鸿蒙 CMP 线版本对不齐,不能整体编译。 |
| 状态机被注解包裹 | RootController 的渲染槽位是 @Composable typealias,initServiceScreens() 在状态机里注册 Compose 内容——不剥干净就无法 headless 编译。 |
| 依赖矩阵 | 状态机依赖 kotlinx-coroutines(StateFlow),需要 ohos 变体;com.benasher44:uuid、kotlinx-collections-immutable 等上游依赖没有 ohos 制品。 |
| 构建体系不同 | buildSrc 约定插件 + AGP + vanniktech 发布与鸿蒙工具链无法共存于一个 Gradle 构建。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:导航状态机的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。栈管理、LaunchFlag、Flow/多栈/模态语义完全由上游原版实现负责,ArkTS 只负责调用 Native、解析 JSON 状态快照和渲染页面。
1.2 库提供的能力
Demo(四 Tab ArkUI 页面)提供一个真实的导航会话,对应桥接的全部入口:
- 推入/弹出:
root.push/popBackStack/backToScreen,根栈可视化展示每一层的 realKey、动画与方向; - present 流:弹出
Present流(自带独立栈),流内继续 push,收流整栈退回; - 多栈 Tab:启动
Main多栈宿主,Feed / Search / Cart 三个 Tab 各持独立栈,switchTab切换不丢栈; - 模态:
ModalController叠放底部抽屉与 Alert,根 pop 先收最顶层模态,modalDismiss立即移除; - LaunchFlag:singleNewTask 清栈重启 / singleInstance 复用 / clearPrevious 移除前一个,肉眼可见栈形变化;
- 深链:
setDeepLink+pushDeepLink/presentDeepLink,odyssey://Actions/hello直接路由; - 观测:每一步的 Breadcrumb 面包屑(
Start → Actions、Actions -> Push、tab #0 <-> #1…)实时滚动,level徽章显示根栈深度。
响应内嵌完整状态快照:
| 字段 | 作用 |
|---|---|
ok / op | 操作是否成功(失败时 error 带原文)与本次操作回显。 |
current | 当前屏:realKey / 清洗后 key / params / animation(none|push|present|fade) / time / forward。 |
remove | NavConfiguration.screenToRemove 的清洗键——上游 pop 动画的"退场屏"语义。 |
level / stack[] | 根栈深度与根 backstack 逐层快照。 |
flow / tabs | 顶部为 Flow 时的子控制器栈 / 多栈宿主的 Tab 名、位置与当前 Tab 栈。 |
modals[] / topModal | 模态栈(key、name、dialogState)与顶层模态。 |
breadcrumbs[] | 最近 10 条导航事件。 |
状态机的全部能力(LaunchFlag、深链路由、子控制器树、模态优先级)都在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 7 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的导航逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 导航状态机(RootController 607 行、双控制器、扩展集)原样编译,不重写;Compose 渲染层 21 + 4 个文件移入 composeMain 留档待工具链对齐;新增源文件仅 3 个(ohos RandomKey actual、桥、状态仓)。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);buildSrc 整体移除;odyssey-android(Hilt)与 sample/* 留盘不进构建。 |
| 桥接稳定 | 1 个 C ABI 单入口 + UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 唯一外部依赖 coroutines 换到社区 ohos 制品线(1.10.2-1.0.0);uuid 依赖以同语义实现替换(jvm java.util.UUID,ohos 零依赖计数器)。 |
| UI 完整 | 四 Tab 覆盖全部桥接入口:推入/弹出、present/模态、多栈、Launch/深链 + 面包屑日志。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。Odyssey 的渲染层(
AnimatedContent过场、ModalNavigator抽屉)留在composeMain;ArkTS Demo 画的是同一台状态机的输出——栈形、动画类型、方向、模态栈、面包屑,证明的是导航语义跨平台成立。
二、实现路线图
第 1 阶段:项目初始化 ── 模块与 Compose 依赖度盘点、coroutines ohos 制品线验证、路线定型(状态机编译 + 渲染层隔离)
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 8.14.1、buildSrc 移除
第 3 阶段:源码手术 ── composeMain 隔离(21+4 文件)、RootController 剥注解、TabItem/TabsNavModel 类型下沉、RandomKey 双平台 actual
第 4 阶段:桥协议建模 ── 14 个 op 的 JSON 契约、全量状态快照 schema、错误通道、冷启动补画
第 5 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 6 阶段:ArkUI 宿主 ── 四 Tab 页面、HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
odyssey-core/ 上游导航模型(16 个纯 Kotlin 文件,进 ohos 构建)
odyssey-compose/src/commonMain/ 上游状态机(14 个纯 Kotlin 文件,进 ohos 构建)
odyssey-compose/src/composeMain/ 上游 Compose 渲染层(21 个文件 + jvmMain 4 个 UI actual,留档不编译)
odyssey-compose/src/ohosMain/ 新增:createUniqueKey 的 ohos actual
odyssey-android/ Hilt 集成,Android 专属,留盘不进 settings
sample/ 上游各平台示例,留盘不进 settings
example/nativeApp/ Kotlin/Native 动态库(Bridge + NavigationStore + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 四 Tab 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
doc/ 本文章与真机效果图
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :odyssey-core、:odyssey-compose 与 :example:nativeApp,rootProject.name = "ohos_odyssey"。上游 buildSrc(multiplatform-setup 约定插件 + maven 发布脚本)、AGP、Hilt、vanniktech 发布全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(Temurin,org.gradle.java.home 钉 jdk-21.0.12.101-hotspot) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 8.14.1(上游 8.6;2.2.21 旧线必须降 8.x,qrose 同款) |
| coroutines | 社区 ohos 制品线 1.10.2-1.0.0(与 KMPalette 同配对) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.odyssey |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点(本适配的成败手)
逐文件统计 androidx.compose import,得到整个适配的地基:
| 模块 | commonMain 文件 | Compose 依赖形态 | 处置 |
|---|---|---|---|
odyssey-core | 16 | 零 Compose import(构建脚本里的 compose 依赖是遗留声明,实际 0 使用);唯一外部依赖 coroutines(CFlow) | 整体编译,一行不动 |
odyssey-compose | 35 | 14 个文件零 Compose import(控制器/扩展/Bundle/建图);21 个文件 1~15 个 import(渲染层) | 14 个编译,21 个隔离 |
odyssey-android | 2 | Hilt/Android 专属 | 留盘不进构建 |
关键发现有两个。其一,RootController.kt(607 行,状态机心脏)只有 3 个 Compose import:@Composable typealias × 2、Color × 1——栈管理、LaunchFlag、Flow/多栈构造、深链路由、面包屑全部是 MutableStateFlow + 纯 Kotlin。其二,TabItem / TabsNavModel 这类"机器只消费 name、UI 字段全是 Painter/Color/TextStyle"的类型,正是 qrose 适配里 Code128Type 下沉的同款情况。这两点决定了"状态机编译 + 渲染层隔离"可行。
1.4 页面边界
标题区 Odyssey · OpenHarmony / level 徽章 / 顶层模态徽章
Tab 切换 Push/Pop · Present/弹窗 · 多栈 Tab · Launch/深链
Push/Pop 当前屏卡片、根栈列表、push×2 / pop / backToScreen
Present/弹窗 流内当前屏、present 流、流内 push、ModalSheet / AlertDialog / 关闭、模态栈
多栈 Tab 启动 Main、Feed/Search/Cart 切换 chips、Tab 内推入、当前 Tab 栈
Launch/深链 LaunchFlag 选择、launch 三入口、odyssey:// 输入、setDeepLink / pushDeepLink / presentDeepLink
底部 桥状态行 + Breadcrumb 面包屑(最近 10 条,最新在前)
页面启动时先发 {"op":"create"} 建图冷启动(等效上游 setNavigationContent 的首屏绘制),之后每个按钮都是一次 odysseyDispatch 往返。
第 2 阶段:目标与依赖打通
2.1 制品线验证
先查社区 Maven(https://maven.eazytec-cloud.com)上 coroutines 的 ohos 制品:
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2-1.0.0
variants: ohosArm64Api-published / ohosX64Api-published / jvmApi-published / jvmRuntime-published
1.10.2-1.0.0 线与 HarmonyOS Kotlin 2.2.21-1.0.0 配对(KMPalette 同款组合,已过审)。与 KStore 适配对照:它需要的 coroutines 1.11.0-0.1.0-07 基于 stdlib 2.4.20,必须升 Kotlin 到 2.4.20-0.1.0-07;而 Odyssey 状态机只用到 StateFlow/Flow 基础面,1.10.2 线完全够用——版本跟着依赖走,而不是跟风升线。其余上游依赖(com.benasher44:uuid、kotlinx-collections-immutable、Compose 四件套)经盘点要么机器代码零使用,要么可以用更少的依赖同语义替换(见 3.4)。
2.2 工具链钉版与根构建替换
上游 Kotlin 1.9.22 / Compose 1.6.0 与鸿蒙工具链互斥,钉 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 同款旧线),Gradle 从 8.6 升 8.14.1。settings.gradle.kts 把 eazytec 双仓写在最前:
pluginManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
gradlePluginPortal()
google()
mavenCentral()
}
}
// dependencyResolutionManagement 同构,PREFER_PROJECT
rootProject.name = "ohos_odyssey"
include(":odyssey-core")
include(":odyssey-compose")
include(":example:nativeApp")
gradle.properties 钉死关键项:version=1.3.61-ohos.1、kotlin.native.ignoreDisabledTargets=true、kotlin.native.distribution.downloadFromMaven=true、org.gradle.java.home 指向 Temurin 21。版本目录瘦身到三项:HarmonyOS Kotlin 插件、社区 coroutines、kotlin-test。buildSrc 必须整体移除——它的构建脚本引用版本目录里已删除的 AGP/compose 条目,留着会拖垮整个配置期。
根构建只保留两件事:设 group / version,全局禁用 ohos 测试任务:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
2.3 编译状态机
两个库模块收敛为纯 KMP 形态:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {
api(project(":odyssey-core"))
implementation(libs.kotlinx.coroutines.core) // 1.10.2-1.0.0
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
}
}
实测:
./gradlew :odyssey-core:compileKotlinJvm :odyssey-compose:compileKotlinJvm # BUILD SUCCESSFUL
./gradlew :odyssey-compose:jvmTest # 7/7 PASSED
注意 odyssey-core 在鸿蒙线的正确形态:commonMain 零改动、构建脚本里遗留的 compose 依赖声明直接删除(源码本就没有一行 Compose import)。
2.4 测试依赖的处置
上游没有 commonTest。本仓新写 7 个状态机用例(上游语义的回归网),统一在 JVM 跑,ohos 侧只承担编译与链接——根构建的 Ohos*Test* 禁用规则让测试依赖永远不需要 ohos 变体。
第 3 阶段:源码手术(技术难点)
3.1 composeMain 隔离清单
21 个 commonMain Compose 文件 + 4 个 jvmMain UI actual 移入 src/composeMain(不注册源集,不参与编译,git 以 100% rename 保留历史):
setup/(Odyssey + Setup.kt、OdysseyConfiguration.kt、SetNavigationContent.kt)
local/LocalRootController.kt
base/(Navigator.kt、BottomBarNavigator.kt、AnimatedHost.kt)
animations/(AnimatedContentWithCallback.kt、Navigation + Animation.kt、Transitions.kt)
helpers/(NoRippleClickable.kt、WindowSize.kt)
navigation/modal_navigation/(ModalNavigator.kt、configuration/、views/×2)
utils/(ModalNavigator + FullScreen.kt)
extensions/(RootComposeBuilder + DSL.kt、Flow + State.kt)
navigation/bottom_bar_navigation/(原 TabItem.kt、原 BottomNavConfiguration.kt)
composeMain/jvmMain/(WindowSize、Odyssey + Setup、OdysseyConfiguration、ModalNavigator + FullScreen)
androidMain / iosMain / jsMain / macosMain / wasmJsMain 整目录无对应 target,自然不编译,原样留档。
3.2 RootController 四刀一增
607 行状态机,只动 Compose 接缝:
// ① typealias 保留签名、剥 @Composable——状态机把渲染槽位当不透明 lambda
typealias RenderWithParams<T> = (T) -> Unit
typealias Render = (key: String) -> Unit
// ② backgroundColor 从 androidx.compose.ui.graphics.Color 改为 Any?(机器只透传给子控制器)
var backgroundColor: Any? = null
// ③ 删除 @Composable RenderScreen / renderScreen 两个渲染入口(留档 composeMain)
// ④ initServiceScreens() 置空——上游在这里为 flow/multiStack 保留键注册
// CompositionLocalProvider + Navigator 的 Compose 内容;headless 构建由宿主渲染
private fun initServiceScreens() {}
// ⑤ 新增只读 backstack(qrose 的非 Compose 公共入口同思路,给 headless 宿主读栈)
val backstack: List<Screen> get() = _backstack.toList()
launch / popBackStack / backToScreen / removeTopScreen / handleLaunchFlag / proceedDeepLink / launchFlowScreen / launchMultiStackScreen / handleScreenBreadcrumbs——状态转移逻辑一行不动。
3.3 类型下沉
| 类型 | 机器消费 | UI 字段 | 处置 |
|---|---|---|---|
TabItem | name | @Composable configuration: TabConfiguration(Painter/Color/TextStyle) | commonMain 留无 Compose 版;原文件进 composeMain |
TabsNavModel / TabsNavConfiguration / TabsNavType | 多栈类型签名 | @Composable get、launchedEffect | 剥注解留 commonMain;Bottom/Top/Custom 三个 UI 配置进 composeMain |
RootComposeBuilder | 程序化建图(桥在用) | 无(一个未使用的 Color import) | 删废 import 留 commonMain |
3.4 RandomKey 双平台 actual
上游 createUniqueKey 的 actual 依赖 com.benasher44:uuid(无 ohos 制品)。同语义替换:
// jvmMain:该库在 JVM 上的实现本就是包 java.util.UUID,依赖整个去掉
actual fun createUniqueKey(key: String): String = "$key${java.util.UUID.randomUUID()}"
// ohosMain:零依赖计数器 + splitmix64,键只需进程内唯一
private val counter = AtomicLong(0L)
actual fun createUniqueKey(key: String): String {
val n = counter.incrementAndGet()
var x = (n.toULong() * 0x9E3779B97F4A7C15uL) xor (n.toULong() shl 17)
x = (x xor (x shr 30)) * 0xBF58476D1CE4E5B9uL
x = (x xor (x shr 27)) * 0x94D049BB133111EBuL
return "$key-$n-${x.toString(16)}"
}
kotlinx-collections-immutable 经盘点机器代码零使用,直接不进依赖表——最终整个 fork 的外部依赖只剩 coroutines 一项。
第 4 阶段:桥协议建模
4.1 JSON 契约
ru.alexgladkov.odyssey.*(上游状态机,原样编译)
↓ RootController / MultiStackRootController / ModalController + StateFlow
Bridge.kt + NavigationStore.kt(@CName × 1,扁平 JSON 进出)
↓ UTF-8 JSON
N-API odysseyDispatch()
↓
ArkUI Index.ets(四 Tab)
14 个 op 一口进:
create / snapshot 建图冷启动(重建状态机)/ 读全量快照
push {screen,params} 推入"活动子作用域"(流子控制器或当前 Tab,否则根)
present {screen,startScreen,params} 弹出流
launch {screen,startScreen,startTabPosition,params,flag} 通用启动 + LaunchFlag
pop / backTo {screen} 弹出 / 退回指定屏
switchTab {position} 多栈切 Tab
modalSheet {name} / modalAlert {name} / modalDismiss {key?} 模态三件套
setDeepLink {uri} 设置深链
pushDeepLink / presentDeepLink {screen,params} 深链路由(push/present 两种动画语义)
响应统一 {"ok":true,"op":"...","state":{...全量快照}} 或 {"ok":false,"op":"...","error":"..."}。全量 state 内嵌是刻意设计:C ABI 同步一来一回没有推送通道,ArkTS 每次操作后拿到的就是渲染所需的全部事实,无需订阅。
4.2 状态仓与冷启动补画
NavigationStore 持有 RootController 单例,建图镜像上游 sample(Actions / Push 简单屏,Present、TabPresent 流,Main 多栈 = Feed / Search / Cart 三 Tab 各含 Tab 屏),ensure() 时挂 ModalController 与 Breadcrumb 回调(保留最近 10 条)。
一个必须补的语义:上游里 Flow / TabHost 的首屏由 Navigator / BottomBarNavigator 调 drawCurrentScreen 画出,headless 桥要扮演这个角色:
private fun drawActiveScopeIfEmpty() {
when (val params = NavigationStore.ensure().currentScreen.value?.screen?.params) {
is FlowBundle -> if (params.rootController.backstack.isEmpty())
params.rootController.drawCurrentScreen(startScreen = params.startScreen)
is MultiStackBundle -> {
val active = params.rootController.stackChangeObserver.value ?: return
if (active.rootController.backstack.isEmpty()) active.rootController.drawCurrentScreen()
}
else -> {}
}
}
present / launch / switchTab 收尾时调用,保证 Demo 永不渲染空栈。realKey 里的保留键(odyssey_flow_reserved_type$Present)由桥侧 cleanKey 复刻上游 cleanRealKeyFromType 清洗。
4.3 错误通道与协议纪律
导出整体 try/catch,Kotlin 异常永远不跨 FFI;非法 JSON、缺 screen、未知 op、非 TabHost 上 switchTab,全部走同一个 ok:false 通道,ArkTS 统一渲染到桥状态行。JSON 解析用手写 MiniJson 扁平解析器(qrose 审核模式,KN 依赖最小化,不上 kotlinx.serialization)。Dispatchers.Main 只存在于 CFlow.watch(Swift 侧旧接口),桥全程直读 StateFlow.value / Flow.first(),不碰 Main——避开鸿蒙线主线程派发的已知问题。
第 5 阶段:原生桥接(技术难点)
5.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libodyssey_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosodyssey.so)
│ OdysseyDispatch
▼
UTF-8 JSON + DisposeString
5.2 Kotlin/Native 导出函数
@CName("OdysseyDispatch")
fun odysseyDispatch(request: String): String // 14 个 op 一口进
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
OdysseyDispatch | odysseyDispatch(req) | 建图 + push / present / launch / pop / backTo / switchTab / 模态三件套 / 深链三件套 / snapshot |
5.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"odysseyDispatch", nullptr, NapiOdysseyDispatch, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回字符串必须经 libohosodyssey_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosodyssey_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号),不拖整份生成头。
5.4 CMake 和类型声明
add_library(ohosodyssey SHARED IMPORTED)
set_target_properties(ohosodyssey PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(odyssey_napi SHARED napi_init.cpp)
target_link_libraries(odyssey_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosodyssey)
OHOS_ARCH 为空或 libohosodyssey.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const odysseyDispatch: (request: string) => string;
第 6 阶段:ArkUI 宿主
6.1 页面交互
四 Tab 全中文 UI。Push/Pop Tab:当前屏卡片(realKey、参数、动画类型/时长、方向)+ 根栈列表(带"当前"标记)+ push×2 / pop / backToScreen。Present/弹窗 Tab:流内当前屏、present 流、流内 push、ModalSheet(“分享面板”)/ AlertDialog(“确认删除”)/ 关闭、模态栈可视化(含 dialogState)。多栈 Tab:启动 Main、Feed/Search/Cart 色块 chips、Tab 内推入、当前 Tab 栈。Launch/深链 Tab:LaunchFlag 四选 chips、launch 三入口、odyssey://Actions/hello 输入框、setDeepLink / pushDeepLink / presentDeepLink。底部桥状态行(如 modalDismiss ✓ level=3)+ Breadcrumb 反向滚动列表。
6.2 构建、签名和安装
./gradlew :odyssey-compose:jvmTest # 7 项交叉验证
./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 保持空数组)。
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.odyssey -m entry
四、完整代码对照
4.1 整体架构
ru.alexgladkov.odyssey.core/**(上游模型原样编译)
│ Screen / NavConfiguration / Breadcrumb / LaunchFlag
ru.alexgladkov.odyssey.compose/**(上游状态机原样编译,Compose 剥离)
│ RootController / MultiStackRootController / ModalController + StateFlow
example/nativeApp(Bridge.kt + NavigationStore.kt + MiniJson)
│ OdysseyDispatch(@CName × 1)
libodyssey_napi.so
│ odysseyDispatch
Index.ets
│ JSON parse + @State 更新
▼
ArkUI 真机四 Tab 页面(栈形 / 动画 / 模态 / 面包屑可视化)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_odyssey、odyssey-android/sample 移出 |
gradle/libs.versions.toml | HarmonyOS Kotlin 2.2.21-1.0.0 + coroutines 1.10.2-1.0.0 + kotlin-test |
build.gradle.kts(根) | 版本 1.3.61-ohos.1、全局禁用 Ohos*Test* 任务 |
odyssey-core/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,唯一依赖 coroutines |
odyssey-compose/build.gradle.kts | 同构 + api(project(":odyssey-core")) |
odyssey-compose/src/composeMain/** | 上游 Compose 渲染层留档(21 + 4 文件,不编译) |
odyssey-compose/src/ohosMain/.../RandomKey.kt | AtomicLong + splitmix64 零依赖 actual |
odyssey-compose/src/commonTest/.../RootControllerTest.kt | 7 项状态机交叉验证 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib(baseName=ohosodyssey)、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 1 个 C ABI、14 op 分发、MiniJson、错误通道、冷启动补画 |
nativeApp/.../NavigationStore.kt | 状态机宿主、建图、全量状态快照序列化 |
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 验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(25 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 状态机 | RootController.launch | 统一入口:Simple / Flow / MultiStack 三分发 + LaunchFlag + 深链 |
| 状态机 | popBackStack / backToScreen | 弹出(模态优先)/ 递归退回指定屏 |
| 状态机 | currentScreen: StateFlow<NavConfiguration?> | 当前屏 + screenToRemove(退场屏语义) |
| 状态机 | backstack(本仓新增,只读) | headless 宿主读栈的唯一入口 |
| 状态机 | MultiStackRootController.switchTab | 多栈切换,发 Breadcrumb.TabSwitch |
| 状态机 | ModalController.present / popBackStack(key) | 模态叠放 / 按 key 移除 |
| Native | OdysseyDispatch | C ABI JSON 导出(14 op) |
| N-API | odysseyDispatch(req) | 向 ArkTS 暴露能力 |
| ArkTS | send(request) | 触发操作并以全量 state 刷新 UI |
4.4 ArkTS 与 Kotlin 的边界
const res = JSON.parse(odysseyDispatch('{"op":"push","screen":"Push","params":"#3"}')) as DispatchOk;
// res.ok === true;res.state.stack = [Actions, Push(当前)],level=2,
// res.state.current.animation='push', time=300, forward=true
"push" -> {
val target = NavigationStore.activeChildController() ?: NavigationStore.ensure()
target.push(screen = json.screenArg(), params = json.paramsArg())
NavigationStore.stateJson()
}
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:状态机编译 + Compose 隔离,而不是硬接社区 CMP 线
四条备选路线:ArkTS 重写导航(白做,栈管理/Flow/多栈/深链语义已存在且可测);把 setNavigationContent 原样编进 ohos(上游 Compose 1.6 与社区鸿蒙 CMP 线对不齐,强行对齐等于再打一套 Compose 发行版);只用 odyssey-core 丢掉状态机(RootController 等全在 odyssey-compose 里,等于重写);等社区制品齐了再做(阻塞交付)。选定路线:剥掉注解让状态机 headless 编译,渲染层留档 composeMain 待工具链对齐后接回,两条路不冲突。
决策 2:工具链钉 2.2.21-1.0.0 + Gradle 8.14.1,版本跟着依赖走
状态机只用 StateFlow/Flow 基础面,coroutines 1.10.2-1.0.0(配对 Kotlin 2.2.21-1.0.0)足够;不像 KStore 需要 1.11.0 线而必须升 Kotlin 2.4.20。2.2.21 旧线要求 Gradle 降 8.x(8.14.1,qrose 同款),上游 8.6 直接升级。两条工具链线的经验不能互相套用,钉版依据是"依赖矩阵的最小可行组合"。
决策 3:根构建替换(cmp_qrose 模式),odyssey-android / sample 留盘
上游 buildSrc 约定插件 + AGP + Hilt + vanniktech 发布对鸿蒙没有保留价值,整体替换为极简 ohos 构建。odyssey-android(Hilt 注入)是 Android 专属语义,sample/* 依赖上游 Compose 1.6 工具链——留盘不进 settings.gradle.kts,git 历史完整保留。
决策 4:单入口 JSON + 全量 state 内嵌
只导出 1 个 C ABI(OdysseyDispatch),对齐 qrose 已过审的桥面;每个变更操作的响应内嵌渲染所需的全部事实(栈形、动画、方向、模态、面包屑),同步一来一回,不需要 N-API 线程安全回表——推送通道留作未来优化。MiniJson 手写解析,KN 侧不引入 kotlinx.serialization。
决策 5:jvmTest 7 用例交叉验证(新写回归网)
上游没有测试。本仓在 commonTest 新写 7 个用例钉住状态机语义(push/pop 往返、单层 pop 触发 onApplicationFinish、SingleNewTask 清栈、ClearPrevious 栈形、Flow 子控制器构造、多栈 switchTab、模态优先级),JVM 与 ohos 跑同一份 commonMain——测试绿 + ohos 链接成功,就是"状态机没漂"的闸门。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)、JDK 21(Temurin 21.0.12)、Gradle 8.14.1、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
./gradlew :odyssey-compose:jvmTest
7 项用例全部通过:
| 用例 | 钉住什么 |
|---|---|
push_and_pop_returns_to_start | push 入栈、pop 回落,currentScreen 驱动 |
pop_on_single_root_screen_fires_application_finish | 根栈只剩一层时 pop 触发 onApplicationFinish |
launch_flag_single_new_task_clears_backstack | SingleNewTask 清栈重启 |
launch_flag_clear_previous_replaces_top | ClearPrevious 移除前一个(栈变 [start, start]) |
flow_launch_creates_child_controller | FlowBundle 参数、子控制器类型与 startScreen 路由 |
multistack_launch_and_tab_switch | 多栈构造、Tab 数量、switchTab 后 stackChangeObserver 指向 SearchTab |
modal_present_and_root_pop_dismisses_modal_first | 模态叠放、top() 语义、根 pop 先收模态、按 key 移除 |
6.3 构建链验证
:odyssey-core:compileKotlinJvm / :odyssey-compose:compileKotlinJvm: BUILD SUCCESSFUL
:odyssey-compose:jvmTest: 7/7 PASSED
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
copyKnSoArm64 / copyKnSoX64: 三件套回填 entry/libs/{arm64-v8a,x86_64}
KN so(未 strip debug)arm64 约 5.7 MB、x86_64 约 5.3 MB,连同对应 triple 的 libc++_shared.so(约 1.85 / 1.7 MB)自动拷入 HAP 工程;HAP 由 DevEco 构建签名后经 hdc install 落到真机,aa start 拉起 EntryAbility。
6.4 真机功能用例
- 启动即
create:根栈出现Actions,面包屑Start -> Actions,level=1; - Push/Pop Tab 连续
push Push(#1)(#2):栈形增长,"当前"标记上移,动画卡片显示push(300ms) 前进; pop逐层回落;根栈只剩 Actions 时再 pop,onApplicationFinish触发(应用退出语义);- Present/弹窗 Tab:
present Present后流内当前屏为PresentScreen,流内 push Push只进流栈不污染根栈,根栈第三层显示Present; ModalSheet(分享面板)+AlertDialog(确认删除)叠放:顶部出现"模态"徽章,根pop先收最顶层 Alert,modalDismiss清空后徽章消失;- 多栈 Tab:启动 Main 后
switchTab到 SearchTab,面包屑出现tab #0 <-> #1,Tab 内推入只影响当前 Tab 栈,切回 Feed 栈原样保留; - Launch/深链:
singleNewTask启动 Actions 后栈深回到 1;setDeepLink(odyssey://Actions/hello)+pushDeepLink按 URI 路由,params=hello出现在当前屏卡片; - 每步操作的桥状态行(如
modalDismiss ✓ level=3)与 Breadcrumb 列表实时留痕。
6.5 验证结论
导航状态机、coroutines 运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,四 Tab 会话展示了真实的导航状态:栈形演化、LaunchFlag 策略、Flow/多栈子控制器、模态优先级、深链路由与面包屑事件流(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/Odyssey.git
cd Odyssey
export JAVA_HOME="/path/to/jdk-21"
./gradlew :odyssey-core:compileKotlinJvm :odyssey-compose:compileKotlinJvm # 交叉验证编译
./gradlew :odyssey-compose:jvmTest # 7 项状态机回归
./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.odyssey -m entry
hdc shell hilog | grep Odyssey
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让状态机真正编译成
ohosArm64/ohosX64动态库——而导航库的前提是先把 Compose 从状态机里剥干净,@Composabletypealias、Color属性、initServiceScreens里的 Compose 注册一处不剥都在编译期炸; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- buildSrc 不能"留着不用":它引用版本目录里已删除的 AGP/compose 条目,版本目录一瘦身它的自身构建就崩,必须整体移除;
com.benasher44:uuid没有 ohos 制品——jvm actual 改java.util.UUID(该库 JVM 实现本就是包它),ohos 手写 AtomicLong + splitmix64,依赖不进 KN 就没有解析风险;- K2 编译器字符串模板
${...}里不能写::class类字面量:jsonString(it.dialogState::class.simpleName)会让解析器在模板处断裂,且报错行号指向几十行之外(级联出十几个 Unresolved + Syntax error,真实断点在模板内)——提到局部变量再插值; - 接口属性不能 smart cast:
ScreenInteractor.params判型后直接用报 “smart cast is impossible”(open/custom getter),同样先接局部变量; Dispatchers.Main只存在于CFlow.watch(iOS 侧旧观察接口),headless 桥全程直读StateFlow.value/Flow.first(),不碰 Main——避开鸿蒙线主线程派发的已知问题;- 上游 Flow/TabHost 首屏是
Navigator/BottomBarNavigator调drawCurrentScreen画的,headless 桥必须在present/launch/switchTab收尾时补drawActiveScopeIfEmpty(),否则流/Tab 栈一直为空; - 批量 sed 替换库名有大小写陷阱:
s/qrose/Odyssey/g会把libqrose_napi变成libOdyssey_napi、bundleName 变大写——so 名、包名、模块名是大小写敏感标识符,提交前必须逐个终检; - 源码文件名带空格和加号(
Odyssey + Setup.kt)在 shell 批处理时必须-print0 | read -d '',否则文件清单直接错乱; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(libodyssey_napi.so/libohosodyssey.so/libc++_shared.so)缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free();签名材料由 DevEco 自动写入build-profile.json5,提交前用git update-index --skip-worktree保护,证书绝不进入 AtomGit。
8.2 已知问题
- Compose 渲染层(
AnimatedContent过场、ModalNavigator抽屉/Alert 动画)未编译,composeMain留档待社区 CMP 鸿蒙线与上游版本对齐后接回;当前 ArkTS Demo 以文字化卡片重述动画元数据(类型/时长/方向); currentScreenStateFlow 的订阅式推送不跨 C ABI:桥是同步一次一调用,ArkTS 以操作响应内嵌的全量快照观察状态;- 桥的
params走字符串通道(扁平 MiniJson 不解嵌套),复杂参数由调用方自行编解码; odyssey-android(Hilt)与sample/*不进构建(源文件留在树里);发布、签名发布链一并剥离,本 fork 不发 Maven Central;- ohos 目标无测试运行器,状态机回归统一在 JVM;
- HAP 安装必须使用与 bundle name
org.terminator.ohos.odyssey匹配的签名。
8.3 未来优化方向
- 社区 CMP Compose 鸿蒙线推进后,把
composeMain的 21 个渲染文件接回编译,切换到真 Compose 渲染; - 增加状态变化推送通道(N-API 线程安全回表),替代操作后全量快照;
- 把 Breadcrumb 做成可订阅事件流导出(分析/埋点场景);
- 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。
九、总结
9.1 核心难点回顾
上游 odyssey-core + odyssey-compose(commonMain)
→ 依赖度盘点:core 零 Compose,compose 35 文件 = 14 纯机器 + 21 渲染层
→ 工具链对齐 Kotlin 2.2.21-1.0.0(coroutines 1.10.2-1.0.0 配对)+ Gradle 8.14.1
→ 根构建替换(cmp_qrose 模式)+ buildSrc 移除 + 模块裁剪
→ 源码手术:composeMain 隔离(21+4)、RootController 四刀一增、类型下沉、RandomKey 双平台
→ ohosArm64 / ohosX64 KLIB + 7 项 jvmTest 回归
→ C ABI(@CName × 1,14 op JSON)+ 全量 state 内嵌
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON 解析
→ ArkUI 真机四 Tab 验收(栈形 / LaunchFlag / Flow / 多栈 / 模态 / 深链 / 面包屑)
9.2 封装层次
- 模型层:上游原版
Screen/NavConfiguration/Breadcrumb/LaunchFlag; - 状态机层:上游原版
RootController/MultiStackRootController/ModalController(Compose 接缝剥离)+ 新增只读backstack; - Native 层:生成双 ABI 动态库并通过 1 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、操作分发、全量快照渲染与面包屑日志;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 导航类 CMP 库的适配钥匙是依赖度盘点而不是"能不能编 Compose":逐文件数
androidx.composeimport,把"机器消费的字段"和"UI 载荷"分开(TabItem只用name、TabsNavModel只做类型签名),状态机就自己从 Compose 里站出来了——渲染层隔离到留档源集,比硬接工具链或重写都便宜; - 版本跟着依赖矩阵走:状态机只需要 StateFlow 基础面,
coroutines 1.10.2-1.0.0+ Kotlin 2.2.21 旧线就是最小可行组合;需要 1.11.0 线的库(KStore)才必须升 2.4.20——钉版依据是依赖可达性,不是"新即正确"; - headless 桥要接住 UI 宿主的隐性职责:上游
Navigator/BottomBarNavigator不只是画页面,还负责drawCurrentScreen冷启动——漏掉这层,流和多栈的栈永远是空的;把宿主职责逐条列出来镜像,才是完整的桥。
9.4 适配成果
当前适配已完成:
- Odyssey
1.3.61导航状态机的双 ABI Kotlin/Native 编译(上游commonMain状态机零重写,手术仅限 Compose 接缝;git 以 100% rename 保留全部历史); - 1 个 C ABI 符号 + C++ N-API 包装层,14 个 op 的 JSON 协议与全量状态快照;
- DevEco Stage ArkUI 四 Tab 验收页面(推入/弹出、present/模态、多栈、LaunchFlag/深链 + 面包屑);
- 7 项 JVM 交叉验证全绿 + 双 ABI 动态库(arm64 约 5.7 MB / x86_64 约 5.3 MB debug)+ HAP 构建、签名、真机安装与运行验证;
- AtomGit 项目文档、OpenHarmony README(中英双份 + 25 节中文主文档)和本地真机效果图(适配提交
鸿蒙适配,82 个文件,+2688 / −655)。
适配过程 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)