开源鸿蒙平台 CMP 三方库「voyager」适配全流程
本文记录
voyager接入 OpenHarmony 的完整过程,覆盖 CMP 导航库盘点、纯 Kotlin 导航内核提取、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
voyager是与 Compose 深度绑定的 CMP 导航库。本次适配不把 Compose 编进鸿蒙,而是把返回栈算法、Navigator 释放链、ScreenModelStore、生命周期存储和 ScreenRegistry 这些纯 Kotlin 内核逐文件提取到独立工程,让同一份导航状态机进入 OpenHarmony ARM64 运行时,ArkUI 页面作为真机验收宿主,Compose Multiplatform(CMP)应用可以在工具链对齐后复用同一套内核。
项目地址: AtomGit/oh-tpc/voyager.git
上游仓库: adrielcafe/voyager
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
voyager 为 Compose Multiplatform 提供导航:Navigator(screen) { } 一个 Composable 管理返回栈,Screen.Content() 声明页面,rememberScreenModel() 取与屏幕生命周期绑定的状态容器,ScreenRegistry 支撑多模块导航。KMP 项目通常把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套导航能力,但上游 12 个库模块全部深度绑定 androidx.compose,默认构建不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以显示按钮,却无法证明公共导航实现在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证导航内核、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 CMP 1.10.3 / Kotlin 2.2.21 / AGP 8.12 与社区鸿蒙 CMP 1.9.2 / HarmonyOS Kotlin 2.2.21-1.0.0 无法共存于一个 Gradle 构建。 |
| Compose 依赖 | Screen.Content() 是 @Composable,栈实现绑定 SnapshotStateList,Navigator 构造需要 SaveableStateHolder。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 生态依赖 | coroutines / atomicfu 没有现成 ohos klib 依赖可用。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在导航内核提取、双构建隔离和 C ABI/N-API 桥接层。导航语义仍由与上游逐字一致的公共实现负责,ArkTS 只负责调用 Native、解析 JSON 和展示结果。
1.2 库提供的能力
Demo 提供四个可观察场景,对应桥接的全部入口:
- 返回栈:
push/pushAll/replace/replaceAll/pop/popAll/popUntil/popUntilRoot与StackEvent(Push/Replace/Pop/Idle)事件模型,含嵌套子 Navigator 的childPush/childPop/childPopUntilRoot; - ScreenModel:
getOrPut幂等缓存、tag 隔离、bump/setState状态读写、disposeScreen/disposeNavigator作用域释放; - 生命周期:
attachA/B 两种ScreenDisposable、remove触发全部onDispose、effectOnce/hasExecuted一次性效果登记与逆序回收; - 注册表:
registerhome / posts / settings / post 四种ScreenProvider、get解析 Screen、list清单、未注册时报上游原文错误。
栈快照响应包含:
| 字段 | 作用 |
|---|---|
count / canPop / lastEvent | 栈规模、可弹出边界(minSize=1)、最近事件。 |
top / items | 栈顶名称与整栈序列(Home > Detail)。 |
level / children | 嵌套层级与子 Navigator 快照。 |
stateKeys | saveable 状态键数量,dispose 后下降。 |
lastPopped / disposed | 最近弹出与已释放的 Screen。 |
events | 本次会话事件日志(上限 60 条环形)。 |
内核层还保留上游自带的 24 个 SnapshotStateStack JVM 用例作为交叉验证。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 导航算法与释放链全部来自上游 commonMain / nativeMain 的逐文件提取,头部注释标注差异。 |
| 平台目标 | 独立 example 构建声明 ohosArm64 + ohosX64,生成双 ABI 动态库。 |
| 构建隔离 | 根 Gradle 构建零改动,12 个上游库模块、samples、build-config 全部不碰。 |
| 桥接稳定 | 使用 5 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址。 |
| CMP 兼容 | CMP 工程可在工具链对齐后复用同一内核;ArkUI 只负责真机验收。 |
| UI 完整 | 四 Tab 覆盖全部桥接入口,含快捷输入、状态卡片、事件日志和错误横幅。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。CMP 工程可以在社区 CMP 工具链对齐后直接依赖同一导航内核,在自己的 Compose 页面中使用标准 Voyager API。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块 Compose 依赖度、确定提取边界和交付物
第 2 阶段:目标与依赖打通 ── 双构建隔离、eazytec Maven、ohosArm64/ohosX64 和 so 回填
第 3 阶段:导航场景建模 ── 四类内核场景、VoyagerSession 会话和事件日志
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── JSON 快照、状态卡片、操作按钮和错误边界
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
voyager-core/ ... voyager-livedata/ 上游 12 个库模块(主构建,零改动)
example/settings.gradle.kts 独立鸿蒙构建(eazytec Maven,ohos_voyager)
example/nativeApp/ Kotlin/Native 动态库(内核提取 + Bridge)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
example 是独立 Gradle 工程(自带 8.14.1 wrapper),不把 DevEco 工程作为根项目的 Kotlin 子模块,Gradle 和 Hvigor 可以分开执行。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | 独立 example 构建使用 HarmonyOS Kotlin 2.2.21-1.0.0 |
| JDK | 21 |
| OpenHarmony 目标 | ohosArm64 + ohosX64 |
| Gradle | 8.14.1(example wrapper);根构建 8.14.2 零改动 |
| DevEco product | 6.0.1(21) |
| DevEco Studio | 6.0.1 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | cafe.adriel.voyager.ohos |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 模块 | commonMain 文件 | 零 Compose | 处置 |
|---|---|---|---|
voyager-core | 24 | 16 | 提取(Stack、concurrent、lifecycle store、registry 等) |
voyager-navigator | 8 | 2 + Navigator 类本体 | 提取(去 Composable 表面) |
voyager-screenmodel | 3 | 接口与 Store 逻辑 | 提取(StateFlow 换字段) |
voyager-transitions / tab / bottom-sheet | 5 / 2 / 2 | 0 | 不提取,ArkTS 承接 |
voyager-hilt / livedata / rxjava / kodein / koin | — | — | DI 与互操作层,与鸿蒙桥无关 |
结论:能提取的内核约占核心三模块文件数的一半,恰好是导航语义的全部。
1.4 页面边界
标题区 VOYAGER · OPENHARMONY / 版本握手行
导航区 返回栈 / ScreenModel / 生命周期 / 注册表
返回栈区 状态卡片、栈序列、目标名称输入、栈操作、嵌套导航、子 Navigator 卡片
ScreenModel 区 状态卡片、目标屏输入、类型与 tag、Store 操作、store key 展示
生命周期区 状态卡片、perScreen 清单、目标屏输入、attach/remove、effectOnce
注册表区 状态卡片、注册/解析/清单操作、provider 清单
公共区 事件日志(环形 60 条)、错误横幅
页面启动时先调 voyagerVersion() 做握手,再向四个桥各发一次 snapshot 同步初始状态。
第 2 阶段:目标与依赖打通
2.1 双构建隔离
根 settings.gradle.kts(gradlePluginPortal / google / mavenCentral + compose dev 仓库)原样保留;example/settings.gradle.kts 把 eazytec Maven 写在最前:
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_voyager"
include(":nativeApp")
三重含义:根构建不解析 HarmonyOS Kotlin 插件;example 构建不解析 AGP/compose(ohosArm64() 只在 HarmonyOS Kotlin 插件里存在);两套 wrapper 互不干扰。
2.2 构建 Native 产物
cd example
./gradlew :nativeApp:linkDebugSharedOhosArm64 :nativeApp:linkDebugSharedOhosX64
kotlin {
ohosArm64 {
binaries.sharedLib { baseName = "ohosvoyager" }
}
ohosX64 {
binaries.sharedLib { baseName = "ohosvoyager" }
}
}
copyKnSoArm64 / copyKnSoX64 由 finalizedBy 挂在链接任务后,把 libohosvoyager.so 和匹配 triple 的 libc++_shared.so 从 ~/.konan/dependencies 一并拷进:
example/harmonyApp/entry/libs/arm64-v8a/libohosvoyager.so
example/harmonyApp/entry/libs/x86_64/libohosvoyager.so
ohosMain.dependencies {} 保持为空:内核提取层零第三方依赖,libohosvoyager.so 只链接 Kotlin/Native 运行时和 libc++。
2.3 零改动消费
上游 12 个库模块、build-config 约定插件和 samples/ 不进 example 构建。鸿蒙侧不依赖任何上游 Gradle 模块,而是把纯 Kotlin 源文件按上游包名拷进 nativeApp 同一 sourceSet 一起编译,保证提取层的包结构与上游 API 完全一致。
第 3 阶段:导航场景建模
3.1 JSON 契约
cafe/adriel/voyager/**(内核提取层)
↓ Stack / Navigator / ScreenModelStore / ScreenLifecycleStore / ScreenRegistry
VoyagerSession(root Navigator + 子 Navigator + 事件环形缓冲)
↓ op 分发
Bridge.kt(@CName × 5)
↓ UTF-8 JSON
N-API voyagerStack() / voyagerScreenModel() / ...
↓
ArkUI Index.ets(四 Tab)
页面不复制导航业务判断,每个栈快照、事件和错误都由公共 Kotlin 代码生成。
3.2 会话与数据建模
private class VoyagerSession {
val root: Navigator = Navigator(
screens = listOf(DemoScreen("Home")),
key = "root",
disposeBehavior = NavigatorDisposeBehavior(),
)
private val children = LinkedHashMap<String, Navigator>()
private val events = ArrayDeque<String>() // 上限 60
private var lastPopped: DemoScreen? = null // 供 disposePopped 复现释放链
}
Demo 屏幕保持上游语义——每次导航都是新实例、新 ScreenKey:
public class DemoScreen(
public val name: String,
explicitKey: ScreenKey? = null,
) : Screen {
override val key: ScreenKey = explicitKey ?: uniqueScreenKey
}
3.3 运行逻辑
内核算法逐字来自上游。压栈与批量压栈:
public override infix fun push(item: Item) {
stateStack += item
lastEvent = StackEvent.Push
}
popUntil 的短路算法:
public override infix fun popUntil(predicate: (Item) -> Boolean): Boolean {
var success = false
val shouldPop = {
lastItemOrNull
?.let(predicate)
?.also { success = it }
?.not()
?: false
}
while (canPop && shouldPop()) {
stateStack.removeLastElement()
}
lastEvent = StackEvent.Pop
return success
}
Navigator 的尾递归父链回溯:
public fun popUntilRoot() {
popUntilRoot(this)
}
private tailrec fun popUntilRoot(navigator: Navigator) {
navigator.popAll()
if (navigator.parent != null) {
popUntilRoot(navigator.parent)
}
}
屏幕出栈后的释放链:
@InternalVoyagerApi
public fun dispose(screen: Screen) {
ScreenLifecycleStore.remove(screen)
stateKeys
.toSet()
.asSequence()
.filter { it.startsWith(screen.key) }
.forEach { key -> stateKeys -= key }
}
ScreenModelStore 的幂等缓存:
public inline fun <reified T : ScreenModel> getOrPut(
holderKey: String,
tag: String?,
factory: () -> T,
): T {
val key = getKey<T>(holderKey, tag)
lastScreenModelKey = key
return screenModels.getOrPut(key, factory) as T
}
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libvoyager_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosvoyager.so)
│ VoyagerStack / VoyagerScreenModel / ...
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制状态语义,所以选择 C ABI + JSON。
4.2 Kotlin/Native 导出函数
@CName("VoyagerVersion")
public fun voyagerVersion(): String = try {
"{\"ok\":true,\"upstream\":\"2.2.21-1.10.3\",\"fork\":\"2.2.21-1.10.3-ohos.1\",...}"
} catch (t: Throwable) {
fail(t)
}
@CName("VoyagerStack")
public fun voyagerStack(request: String): String = try {
handleStack(request)
} catch (t: Throwable) {
fail(t)
}
五个导出一览:
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
VoyagerVersion | voyagerVersion() | 工具链握手 |
VoyagerStack | voyagerStack(req) | Stack / Navigator / 嵌套导航 / dispose |
VoyagerScreenModel | voyagerScreenModel(req) | ScreenModelStore |
VoyagerLifecycle | voyagerLifecycle(req) | ScreenLifecycleStore + LifecycleEffectStore |
VoyagerRegistry | voyagerRegistry(req) | ScreenRegistry |
每个导出整体 try/catch,异常转 {"ok":false,"error":"上游原文"},Kotlin 异常永远不跨 FFI。
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"voyagerVersion", nullptr, NapiVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"voyagerStack", nullptr, NapiStack, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
// voyagerScreenModel / voyagerLifecycle / voyagerRegistry 同构
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosvoyager_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。
4.4 CMake 和类型声明
add_library(ohosvoyager SHARED IMPORTED)
set_target_properties(ohosvoyager PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(voyager_napi SHARED napi_init.cpp)
target_link_libraries(voyager_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosvoyager)
$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const voyagerVersion: () => string;
export const voyagerStack: (request: string) => string;
export const voyagerScreenModel: (request: string) => string;
export const voyagerLifecycle: (request: string) => string;
export const voyagerRegistry: (request: string) => string;
第 5 阶段:ArkUI 宿主
5.1 加载快照和触发操作
import { voyagerStack } from 'libvoyager_napi.so';
private stackOp(op: string, extra: Record<string, string> = {}): void {
const req: Record<string, string> = { 'op': op };
for (const k of Object.keys(extra)) {
req[k] = extra[k];
}
this.refreshStack(JSON.stringify(req));
}
每个响应都带全量栈快照,ArkTS 用 @State 直接刷新状态卡片、子 Navigator 卡片和事件日志,不重新实现事件顺序。
5.2 页面交互
返回栈 Tab 支持目标名称输入和四个快捷 Chip(详情 / 设置 / 编辑 / 底部弹层)、十种栈操作、四种嵌套导航操作;ScreenModel Tab 支持类型切换(counter / state)、tag 输入、六种 Store 操作;生命周期 Tab 支持 attach A/B、remove、effectOnce(effectKey 可编辑)和 hasExecuted;注册表 Tab 支持四种 provider 注册、按参数解析和未注册报错演示。
5.3 对比度和错误边界
选中 Tab 使用深蓝底白字,操作 Chip 使用浅蓝底深蓝字带描边,统计卡片数值加粗、标签浅灰。ok:false 时错误横幅红底红字显示 error 原文(例如未注册 provider 时上游的 ScreenProvider not registered: ...),Native 调用异常捕获后同样落入横幅并写 hilog。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/src/ohosMain/kotlin/
│ ├── cafe/adriel/voyager/core/stack/Stack.kt、SnapshotStateStack.kt
│ ├── cafe/adriel/voyager/core/model/ScreenModelStore.kt
│ ├── cafe/adriel/voyager/navigator/Navigator.kt
│ └── example/nativeapp/Bridge.kt、MiniJson.kt
└── harmonyApp/
├── entry/src/main/cpp/CMakeLists.txt
├── entry/src/main/cpp/napi_init.cpp
└── entry/src/main/ets/pages/Index.ets
6.2 构建、签名和安装
cd example
export JAVA_HOME="/path/to/jdk-21"
./gradlew :nativeApp:linkDebugSharedOhosArm64 :nativeApp:linkDebugSharedOhosX64
cd harmonyApp
hvigorw assembleHap --mode module -p product=default -p buildMode=debug --no-daemon
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机 signing/(已在 .gitignore),不进入仓库。
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 cafe.adriel.voyager.ohos -m entry
四、完整代码对照
4.1 整体架构
cafe/adriel/voyager/**(内核提取层)
│ Stack 算法 / Navigator 释放链 / ScreenModelStore / ScreenRegistry
▼
Bridge.kt + MiniJson.kt
│ VoyagerVersion / VoyagerStack / VoyagerScreenModel / VoyagerLifecycle / VoyagerRegistry
▼
libvoyager_napi.so
│ voyagerVersion / voyagerStack / voyagerScreenModel / voyagerLifecycle / voyagerRegistry
▼
Index.ets
│ JSON parse + @State 更新
▼
ArkUI 真机页面(四 Tab)
4.2 文件清单
| 文件 | 职责 |
|---|---|
example/settings.gradle.kts | 独立构建、eazytec Maven 置前、ohos_voyager |
example/nativeApp/build.gradle.kts | ohosArm64 / ohosX64 sharedLib、opt-in 注解、copyKnSo* 回填 |
nativeApp/.../core/stack/Stack.kt | 栈接口与 StackEvent(1:1) |
nativeApp/.../core/stack/SnapshotStateStack.kt | 栈实现(去 snapshot 载体,算法逐字) |
nativeApp/.../core/model/ScreenModelStore.kt | 幂等缓存与作用域释放(算法逐字) |
nativeApp/.../navigator/Navigator.kt | level / popUntilRoot / dispose(去 Composable 表面) |
nativeApp/.../core/registry/ScreenRegistry.kt | 多模块导航注册表(1:1) |
nativeApp/.../core/concurrent/* | ThreadSafe 并发原语 + CAS 自旋锁垫片 |
nativeApp/.../example/nativeapp/Bridge.kt | 5 个 C ABI、会话管理、op 分发 |
nativeApp/.../example/nativeapp/MiniJson.kt | 扁平 JSON 解析与转义 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath |
harmonyApp/.../ets/pages/Index.ets | ArkUI 四 Tab 验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(22 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 内核 | Stack.push/pop/popUntil | 返回栈操作与事件 |
| 内核 | Navigator.popUntilRoot | 尾递归父链回溯 |
| 内核 | Navigator.dispose | 触发 ScreenLifecycleStore 清理 |
| 内核 | ScreenModelStore.getOrPut | 幂等状态容器缓存 |
| 内核 | ScreenRegistry.register/get | 多模块导航注册与解析 |
| Native | VoyagerStack 等 ×5 | C ABI JSON 导出 |
| N-API | voyagerStack(req) 等 ×5 | 向 ArkTS 暴露数据 |
| ArkTS | stackOp(op, extra) | 触发操作并刷新 UI |
4.4 ArkTS 与 Kotlin 的边界
const res = JSON.parse(voyagerStack(JSON.stringify({ op: 'push', name: 'Detail' }))) as StackInfo;
// res.items === "Home > Detail",res.count === 2,res.lastEvent === "Push"
private fun handleStack(request: String): String {
val json = MiniJson.parse(request)
val op = MiniJson.str(json, "op", "snapshot")
// ...op 分发到内核
return stackJson(op, extra)
}
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:内核提取而不是整库编译 ohosArm64
上游 12 个模块绑 AGP/compose 插件,hilt 依赖 kapt、livedata 是纯 Android,整库换 HarmonyOS Kotlin 会在配置期失败。逐文件提取让导航语义零风险进入鸿蒙。
决策 2:双构建隔离,根构建零改动
根构建继续服务 Android / iOS / Desktop / Web 发布;example 构建钉 HarmonyOS Kotlin 2.2.21-1.0.0。两套 wrapper(8.14.2 / 8.14.1)互不干扰,上游发布物不受影响。
决策 3:零第三方依赖 + JSON 作为跨语言契约
coroutines / atomicfu 没有现成 ohos klib,ohosMain.dependencies {} 保持为空;atomicfu 锁用 kotlin.concurrent.atomics CAS 自旋锁垫片按原 API 重建。JSON 让 ArkTS、C++ 和 Kotlin/Native 的边界清楚可调试。
决策 4:算法逐字保留 + 根构建交叉验证
提取层每个文件头部注释上游路径与差异清单;根构建保留上游 24 个 SnapshotStateStack JVM 用例继续可跑,证明提取没有改坏算法。错误契约也对齐——ScreenProvider not registered: ... 等异常原文回传。
决策 5:ArkUI 作为验收宿主,CMP 复用同一内核
当前页面验证 OpenHarmony Native 链路;等社区 CMP 工具链与上游 1.10.x 对齐,提取层可以整体退场,Navigator(HomeScreen()) 直接进 ComposeArkUIViewController。
六、测试与验证
6.1 测试环境
本次真机验证使用 macOS、JDK 21(homebrew openjdk@21)、DevEco Studio、OpenHarmony ARM64 Native SDK 和 HUAWEI Mate 60 Pro,设备序列号为 FMR0223825079397。
6.2 共享样例测试
./gradlew :voyager-core:jvmTest
根构建的上游用例(含 SnapshotStateStackTest 24 个)继续全绿,作为提取层算法一致性的对照基准。
6.3 HAP 验证
linkDebugSharedOhosArm64: BUILD SUCCESSFUL
linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: VoyagerLifecycle/VoyagerRegistry/VoyagerScreenModel/VoyagerStack/VoyagerVersion 均为全局 T 符号
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
entry-default-unsigned.hap 约 6.7 MB,内含双 ABI 各三件套(libvoyager_napi.so / libohosvoyager.so / libc++_shared.so)共 6 个 .so。
6.4 功能用例
- 启动后版本行显示
2.2.21-1.10.3-ohos.1 · 上游 2.2.21-1.10.3 · Kotlin 2.2.21-1.0.0; - 返回栈 Tab:
push后 items 变Home > Detail、lastEvent=Push;连续pop到只剩 Home 后popped=false(minSize=1 生效); attachChild后childPush出现 level=1 子栈卡片;childPopUntilRoot一次清空子栈与根栈;pop后dispose 已弹出:事件日志出现 dispose 记录,stateKeys 计数下降;- ScreenModel Tab:连续
getOrPut返回同一 instance 哈希(created=false);disposeScreen后缓存数下降; - 生命周期 Tab:
attach A/B后remove,事件日志两条LoggerX.onDispose(...);effectOnce第二次first=false; - 注册表 Tab:未注册时
get显示上游原文错误;注册后get post#42得到Post#42; - 每个 op 后事件日志(环形 60 条)滚动更新。
6.5 验证结论
导航内核、Native 动态库、C ABI、N-API、ArkUI 页面和签名 HAP 已完成链路验证。应用在 HUAWEI Mate 60 Pro 上成功安装并进入前台,页面展示了真实的栈快照、事件顺序、ScreenModel 复用和释放链行为。
七、运行效果
7.1 真机截图

截图中可以看到:
- 页面标题为
Voyager · OpenHarmony,下方版本握手行显示2.2.21-1.10.3-ohos.1 · 上游 2.2.21-1.10.3 · Kotlin 2.2.21-1.0.0; - 顶部四个 Tab:返回栈 / ScreenModel / 生命周期 / 注册表,当前选中返回栈(深蓝底白字);
- 状态卡片显示栈数量、
canPop、lastEvent与stateKeys; - 栈序列
Home展示在灰色底卡片中,目标 Screen 名称输入框提供详情 / 设置 / 编辑 / 底部弹层四个快捷 Chip; - 栈操作区覆盖 push / pushAll / replace / replaceAll / pop / popAll / popUntil / popUntilRoot / dispose / 重置;
- 嵌套导航区提供 attachChild / child push / child pop / child popUntilRoot;
- 底部事件日志区滚动显示内核产生的真实事件序列。
7.2 命令速查
git clone https://atomgit.com/oh-tpc/compose-multiplatform-AnimatedImage.git
cd compose-multiplatform-AnimatedImage
export JAVA_HOME="/path/to/jdk-21"
cd example
./gradlew :nativeApp:linkDebugSharedOhosArm64 :nativeApp:linkDebugSharedOhosX64
cd ../ && ./gradlew :voyager-core:jvmTest # 根构建对照验证
cd example/harmonyApp
hvigorw assembleHap --mode module -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 cafe.adriel.voyager.ohos -m entry
hdc shell hilog | grep Voyager
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 CMP 适配,必须让导航内核真正编译成
ohosArm64/ohosX64动态库; - HarmonyOS Kotlin 的原子 API 在
kotlin.concurrent.atomics且需@OptIn(ExperimentalAtomicApi),老的kotlin.native.concurrent.AtomicInt已 ERROR 级废弃; - 构造参数默认值不能引用
this作为扩展接收者,uniqueScreenKey这类默认值要移到类体内; - 提取层需要像上游根构建一样 opt-in
InternalVoyagerApi/ExperimentalVoyagerApi,否则全屏红; - ArkTS 的
TextOverflow没有Middle枚举值,用Ellipsis; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - 签名材料写入本机
signing/并加入 .gitignore,证书不进入 AtomGit。
8.2 已知问题
- 鸿蒙侧没有 Compose UI:
Screen.Content()、ComposableNavigator()、转场 / Tab / BottomSheet 导航器不参与 ohos 编译,由 ArkTS Demo 承接; SnapshotStateStack失去 Compose 快照观察性(算法逐字保留),鸿蒙侧由 ArkTS 状态驱动刷新;StateScreenModel的 StateFlow 换成普通字段 +setState,screenModelScope未桥接;- atomicfu 锁是 CAS 自旋锁垫片,当前桥调用在 JS 线程串行、临界区极小,锁纪律等价;
- Demo reset 直接清全局 store(
newOwners/screenModels放宽为 internal),是演示需要而非上游 API; - HAP 安装必须使用与 bundle name
cafe.adriel.voyager.ohos匹配的签名。
8.3 未来优化方向
- 增加栈变化事件推送(N-API 线程安全回表),替代当前的请求-响应快照;
- 增加嵌套导航多层级(孙 Navigator)与 disposeNestedNavigators 行为的验收;
- 等社区 CMP for ohos 与上游 1.10.x 对齐后,删除提取层直接依赖上游模块,接入
ComposeArkUIViewController; - 在 CI 中加入 Native 链接、ELF 符号检查和 HAP 构建;
- 补充不同设备 ABI、API 版本的验收矩阵和自动化截图脚本。
九、总结
9.1 核心难点回顾
上游 commonMain 导航内核
→ 逐文件提取(头部标注差异)
→ ohosArm64 / ohosX64 Kotlin/Native
→ C ABI(@CName × 5)
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON 解析
→ ArkUI 真机四 Tab
9.2 封装层次
- 内核层:提供 Stack 算法、Navigator 释放链、ScreenModelStore、生命周期存储和 ScreenRegistry;
- Native 层:生成双 ABI 动态库并通过 5 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理四 Tab 状态、操作分发、错误文本和页面交互;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 先盘点模块的 Compose 依赖度,把「能提取的内核」和「必须重做的 UI」分开,再动手;
- 用双构建隔离代替改造根构建,上游发布物零风险,鸿蒙工具链版本随社区钉死;
- 算法逐字保留 + 根构建 JVM 用例对照 + 错误契约对齐,三层证明「内核没有漂」,比只跑通 Demo 更有说服力。
9.4 适配成果
当前适配已完成:
- voyager
2.2.21-1.10.3导航内核的双 ABI Kotlin/Native 编译(28 个 Kotlin 文件、约 1700 行); - 5 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号); - DevEco Stage ArkUI 四 Tab 验收页面;
- HAP 构建(双 ABI 六件套 so)、签名、设备安装和 Mate 60 Pro 真机验证;
- AtomGit 项目文档、OpenHarmony README(中英双份)和本地效果图。
适配过程 FAQ
以下问题来自
ohosArm64目标接入、NAPI 桥接、HAP 构建与真机验收的实际流程,按出现频率排序。
Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?
- 确认 DevEco Studio 版本与工程
build-profile.json5的compatibleSdkVersion一致; - 在
File > Settings > Build, Execution, Deployment > Ohpm配置可用的 ohpm 仓库镜像或代理; - 删除
oh_modules、oh-package-lock.json5与.hvigor缓存后重新 Sync; - 公司网络需要代理时,同时给 ohpm 与 hvigor 配置
http.proxy。
Q2:CMake 报 Missing .../libxxx.so 或找不到 Kotlin/Native 动态库?
- 先在
example目录执行./gradlew :nativeApp:linkDebugSharedOhosArm64(模拟器用linkDebugSharedOhosX64); - 确认
libxxx.so与libc++_shared.so已复制到ohos/entry/libs/<abi>/; - 检查
entry/build-profile.json5的abiFilters与设备 ABI 是否一致(真机arm64-v8a,模拟器x86_64); - 清理
entry/.cxx与entry/build后重试。
Q3:ArkTS 报 Cannot find module 'libxxx_napi.so' 或调用返回 undefined?
- 确认 CMake 的
add_library(<target> SHARED ...)名称与import ... from 'libxxx_napi.so'完全一致; - 解包 HAP,确认
libs/<abi>/libxxx_napi.so已打包; - 删除
entry/build、entry/.cxx与oh_modules后重新构建; - 检查
module.json5的srcEntry与页面路径是否正确。
Q4:真机安装报 signature verification failed 或 install failed?
- 在 DevEco Studio 的
Signing Configs配置证书、Profile 与密码; bundleName必须与签名 profile 中的 bundle name 一致;- 设备需加入 profile 的
deviceIds,调试证书有效期需覆盖当前时间; - 用
hdc list targets确认设备在线,再执行hdc install -r xxx-signed.hap。
Q5:应用启动白屏、闪退或 NAPI 调用崩溃?
- 用
hdc hilog | grep -i "napi\|kotlin"查看原生日志; - 确认
libc++_shared.so与 Kotlin/Native 工具链版本匹配,且 ABI 与设备一致; - 检查 C ABI 返回的字符串是否由 Native 侧分配、ArkTS 侧复制后是否调用
XxxFree释放; - 在 DevEco 的 Native 调试配置中附加
lldb,定位空指针与越界访问。
Q6:Gradle 报 ohosArm64 目标不存在或 Kotlin/Native 编译失败?
- 使用鸿蒙发行版 Kotlin(如
2.2.21-1.0.0),并在settings.gradle.kts配置 eazytec 仓库; - Gradle 建议 8.x、JDK 21,版本不匹配时先统一工具链;
- 确认
kotlin { ohosArm64() }已应用,且local.properties的 SDK 路径有效; - 清理
~/.gradle/caches中对应版本的 Kotlin/Native 缓存后重试。
Q7:HAP 构建报 SDK 版本不匹配或 compatibleSdkVersion 错误?
- 对齐 DevEco Studio、HarmonyOS SDK 与
build-profile.json5中的compatibleSdkVersion; - 升级 DevEco Studio 后同步升级 hvigor 与 SDK,避免
modelVersion与 IDE 不匹配; - 命令行构建时设置
DEVECO_SDK_HOME指向 DevEco 的sdk目录。
Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?
- 使用 DevEco 自带的 node 与 hvigor:
"$DEVECO/Contents/tools/node/bin/node" "$DEVECO/Contents/tools/hvigor/bin/hvigorw.js" ...; - 设置
JAVA_HOME、NODE_HOME、DEVECO_SDK_HOME; - 加上
--no-daemon排除守护进程缓存影响,并清理entry/build与.hvigor。
参考文档
更多推荐


所有评论(0)