本文记录 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 / ohosX64 Gradle 变体),因此不需要提取内核——真实的引擎(密封三态、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 MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-08(eazytec Maven)
JDK21(org.gradle.java.home 指向 Temurin 21.0.12)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle9.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 product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.sandwich
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块源码集形态依赖形态处置
sandwichcommonMain + jvmMain + nonJvmMain纯 Kotlin + coroutines,零 UI、零 AGP整体编译(jvm + 双 ohos)
sandwich-ktor100% commonMainktor-client-core + coroutines整体编译(零平台源码集)
sandwich-ktor-serializationcommonMain + commonTestserialization-json + ktor-core整体编译
sandwich-testcommonMain + commonTest仅依赖 :sandwich整体编译
sandwich-retrofit* 三件套JVM / AndroidRetrofit / OkHttp / Moshi留盘不进构建
sandwich-ktorfitKMP + KSPktorfit-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 方法覆盖内核
SandwichDispatchsandwichDispatch(req)密封三态 + operator / mapper 管道 + apiResponseOf + 序列化钩子 + runAndRetry
SandwichVersionsandwichVersion()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.tomlKotlin 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.kt2 个 C ABI、runBlocking 包裹、kotlinx.serialization DTO、五场景分发、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 五 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 核心场景在用)
NativeSandwichDispatch / SandwichVersionC ABI JSON 导出
N-APIsandwichDispatch(req) / sandwichVersion()向 ArkTS 暴露能力
ArkTSdispatch(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 断言集45fakeSuccess / 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 功能用例

  1. 启动后头部徽章显示 2.4.0-ohos.1、关于 Tab 出现 kotlin: 2.4.20-0.1.0-08 与 targets: ohosArm64 + ohosX64 + jvm 步骤(均来自真实 Native 调用,非静态文字);
  2. 核心场景:三个密封实例 Success(data=User(1, OpenHarmony, harvester))、Failure.Error(payload=InternalServerError)、Failure.Exception(message=network unreachable) 逐条列出,operator 三次分发各自命中 onSuccess / onError / onException,assertSuccessData 断言通过;
  3. Ktor 场景选 200 / 201:apiResponseOf(status=…) → Success(data=User(id=1, name=OpenHarmony, role=harvester));
  4. Ktor 场景选 400 / 500:Failure.Error(payload=…) + statusCode extension: 400/500 + headers extension: application/json;同一场景内引擎抛异常的请求归入 Failure.Exception(message=connection refused),进程不崩;
  5. 序列化场景选 200:成功体反序列化为 User(id=1, name=OpenHarmony, role=harvester);选 400:onErrorDeserialize<ErrorEnvelope> 命中 ErrorEnvelope(code=1001, message=Invalid request);
  6. 重试场景失败次数=2:attempt 1、2 各留痕 retry after: …,第 3 次返回 Success,total attempts: 3;
  7. 每步操作日志按时间戳滚动留痕(→ 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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :sandwich 等引擎模块真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. ktor-client-core 在 Maven Central 上的 3.5.2 没有 ohos 变体,必须钉社区线 3.5.1-0.1.0-01——版本主体写对、仓库线选错,解析照样失败;
  4. serialization 插件最容易丢,且丢了编译期不报错:本仓 example/nativeApp(桥模块)起初只 apply 了 multiplatform 插件,@Serializable 的 Bridge DTO 照常编译,直到真机运行才炸 Serializer for class 'BridgeResponse' is not found——桥模块必须显式 apply org.jetbrains.kotlin.plugin.serialization(KStore 复盘第 4 条的同款坑在桥上复发);
  5. 鸿蒙线 coroutines 把 Dispatchers.IO 标为 internal(FFRT 调度模型下没有独立 IO 池),好在 sandwich 上游把 expect 分成 jvm / nonJvm 两组,nonJvmMain 的 Dispatchers.Default 恰好就是正确解——自定义层级模板保留、ohos 入 nonJvm 组,一行 actual 都不用补;
  6. mockito / turbine 没有 ohos 制品:测试统一 JVM 跑 + 根构建全局禁用 Ohos*Test* 任务,上游测试文件零改动;
  7. 鸿蒙线 Dispatchers.Main 跨线程派发有已知问题(resaca 记录),桥用 runBlocking 收 suspend、引擎调度器映射 Default,全程不碰 Main;
  8. 根构建的 /build/ 与 .idea/* 是根锚定模式,盖不住嵌套目录——DevEco 打开工程后生成的 example/harmonyApp/.idea/、.clangd / .clang-tidy 与 example/nativeApp/build/ 要单独追加 gitignore,否则混进提交;
  9. build-profile.json5 的 signingConfigs 条目缺 material 字段会被 hvigor schema 拒绝(报 00303038 must have required property 'material')——仓库内留空数组且 product 不引用签名,勾选自动签名后由 IDE 回填完整配置;
  10. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(libsandwich_napi.so / libohossandwich.so / libc++_shared.so)缺一不可;
  11. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  12. 签名材料由 DevEco 自动写入 build-profile.json5,提交前用 git update-index --skip-worktree 保护,证书绝不进入 AtomGit;
  13. 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 直跑;
  14. 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 三条经验

  1. 先验证生态制品线再定适配路线:kotlinx 与 ktor 的 ohos 制品补齐后,网络建模类库的正确答案就是"真编译整库"——sandwich 全库只有一个 expect 且 nonJvm actual 天然正确,适配面比 KStore 还小(零新增库源码);路线选择必须基于当天的制品可达性,而不是历史结论;
  2. 工具链版本跟着依赖走,且要走到依赖的最深处:Kotlin 钉版不只是看 coroutines 的 stdlib(KStore 的 -07),还要看 ktor 制品声明的 stdlib(本仓的 -08)——依赖图里最深的 stdlib 要求才是钉版依据;Gradle 经验按工具链线区分(2.4.20 线留 9.x,2.2.21 线降 8.14.1);
  3. 交叉验证是"内核没漂"的唯一可信证明,但验证链要延伸到运行期: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-mock 3.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。

参考文档

Logo

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

更多推荐