本文记录 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 作用域释放;
  • 生命周期:attach A/B 两种 ScreenDisposable、remove 触发全部 onDispose、effectOnce / hasExecuted 一次性效果登记与逆序回收;
  • 注册表:register home / posts / settings / post 四种 ScreenProvider、get 解析 Screen、list 清单、未注册时报上游原文错误。

栈快照响应包含:

字段作用
count / canPop / lastEvent栈规模、可弹出边界(minSize=1)、最近事件。
top / items栈顶名称与整栈序列(Home > Detail)。
level / children嵌套层级与子 Navigator 快照。
stateKeyssaveable 状态键数量,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
JDK21
OpenHarmony 目标ohosArm64 + ohosX64
Gradle8.14.1(example wrapper);根构建 8.14.2 零改动
DevEco product6.0.1(21)
DevEco Studio6.0.1 Release
ABIarm64-v8a + x86_64
Bundlecafe.adriel.voyager.ohos
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块commonMain 文件零 Compose处置
voyager-core2416提取(Stack、concurrent、lifecycle store、registry 等)
voyager-navigator82 + Navigator 类本体提取(去 Composable 表面)
voyager-screenmodel3接口与 Store 逻辑提取(StateFlow 换字段)
voyager-transitions / tab / bottom-sheet5 / 2 / 20不提取,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 方法覆盖内核
VoyagerVersionvoyagerVersion()工具链握手
VoyagerStackvoyagerStack(req)Stack / Navigator / 嵌套导航 / dispose
VoyagerScreenModelvoyagerScreenModel(req)ScreenModelStore
VoyagerLifecyclevoyagerLifecycle(req)ScreenLifecycleStore + LifecycleEffectStore
VoyagerRegistryvoyagerRegistry(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.ktsohosArm64 / 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.ktlevel / popUntilRoot / dispose(去 Composable 表面)
nativeApp/.../core/registry/ScreenRegistry.kt多模块导航注册表(1:1)
nativeApp/.../core/concurrent/*ThreadSafe 并发原语 + CAS 自旋锁垫片
nativeApp/.../example/nativeapp/Bridge.kt5 个 C ABI、会话管理、op 分发
nativeApp/.../example/nativeapp/MiniJson.kt扁平 JSON 解析与转义
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath
harmonyApp/.../ets/pages/Index.etsArkUI 四 Tab 验收页面
README.OpenHarmony_CN.md鸿蒙适配主文档(22 节)

4.3 关键 API 对照

层次API作用
内核Stack.push/pop/popUntil返回栈操作与事件
内核Navigator.popUntilRoot尾递归父链回溯
内核Navigator.dispose触发 ScreenLifecycleStore 清理
内核ScreenModelStore.getOrPut幂等状态容器缓存
内核ScreenRegistry.register/get多模块导航注册与解析
NativeVoyagerStack 等 ×5C ABI JSON 导出
N-APIvoyagerStack(req) 等 ×5向 ArkTS 暴露数据
ArkTSstackOp(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 功能用例

  1. 启动后版本行显示 2.2.21-1.10.3-ohos.1 · 上游 2.2.21-1.10.3 · Kotlin 2.2.21-1.0.0;
  2. 返回栈 Tab:push 后 items 变 Home > Detail、lastEvent=Push;连续 pop 到只剩 Home 后 popped=false(minSize=1 生效);
  3. attachChild 后 childPush 出现 level=1 子栈卡片;childPopUntilRoot 一次清空子栈与根栈;
  4. pop 后 dispose 已弹出:事件日志出现 dispose 记录,stateKeys 计数下降;
  5. ScreenModel Tab:连续 getOrPut 返回同一 instance 哈希(created=false);disposeScreen 后缓存数下降;
  6. 生命周期 Tab:attach A/B 后 remove,事件日志两条 LoggerX.onDispose(...);effectOnce 第二次 first=false;
  7. 注册表 Tab:未注册时 get 显示上游原文错误;注册后 get post#42 得到 Post#42;
  8. 每个 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 踩坑复盘

  1. 只改 ArkTS 页面不算 CMP 适配,必须让导航内核真正编译成 ohosArm64 / ohosX64 动态库;
  2. HarmonyOS Kotlin 的原子 API 在 kotlin.concurrent.atomics 且需 @OptIn(ExperimentalAtomicApi),老的 kotlin.native.concurrent.AtomicInt 已 ERROR 级废弃;
  3. 构造参数默认值不能引用 this 作为扩展接收者,uniqueScreenKey 这类默认值要移到类体内;
  4. 提取层需要像上游根构建一样 opt-in InternalVoyagerApi / ExperimentalVoyagerApi,否则全屏红;
  5. ArkTS 的 TextOverflow 没有 Middle 枚举值,用 Ellipsis;
  6. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  7. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  8. 签名材料写入本机 signing/ 并加入 .gitignore,证书不进入 AtomGit。

8.2 已知问题

  • 鸿蒙侧没有 Compose UI:Screen.Content()、Composable Navigator()、转场 / 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 三条经验

  1. 先盘点模块的 Compose 依赖度,把「能提取的内核」和「必须重做的 UI」分开,再动手;
  2. 用双构建隔离代替改造根构建,上游发布物零风险,鸿蒙工具链版本随社区钉死;
  3. 算法逐字保留 + 根构建 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。

参考文档

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐