开源鸿蒙平台 KMP 三方库「sandwich」适配全流程
本文记录
sandwich接入 OpenHarmony 的完整过程,覆盖网络响应建模库盘点、鸿蒙 ktor / kotlinx 制品线选型、真实引擎编译、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
sandwich是基于kotlinx.coroutines+kotlinx-serialization+ktor-client的 Kotlin Multiplatform 密封类网络响应建模库:用ApiResponse.Success/ApiResponse.Failure.Error/ApiResponse.Failure.Exception三态统一建模网络结果,配合操作符(operator)、映射器(mapper)、序列化钩子与重试策略,免去在业务层自建Resource/Result包装类。本次适配与社区早期路线(qrose / haze / PreCompose / voyager 的"零依赖内核提取")有本质区别:社区 Maven 上已经出现完整的 HarmonyOS 制品线(coroutines 1.11.0-0.1.0-07、serialization-json 1.11.0-0.1.0-07、ktor-client-core / ktor-client-mock 3.5.1-0.1.0-01,全部带ohosArm64/ohosX64Gradle 变体),因此不需要提取内核——真实的引擎(密封三态、operator / mapper 管道、apiResponseOf工厂、错误体反序列化、runAndRetry重试)被原样编译成 OpenHarmony KLIB,一个完整网络响应建模引擎运行在真机上,ArkUI 页面作为验收宿主。更干净的是:上游本就是纯 Kotlin KMP 库且原生发布 iOS / macOS / Linux / Windows 目标,全库唯一一处 expect(platformCoroutineDispatcher)在nonJvmMain的 actual 恰好就是鸿蒙正确解——本适配连一行库源码都不需要新增。
项目地址: AtomGit/oh-tpc/sandwich.git
上游仓库: skydoves/sandwich
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
sandwich 为 Kotlin Multiplatform 应用提供"网络响应 ↔ 三态结果"这一件事的完整答案:apiResponseOf<T> 一行把 HttpResponse 封装为 ApiResponse,2xx 进 Success、非 2xx 进 Failure.Error、传输异常进 Failure.Exception;onSuccess / onError / onException 操作符链式分发;mapper 体系做成功 / 失败体转换与全局管道(operate() / maps());deserializeErrorBody<E> / onErrorDeserialize<E> 用 kotlinx.serialization 还原错误信封;runAndRetry + RetryPolicies(fixedDelay / linear / exponentialBackoff)承担指数退避重试。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套响应建模能力,但上游构建(Kotlin 2.4.21 / Gradle 9.8.1 / AGP 9.4.1)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以显示按钮,却无法证明公共响应建模引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证密封类型引擎、ktor 集成层、序列化运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.4.21 / Gradle 9.8.1 / AGP 9.4.1 与鸿蒙工具链无法共存于一个 Gradle 构建;旧钉法 Kotlin 2.2.21-1.0.0 读不了 2.4 元数据。 |
| 依赖矩阵 | 引擎线依赖 kotlinx-coroutines、kotlinx-serialization-json,ktor 线依赖 ktor-client-core 及其传递依赖家族,每一个都要有 ohos 变体才能解析;测试依赖 mockito / turbine 没有制品。 |
| 模块分化 | sandwich-retrofit* 三件套是 Retrofit / OkHttp 的 JVM 生态专属,sandwich-ktorfit 依赖 KSP 代码生成——鸿蒙侧均无对应制品线。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 密封语义跨不了桥 | ApiResponse 是密封泛型接口,operator / mapper / retry 是组合式 Kotlin API,无法逐函数平移成同步 C 签名。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:四个纯 Kotlin 引擎模块的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。响应建模语义完全由上游原版实现负责,ArkTS 只负责调用 Native、解析 JSON 和展示结果。
1.2 库提供的能力
Demo 提供一个真实的网络响应建模会话,对应桥接的全部入口:
- 核心(core):
fakeSuccess/fakeError/fakeException构造密封三态(sandwich-test 工厂),ApiResponseOperator三回调分发,全局operate()/maps()管道,assertSuccessData断言; - ktor 管线(ktor):真实
HttpClient(MockEngine)请求管线 →apiResponseOf<T>三态分流,statusCode/headers扩展读取,引擎抛异常 →Failure.Exception; - 序列化(serialize):成功体反序列化为
@Serializable数据类,失败体走onErrorDeserialize<E>还原错误信封; - 重试(retry):
runAndRetry+RetryPolicies.fixedDelay,前置失败次数可调,逐次 attempt 留痕; - 版本握手(version):
sandwichVersion()返回 fork 版本号,来自真实 Native 调用而非静态文字。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
op / steps | 回显本次场景与逐步执行结果(每步 name + result)。 |
durationMs | 桥内场景耗时(TimeSource.Monotonic 实测)。 |
引擎侧完整能力(envelope 信封、ApiResponseMerger 合并策略、flow 管道、linear / exponentialBackoff 退避、sandwich-test 断言集)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 51 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的建模逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | sandwich / sandwich-ktor / sandwich-ktor-serialization / sandwich-test 的全部源文件原样编译,不提取、不重写、零新增库源码(唯一 expect 的 nonJvmMain actual 恰为鸿蒙正确解)。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);sandwich-retrofit* / sandwich-ktorfit / app / baselineprofile / sandwich-bom 留盘不进构建。 |
| 桥接稳定 | 使用 2 个 C ABI 和 UTF-8 JSON(kotlinx.serialization 编解码),不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 版本主体与上游完全一致,只把仓库换到 ohos 制品线(换线不换版;ktor 3.5.2 → 3.5.1-0.1.0-01 是全仓唯一版本差异),上游测试零改动全绿。 |
| UI 完整 | 五 Tab 覆盖全部桥接入口:核心 / Ktor / 序列化 / 重试 / 关于,状态行 + 时间戳操作日志 + 版本握手。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。
sandwich本身不碰 UI,适配面天然最小;ArkTS Demo 只是把桥暴露的响应建模闭环画出来。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认 ktor / kotlinx ohos 制品线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-08 钉版、serialization 插件补位
第 3 阶段:桥协议建模 ── 场景化分发契约(version/core/ktor/serialize/retry)、steps 载荷、错误通道
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 五 Tab 场景页、状态码选择器、操作日志和版本握手
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
sandwich/ 上游核心引擎(进 ohos 构建,jvm + ohosArm64 + ohosX64)
sandwich/src/{commonMain, jvmMain, nonJvmMain} ← 上游原样,零新增源码
sandwich-ktor/ 上游 ktor 集成(100% commonMain,进 ohos 构建)
sandwich-ktor-serialization/ 上游序列化钩子(commonMain + commonTest,进 ohos 构建)
sandwich-test/ 上游测试工厂与断言(commonMain + commonTest,进 ohos 构建)
example/nativeApp/ Kotlin/Native 动态库(Bridge + kotlinx.serialization DTO)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
app/ baselineprofile/ sandwich-retrofit*/ sandwich-ktorfit/ sandwich-bom/ 留盘不进 settings
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :sandwich、:sandwich-ktor、:sandwich-ktor-serialization、:sandwich-test 与 :example:nativeApp,rootProject.name = "ohos_sandwich"。上游的 spotless / dokka / vanniktech 发布插件、binary compatibility 校验、AGP classpath、baselineprofile 全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.4.20-0.1.0-08(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向 Temurin 21.0.12) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 9.8.1(沿用上游 wrapper;2.4.20 线支持 Gradle 9.x) |
| 依赖制品线 | coroutines / coroutines-test 1.11.0-0.1.0-07、serialization-json 1.11.0-0.1.0-07、ktor-client-core / ktor-client-mock 3.5.1-0.1.0-01(ohos 制品线) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.sandwich |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 模块 | 源码集形态 | 依赖形态 | 处置 |
|---|---|---|---|
sandwich | commonMain + jvmMain + nonJvmMain | 纯 Kotlin + coroutines,零 UI、零 AGP | 整体编译(jvm + 双 ohos) |
sandwich-ktor | 100% commonMain | ktor-client-core + coroutines | 整体编译(零平台源码集) |
sandwich-ktor-serialization | commonMain + commonTest | serialization-json + ktor-core | 整体编译 |
sandwich-test | commonMain + commonTest | 仅依赖 :sandwich | 整体编译 |
sandwich-retrofit* 三件套 | JVM / Android | Retrofit / OkHttp / Moshi | 留盘不进构建 |
sandwich-ktorfit | KMP + KSP | ktorfit-lib + KSP codegen | 留盘不进构建 |
expect 声明全库只有 1 处:internal expect val platformCoroutineDispatcher: CoroutineDispatcher。上游给了 jvm 与 nonJvm 两组 actual——jvmMain 是 Dispatchers.IO,nonJvmMain 是 Dispatchers.Default。鸿蒙线 coroutines 把 Dispatchers.IO 标为 internal(FFRT 调度模型下没有独立 IO 池),而 ohos 目标恰好落在 nonJvm 组——上游 nonJvmMain 的 Dispatchers.Default 就是正确解。因此本适配连 KStore 那样的"新增 8 行 ohos actual"都不需要,全仓唯一新增的 Kotlin 源文件是演示桥 Bridge.kt。
sandwich-ktor 值得单独说明:它的全部实现站在 ktor-client-core 的 HttpResponse / HttpClient 抽象上,而社区 Maven 的 ktor-client-core:3.5.1-0.1.0-01 带完整的 ohosArm64ApiElements-published / ohosX64ApiElements-published 变体(连同 ktor-client-mock 与传递依赖家族)——过去挡住网络库适配的"ohos 上没有 ktor 制品"这个障碍已经消失。上游声明 ktor 3.5.2,社区线最近的是 3.5.1-0.1.0-01,这是全仓唯一的版本差异(同 patch 系内的一个小版本差)。
1.4 页面边界
标题区 Sandwich · OpenHarmony / 版本徽章(sandwichVersion() 握手注入)
Tab 切换 核心 / Ktor / 序列化 / 重试 / 关于
核心 Tab 密封三态 + operator 分发 + operate()/maps() + assertSuccessData 步骤列表
Ktor Tab 状态码选择器(200/201/400/500)+ apiResponseOf 三态分流 + statusCode/headers 扩展
序列化 Tab 状态码选择器 + 数据类反序列化 / onErrorDeserialize 错误信封
重试 Tab 前置失败次数步进器(0-4)+ runAndRetry 逐次 attempt 留痕
状态行 最近一次场景结果(✓ 成功 / ✗ 失败 + 耗时 + 错误原文)
日志区 带时间戳的操作日志(→ 请求 / ← 响应,最新在前,上限 100 条)
关于区 版本握手、引擎清单、仓库与社区链接
页面启动时先调 sandwichVersion() 做握手,再自动触发一次 op=version 场景。
第 2 阶段:目标与依赖打通
2.1 制品线验证(本适配的成败手)
先查社区 Maven(https://maven.eazytec-cloud.com)上有没有 ohos 的 ktor / kotlinx 制品。用 .module 元数据逐个验证:
io.ktor:ktor-client-core:3.5.1-0.1.0-01
variants: ohosArm64ApiElements-published / ohosX64ApiElements-published (+ Sources)
depends on: org.jetbrains.kotlin:kotlin-stdlib:2.4.20-0.1.0-08
io.ktor:ktor-client-mock:3.5.1-0.1.0-01
variants: 同上(jvm + ohos 双线齐全)
org.jetbrains.kotlinx:kotlinx-coroutines-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: 同上
三个关键结论:其一,coroutines / serialization 版本主体与上游声明逐字相同(1.11.0),ktor 从 3.5.2 落到同 patch 系的 3.5.1-0.1.0-01——commonMain 里一行依赖坐标都不用改,只换 libs.versions.toml 的版本号;其二,ohos 制品同时发布 jvm 变体,jvm 交叉验证可以用同一个版本号,不需要双线隔离;其三,Maven Central 上的 ktor-client-core:3.5.2 没有 ohos 变体,版本必须带 -0.1.0-01 后缀且落在社区线才解析得到。测试依赖 mockito / turbine 没有制品,处置见第 2.4 节。
2.2 工具链钉版(为什么是 2.4.20-0.1.0-08)
社区此前批准的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 用的),KStore 适配时升到 2.4.20-0.1.0-07(由 coroutines 制品线的 stdlib 元数据反推)。本次适配多了一层约束:ktor-client-core:3.5.1-0.1.0-01 的 .module 元数据声明依赖 kotlin-stdlib 2.4.20-0.1.0-08——工具链版本继续跟着依赖走,Kotlin 钉 2.4.20-0.1.0-08。若沿用 -07,Gradle 会把 stdlib 解析到 -08,与编译器内嵌版本出现制品错位。额外好处:上游源码本来就是 Kotlin 2.4.21 编的,2.4.20 与之同代,源码级兼容成本为零;Gradle 9.8.1 上游 wrapper 原样沿用(2.4.20 线支持 Gradle 9.x),不像 2.2.21 旧线必须降 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()
}
}
rootProject.name = "ohos_sandwich"
include(":sandwich")
include(":sandwich-ktor")
include(":sandwich-ktor-serialization")
include(":sandwich-test")
include(":example:nativeApp")
2.3 编译真实引擎
sandwich/build.gradle.kts 收敛为纯 KMP 模块。关键点是把上游自定义层级模板原样保留,ohos 双目标加进 nonJvm 组——jvmMain(Dispatchers.IO actual)与 nonJvmMain(Dispatchers.Default actual)两个源码集目录一行不动:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
applyHierarchyTemplate {
common {
group("jvm") {
withJvm()
}
group("nonJvm") {
withOhosArm64() // nonJvmMain 的 Dispatchers.Default actual
withOhosX64() // 恰好就是鸿蒙 coroutines 的正确解
}
}
}
sourceSets {
all {
languageSettings.optIn("kotlin.contracts.ExperimentalContracts")
languageSettings.optIn("com.skydoves.sandwich.annotations.InternalSandwichApi")
}
val commonMain by getting {
dependencies {
implementation(libs.coroutines) // 1.11.0-0.1.0-07
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
implementation(libs.coroutines.test)
implementation(libs.junit)
implementation(libs.mockito.core) // 仅 JVM 测试解析,见 2.4
implementation(libs.mockito.inline)
implementation(libs.mockito.kotlin)
implementation(libs.serialization)
}
}
}
explicitApi()
}
进 include 集的另外三个模块同构:sandwich-ktor 换成 api(project(":sandwich")) + api(libs.ktor.core);sandwich-ktor-serialization 再叠 api(libs.serialization) 与 serialization 插件;sandwich-test 只依赖 :sandwich。实测:
./gradlew :sandwich:compileKotlinOhosArm64 :sandwich:compileKotlinOhosX64 # BUILD SUCCESSFUL
./gradlew :sandwich-ktor:compileKotlinOhosArm64 :sandwich-ktor-serialization:compileKotlinOhosArm64 \
:sandwich-test:compileKotlinOhosArm64 # BUILD SUCCESSFUL
四个引擎模块的全部源文件一个没动,全仓零新增库源码。
2.4 测试依赖的处置:mockito / turbine 没有 ohos 制品
commonTest 的依赖表对 ohos 测试编译同样可见,mockito / turbine 解析不到 ohos 变体就会卡死 compileTestKotlinOhosArm64。处置是两条纪律的组合:测试统一在 JVM 跑(jvmTest),根构建全局禁用 ohos 测试任务:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
这样上游测试文件(含 mockito 用例)零改动保留,ohos 侧只承担编译与链接。
第 3 阶段:桥协议建模
3.1 JSON 契约
com.skydoves.sandwich/**(上游引擎四模块,原样编译)
↓ ApiResponse 三态 / operator / mapper / retry / 序列化钩子
Bridge.kt(@CName × 2,kotlinx.serialization DTO 进出)
↓ UTF-8 JSON
N-API sandwichVersion() / sandwichDispatch()
↓
ArkUI Index.ets(五 Tab 滚动)
请求与响应:
请求 : {"op":"version"}
{"op":"core"}
{"op":"ktor","statusCode":400}
{"op":"serialize","statusCode":200}
{"op":"retry","failures":2}
响应 : {"ok":true,"op":"...","steps":[{"name":"...","result":"..."}],"durationMs":12,"error":null}
{"ok":false,"op":"-","steps":[],"durationMs":0,"error":"..."}
与 qrose / KStore 的扁平 JSON 手解析不同,本桥直接用 kotlinx.serialization 声明 DTO(BridgeRequest / BridgeResponse / BridgeStep):反正 sandwich-ktor-serialization 已经把 serialization ohos 制品线拉进了依赖图,无需再手写 MiniJson 解析器,编码端 encodeDefaults = true 保证响应字段形状恒定,ArkTS 侧 JSON.parse 一次到位。
两个设计要点:场景化分发——ApiResponse 是密封泛型接口,operator / mapper 是组合式 API,逐函数平移成 C 签名既不可能也无意义,所以桥按"业务场景"分发,一个场景内部自由组合引擎 API,逐步结果以 steps 数组返回;默认值兜底——statusCode / failures 缺省时 Kotlin 侧用默认值,ArkTS 只需传关心的字段。
3.2 引擎怎么被驱动(以 ktor 场景为例)
桥内这样开管线:
private fun mockEngine(status: Int, body: String): MockEngine = MockEngine {
respond(
content = body,
status = HttpStatusCode.fromValue(status),
headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()),
)
}
val client = HttpClient(mockEngine(status, USER_JSON))
val response: ApiResponse<User> = apiResponseOf {
client.get("https://demo.atomgit.local/users/1")
}
MockEngine 只替换传输层,请求管线(状态行、头解析、body 通道)全部真实执行;apiResponseOf<T> 的 reified 三态分流在管线之上,设备上执行的都是真实引擎代码。社区 ohos 制品线尚未发布真实网络引擎(curl 等)时,这是设备上驱动完整 ktor 管线的正确姿势——引擎制品补齐后同一桥换 engine 参数即可跑真实网络。
suspend 调用统一用 runBlocking 收进同步 C ABI:鸿蒙线 Dispatchers.Main 的跨线程派发有已知问题(resaca 仓适配时记录过),桥全程不碰 Main,引擎的 platformCoroutineDispatcher 在 ohos 映射为 Default,天然绕开。
3.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("SandwichDispatch")
fun sandwichDispatch(request: String): String = try {
val req = json.decodeFromString<BridgeRequest>(request)
/* op 分发 */
} catch (t: Throwable) {
json.encodeToString(BridgeResponse(ok = false, op = "-", error = t.message ?: t.toString()))
}
非法 JSON、不支持的 op、场景内部异常,全部走同一个 ok:false 通道,ArkTS 侧统一渲染进状态行与日志。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libsandwich_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohossandwich.so)
│ SandwichDispatch / SandwichVersion
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;密封泛型类型又无法逐函数平移,所以选择 C ABI + 场景化 JSON。
4.2 Kotlin/Native 导出函数
@CName("SandwichDispatch")
fun sandwichDispatch(request: String): String // version / core / ktor / serialize / retry 一口进
@CName("SandwichVersion")
fun sandwichVersion(): String = "2.4.0-ohos.1" // 版本握手
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
SandwichDispatch | sandwichDispatch(req) | 密封三态 + operator / mapper 管道 + apiResponseOf + 序列化钩子 + runAndRetry |
SandwichVersion | sandwichVersion() | fork 版本握手(真机版本徽章的数据来源) |
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"sandwichDispatch", nullptr, NapiSandwichDispatch, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"sandwichVersion", nullptr, NapiSandwichVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohossandwich_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohossandwich_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。
4.4 CMake 和类型声明
add_library(ohossandwich SHARED IMPORTED)
set_target_properties(ohossandwich PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(sandwich_napi SHARED napi_init.cpp)
target_link_libraries(sandwich_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohossandwich)
OHOS_ARCH 为空或 libohossandwich.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const sandwichDispatch: (request: string) => string;
export const sandwichVersion: () => string;
第 5 阶段:ArkUI 宿主
5.1 注入握手和触发场景
aboutToAppear(): void {
this.version = sandwichVersion(); // 版本握手:来自真实 Native 调用
this.runOp('version', undefined, undefined, (res: BridgeResponse) => {
this.aboutSteps = res.steps;
});
}
private dispatch(request: BridgeRequest): BridgeResponse {
const text = sandwichDispatch(JSON.stringify(request));
const res = JSON.parse(text) as BridgeResponse;
this.appendLog('→ ' + request.op + ...); // @State 刷新状态行 + 时间戳日志
this.appendLog('← ok=' + res.ok + ...);
return res;
}
每个 Tab 的运行按钮只做一件事:拼 BridgeRequest → dispatch → 把 steps 刷进对应 Tab 的 @State 数组。状态码选择器 / 失败次数步进器改变请求参数,同一场景可反复验证不同分支。
5.2 页面交互
五 Tab 自上而下:核心 Tab 直接运行(步骤列表展示密封三态与 operator 分发);Ktor / 序列化 Tab 顶部是状态码胶囊选择器(200 / 201 / 400 / 500),点击即切分支;重试 Tab 是 − / + 步进器(0-4 次前置失败);每 Tab 共享底部日志区,每条日志带 [HH:mm:ss] 时间戳,→ 行显示请求、← 行显示响应摘要,最新在前、上限 100 条。关于 Tab 展示版本握手行、引擎清单、ArkTS → NAPI → C ABI → Kotlin 引擎 → ktor 管线 分层说明与仓库 / 社区链接。
5.3 对比度和错误边界
主操作蓝底白字(GitHub 风格主色),成功态 ✓ 成功 深绿、失败态 ✗ 失败 红色,错误原文进状态行与日志。Native 调用本身抛错(理论不该发生)同样被 try/catch 收进日志区。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohossandwich)+ cxxShared + copyKnSo*
│ │ + kotlin-serialization 插件(Bridge DTO 生成 serializer)
│ └── src/ohosMain/kotlin/org/terminator/ohos/sandwich/
│ └── Bridge.kt @CName × 2 + kotlinx.serialization DTO + 五场景分发
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.sandwich、versionName 2.4.0-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/libohossandwich_api.h
├── src/main/cpp/types/libsandwich_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 五 Tab 验收页面
6.2 构建、签名和安装
./gradlew :sandwich:jvmTest :sandwich-ktor-serialization:jvmTest :sandwich-test:jvmTest # 51 项交叉验证
./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.sandwich -m entry
四、完整代码对照
4.1 整体架构
com.skydoves.sandwich/**(上游核心引擎原样编译)
│ ApiResponse 三态 / operator / mapper / operate()/maps() / runAndRetry
com.skydoves.sandwich.ktor/**(上游 ktor 集成原样编译)
│ apiResponseOf / statusCode / headers 扩展
com.skydoves.sandwich.ktor.serialization/**(上游序列化钩子原样编译)
│ deserializeErrorBody / onErrorDeserialize
com.skydoves.sandwich.test/**(上游测试工厂原样编译)
│ fakeSuccess / fakeError / fakeException / assertSuccessData
Bridge.kt + kotlinx.serialization DTO
│ SandwichDispatch / SandwichVersion
libsandwich_napi.so
│ sandwichDispatch / sandwichVersion
Index.ets
│ JSON parse + @State 更新
▼
ArkUI 真机页面(五 Tab 滚动)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_sandwich、Retrofit / ktorfit 系移出 |
gradle/libs.versions.toml | Kotlin 2.4.20-0.1.0-08 + ohos 制品线钉版(coroutines / serialization / ktor) |
build.gradle.kts(根) | 版本 2.4.0-ohos.1、全局禁用 Ohos*Test* 任务 |
sandwich*/build.gradle.kts × 4 | 纯 KMP:jvm + ohosArm64 + ohosX64,上游自定义层级模板保留(ohos 入 nonJvm 组) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填、serialization 插件 |
nativeApp/.../Bridge.kt | 2 个 C ABI、runBlocking 包裹、kotlinx.serialization DTO、五场景分发、错误通道 |
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 | 鸿蒙适配主文档(19 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | apiResponseOf<T> { … } | HttpResponse → 三态 ApiResponse(reified 分流) |
| 引擎 | ApiResponse.Success / Failure.Error / Failure.Exception | 密封三态建模 |
| 引擎 | ApiResponseOperator 三回调 | onSuccess / onError / onException 分发 |
| 引擎 | operate() / maps() | 全局 operator / mapper 管道(ApiResponse.Companion) |
| 引擎 | runAndRetry + RetryPolicies.fixedDelay | 失败重试与退避策略 |
| ktor 线 | Failure.Error.statusCode / .headers | 错误响应的状态码 / 头扩展 |
| 序列化线 | onErrorDeserialize<E> | 错误体反序列化为数据类 |
| 测试线 | fakeSuccess / fakeError / fakeException | 密封实例工厂(随编译进 .so,Demo 核心场景在用) |
| Native | SandwichDispatch / SandwichVersion | C ABI JSON 导出 |
| N-API | sandwichDispatch(req) / sandwichVersion() | 向 ArkTS 暴露能力 |
| ArkTS | dispatch(op) | 触发场景并刷新 UI |
4.4 ArkTS 与 Kotlin 的边界
const res = JSON.parse(sandwichDispatch(JSON.stringify({
op: 'ktor', statusCode: 400
}))) as BridgeResponse;
// res.ok === true;steps 含 Failure.Error(payload=...)、statusCode extension: 400、
// headers extension: application/json、apiResponseOf(engine throws) → Failure.Exception
when (req.op) {
"ktor" -> runBlocking { runKtorScenario(req.statusCode) }
...
}
private suspend fun runKtorScenario(status: Int): List<BridgeStep> {
val client = HttpClient(mockEngine(status, USER_JSON))
val response: ApiResponse<User> = apiResponseOf { client.get("https://demo.atomgit.local/users/1") }
// 2xx → Success(data=User(...));4xx/5xx → Failure.Error;引擎抛异常 → Failure.Exception
}
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:真编译整库而不是提取零依赖内核
早期仓库抽内核,是因为 ohos 上没有它们依赖的 kotlinx / ktor 制品。本次适配先验证制品线(coroutines / serialization / ktor-client 全部存在 ohos 变体),障碍已消失——再抽内核等于把响应建模引擎手抄一遍,反而引入漂移风险。真编译让密封三态、operator 管道、apiResponseOf 分流、重试退避全部是上游原件;且上游原生发布 iOS / macOS / Linux / Windows 目标,commonMain 每一行都已被上游在 Kotlin/Native 上验证过,ohos 只是同一前端的又一个链接目标。
决策 2:工具链升到 Kotlin 2.4.20-0.1.0-08,Gradle 沿用 9.8.1
ktor-client-core 3.5.1-0.1.0-01 基于 stdlib 2.4.20-0.1.0-08 构建,钉版由该制品的 .module 元数据反推;旧钉法 2.2.21 读不了 2.4 元数据,沿用 KStore 的 -07 又会与 ktor 要求的 -08 出现制品错位。上游本来就是 Kotlin 2.4.21,2.4.20 同代零降级。Gradle 方面 2.4.20 线支持 9.x,上游 wrapper 9.8.1 原样保留——只有 2.2.21 旧线才必须降 8.14.1,两条工具链线的经验不能互相套用。
决策 3:根构建替换(cmp_qrose 模式),JVM 生态模块留盘
上游根构建(spotless / dokka / vanniktech 发布 / binary compatibility / AGP classpath / baselineprofile)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。sandwich-retrofit 三件套是 Retrofit / OkHttp 的 JVM 生态(字节码 + JVM TLS),sandwich-ktorfit 依赖 KSP 代码生成,app / baselineprofile 是 Android 专属——鸿蒙没有对应制品线,留盘不进 settings.gradle.kts 是唯一正确处理,将来上游合并快进也不受影响。
决策 4:场景化分发而不是逐函数平移
ApiResponse 是密封泛型接口,operator / mapper / retry 是组合式 API,C ABI 同步签名装不下这些组合语义。桥按业务场景分发(core / ktor / serialize / retry / version),一个场景内部自由组合引擎 API,逐步结果以 steps 数组返回——Demo 的每个 Tab 就是一个场景的验收面板。JSON 编解码直接用 kotlinx.serialization DTO(而非手写 MiniJson):依赖图里本来就有 serialization ohos 制品线,DTO 声明比手写解析器更稳。
决策 5:jvmTest 交叉验证 + 测试零改动
引擎逻辑在 commonMain,JVM 与 ohos 跑同一份代码;51 项 jvmTest 跑的就是上游原测试文件,是"内核没漂"的闸门。mockito / turbine 没有 ohos 制品,处置不是改测试,而是"测试统一 JVM + 根构建禁用 Ohos*Test*"——测试文件一个字节不动,交叉验证的有效性才成立。(sandwich 核心模块上游本无测试源码,jvmTest 显示 NO-SOURCE 属正常。)
六、测试与验证
6.1 测试环境
本次验证使用 Windows 11(x86_64)、JDK 21(Temurin 21.0.12)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机 / 模拟器。
6.2 JVM 交叉验证
./gradlew :sandwich:jvmTest :sandwich-ktor-serialization:jvmTest :sandwich-test:jvmTest
51 项用例全部通过(上游原测试文件,零改动):
| 测试类 | 用例数 | 钉住什么 |
|---|---|---|
SerializationExtensionsTest(ktor-serialization) | 6 | 错误体反序列化、onErrorDeserialize 回调、MockEngine 管线往返 |
| sandwich-test 断言集 | 45 | fakeSuccess / fakeError / fakeException 工厂语义与 assertSuccess* 断言矩阵 |
sandwich 核心 | —(上游无测试源码) | 由上游原生 target 的双重背书 + 零源码改动覆盖 |
依赖换线没有 API 漂移:coroutines 1.11.0、serialization 1.11.0 换成 -0.1.0-07 线、ktor 3.5.2 落到 3.5.1-0.1.0-01 后,上游测试照跑全绿——这正是"换线不换版"的直接证据。
6.3 HAP 验证
:sandwich / :sandwich-ktor / :sandwich-ktor-serialization / :sandwich-test
compileKotlinOhosArm64 / compileKotlinOhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: SandwichDispatch / SandwichVersion / libohossandwich_symbols 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libsandwich_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
双 ABI 三件套实测大小(debugShared 未 strip,含 Kotlin/Native 运行时与 coroutines):libohossandwich.so arm64 约 12.8 MB、x86_64 约 12.1 MB,libc++_shared.so 分别约 1.9 MB / 1.7 MB;NAPI 包装 so 由 CMake 构建期生成,POST_BUILD 与前两者对齐同目录。
6.4 功能用例
- 启动后头部徽章显示
2.4.0-ohos.1、关于 Tab 出现kotlin: 2.4.20-0.1.0-08与targets: ohosArm64 + ohosX64 + jvm步骤(均来自真实 Native 调用,非静态文字); - 核心场景:三个密封实例
Success(data=User(1, OpenHarmony, harvester))、Failure.Error(payload=InternalServerError)、Failure.Exception(message=network unreachable)逐条列出,operator 三次分发各自命中onSuccess / onError / onException,assertSuccessData断言通过; - Ktor 场景选 200 / 201:
apiResponseOf(status=…) → Success(data=User(id=1, name=OpenHarmony, role=harvester)); - Ktor 场景选 400 / 500:
Failure.Error(payload=…)+statusCode extension: 400/500+headers extension: application/json;同一场景内引擎抛异常的请求归入Failure.Exception(message=connection refused),进程不崩; - 序列化场景选 200:成功体反序列化为
User(id=1, name=OpenHarmony, role=harvester);选 400:onErrorDeserialize<ErrorEnvelope>命中ErrorEnvelope(code=1001, message=Invalid request); - 重试场景失败次数=2:attempt 1、2 各留痕
retry after: …,第 3 次返回Success,total attempts: 3; - 每步操作日志按时间戳滚动留痕(
→ ktor statusCode=400/← ok=true steps=6)。
6.5 验证结论
密封三态引擎、ktor 集成层、序列化运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的响应建模会话:版本握手、三态分流、扩展读取、错误信封反序列化与重试闭环(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/sandwich.git
cd sandwich
export JAVA_HOME="/path/to/jdk-21"
./gradlew :sandwich:compileKotlinOhosArm64 :sandwich-ktor:compileKotlinOhosArm64 \
:sandwich-ktor-serialization:compileKotlinOhosArm64 :sandwich-test:compileKotlinOhosArm64 # klib
./gradlew :sandwich:jvmTest :sandwich-ktor-serialization:jvmTest :sandwich-test:jvmTest # 51 项交叉验证
./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.sandwich -m entry
hdc shell hilog | grep Sandwich
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:sandwich等引擎模块真正编译成ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;ktor-client-core在 Maven Central 上的 3.5.2 没有 ohos 变体,必须钉社区线3.5.1-0.1.0-01——版本主体写对、仓库线选错,解析照样失败;- serialization 插件最容易丢,且丢了编译期不报错:本仓
example/nativeApp(桥模块)起初只 apply 了 multiplatform 插件,@Serializable的 Bridge DTO 照常编译,直到真机运行才炸Serializer for class 'BridgeResponse' is not found——桥模块必须显式 applyorg.jetbrains.kotlin.plugin.serialization(KStore 复盘第 4 条的同款坑在桥上复发); - 鸿蒙线 coroutines 把
Dispatchers.IO标为 internal(FFRT 调度模型下没有独立 IO 池),好在 sandwich 上游把 expect 分成 jvm / nonJvm 两组,nonJvmMain的Dispatchers.Default恰好就是正确解——自定义层级模板保留、ohos 入 nonJvm 组,一行 actual 都不用补; - mockito / turbine 没有 ohos 制品:测试统一 JVM 跑 + 根构建全局禁用
Ohos*Test*任务,上游测试文件零改动; - 鸿蒙线
Dispatchers.Main跨线程派发有已知问题(resaca 记录),桥用runBlocking收 suspend、引擎调度器映射Default,全程不碰 Main; - 根构建的
/build/与.idea/*是根锚定模式,盖不住嵌套目录——DevEco 打开工程后生成的example/harmonyApp/.idea/、.clangd/.clang-tidy与example/nativeApp/build/要单独追加 gitignore,否则混进提交; build-profile.json5的signingConfigs条目缺material字段会被 hvigor schema 拒绝(报00303038 must have required property 'material')——仓库内留空数组且 product 不引用签名,勾选自动签名后由 IDE 回填完整配置;- HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(libsandwich_napi.so/libohossandwich.so/libc++_shared.so)缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - 签名材料由 DevEco 自动写入
build-profile.json5,提交前用git update-index --skip-worktree保护,证书绝不进入 AtomGit; - Windows 环境的两个特有坑:Git Bash 下跑
gradlew需先export JAVA_HOME(org.gradle.java.home在 wrapper JVM 启动后才生效);多 Gradle daemon 叠加时 KN 链接 / 测试子进程会报insufficient memory / 页面文件太小(DOS 1455)或cygheap read copy failed——./gradlew --stop、org.gradle.jvmargs降到 2g、--max-workers=2,必要时经cmd /c gradlew.bat直跑; - ArkTS 严格模式的三个编译期坑:可选参数中间留空必须显式传
undefined(回调落到statusCode位置会报类型不匹配);Button没有.onTap只有.onClick;Tabs 滚动栏枚举是BarMode.Scrollable而非ScrollableBarMode.Scrollable。
8.2 已知问题
sandwich-retrofit/sandwich-retrofit-serialization/sandwich-retrofit-datasource(Retrofit / OkHttp 生态)与sandwich-ktorfit(KSP 代码生成)不适用当前鸿蒙工具链,留盘不进构建;- 社区 ohos 制品线尚未发布真实网络引擎(curl / okio 等),Demo 的 HTTP 管线使用
ktor-client-mock(走完整 ktor 请求管线,仅替换传输层);桥协议不依赖具体引擎,制品线补齐后同一桥即可跑真实网络; - 订阅式 API(全局 operator 列表的跨请求注册语义)以场景化方式覆盖,不做逐函数平移;
ApiResponseMerger合并策略随编译进.so但未单独桥接; - ohos 目标的测试任务被禁用(runner 不可用),测试置信由 JVM 目标 + 上游原生 target 双重背书承担;
- android / js / wasmJs / apple / linux / windows 目标剥离(源文件留在树里),发布、dokka、spotless 一并剥离,本 fork 不发 Maven Central;
- HAP 安装必须使用与 bundle name
org.terminator.ohos.sandwich匹配的签名。
8.3 未来优化方向
- 待社区 ohos 制品线发布 ktor 真实引擎后,Demo 增加"真实网络请求"Tab(同一桥、换 engine 参数);
- 把
ApiResponseMerger、flow 管道等能力补上桥接入口; - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。
九、总结
9.1 核心难点回顾
上游 sandwich 四模块(commonMain / jvmMain / nonJvmMain)
→ 验证 ohos ktor / kotlinx 制品线(coroutines / serialization / ktor-client / ktor-client-mock)
→ 工具链对齐 Kotlin 2.4.20-0.1.0-08(由 ktor 制品 stdlib 元数据反推,Gradle 9.8.1 沿用)
→ 根构建替换(cmp_qrose 模式,serialization 插件桥模块补位)
→ ohosArm64 / ohosX64 KLIB + 零新增库源码(nonJvm actual 天然正确)
→ C ABI(@CName × 2)+ 场景化 JSON 分发
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON 解析
→ ArkUI 真机五 Tab 验收(核心 / Ktor / 序列化 / 重试 / 版本握手)
9.2 封装层次
- 引擎层:上游原版
ApiResponse密封三态、operator / mapper 管道、operate()/maps(); - ktor 层:上游原版
apiResponseOf工厂、statusCode/headers扩展、KtorExceptionClassifier; - 序列化层:上游原版
deserializeErrorBody/onErrorDeserialize钩子; - Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、场景分发、日志与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 先验证生态制品线再定适配路线:kotlinx 与 ktor 的 ohos 制品补齐后,网络建模类库的正确答案就是"真编译整库"——sandwich 全库只有一个 expect 且 nonJvm actual 天然正确,适配面比 KStore 还小(零新增库源码);路线选择必须基于当天的制品可达性,而不是历史结论;
- 工具链版本跟着依赖走,且要走到依赖的最深处:Kotlin 钉版不只是看 coroutines 的 stdlib(KStore 的
-07),还要看 ktor 制品声明的 stdlib(本仓的-08)——依赖图里最深的 stdlib 要求才是钉版依据;Gradle 经验按工具链线区分(2.4.20 线留 9.x,2.2.21 线降 8.14.1); - 交叉验证是"内核没漂"的唯一可信证明,但验证链要延伸到运行期:51 项
jvmTest跑的是上游原测试文件,测试零改动才让"换线不换版"有说服力;而 serialization 插件缺失这种"编译期不报错、运行期才炸"的坑,只有真机冷启动才能暴露——编译全绿不等于适配完成,设备上的第一次版本握手才是。
9.4 适配成果
当前适配已完成:
- sandwich
2.4.0(skydoves 上游)真实引擎的双 ABI Kotlin/Native 编译(四个模块源文件零重写、零新增,全仓唯一新增 Kotlin 源文件是演示桥Bridge.kt); - HarmonyOS 制品线在网络响应建模库上的完整落地(coroutines / serialization
1.11.0-0.1.0-07+ ktor-client-core / ktor-client-mock3.5.1-0.1.0-01); - 2 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证SandwichDispatch/SandwichVersion/libohossandwich_symbols全局 T 符号); - DevEco Stage ArkUI 五 Tab 验收页面(核心、Ktor、序列化、重试、关于,含状态码选择器、失败次数步进器、时间戳日志、版本握手);
- 51 项 JVM 交叉验证全绿(上游原测试文件)+ HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证;
- AtomGit 项目文档、OpenHarmony README(中英双份)、适配主文档(19 节)和本地效果图。
适配过程 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)