本文记录
EitherNet接入 OpenHarmony 的完整过程,覆盖 KMP/CMP 工程盘点、
ohosArm64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkTS 示例页面、结果类型验证、HAP 构建、签名边界和效果验证。
EitherNet 的核心职责是把网络请求结果建模为
ApiResult密封类型。适配后的示例没有在 ArkTS 中重新实现一套结果判断,而是让 JVM 测试和 OpenHarmony 页面
共同消费同一份 Kotlin 共享逻辑,再通过 JSON 把稳定结果交给 ArkTS。
项目地址: AtomGit/oh-tpc/EitherNet
上游仓库: square/retrofit
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做开源鸿蒙平台 KMP/CMP 适配
EitherNet 原本是 Kotlin Multiplatform 网络结果建模库,公共 API 位于
commonMain,Retrofit 集成主要面向 JVM。要让同一份结果模型进入 OpenHarmony,
不能只把示例页面重写成 ArkTS,还需要让 Kotlin 代码真正编译为 OpenHarmony
ARM64 Native 产物,并建立可维护的跨语言边界。
如果只在页面里写几个成功和失败文本,无法证明 ApiResult 的 fold、错误类型、
回退值和重试逻辑仍然由共享代码负责。本次适配需要解决下面几个问题:
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | KMP 模块默认只有 JVM 和 Apple Native,需要增加 ohosArm64()。 |
| 工具链不一致 | Kotlin/Native、OpenHarmony Native SDK、Gradle 和 JDK 必须匹配。 |
| 依赖边界 | kotlinx-coroutines 和 Okio 的 OpenHarmony 产物必须能被 Native 目标解析。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin ApiResult 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 内存生命周期 | Kotlin/Native 返回的字符串必须由明确的 ApiFree 释放。 |
| 异步语义 | 重试 API 是挂起函数,Native 桥接需要在边界内安全调用 runBlocking。 |
| 交付链路复杂 | Native 动态库、CMake、HAP、签名和 ARM64 依赖需要分别验证。 |
因此,本项目把适配边界放在三个地方:Kotlin/Native 目标配置、C ABI/N-API
桥接层和 ArkTS 页面层。ApiResult 的状态语义仍由 KMP 维护,ArkTS 只负责解析
JSON、展示场景和触发示例操作。
1.2 库提供的能力
EitherNet 的公共类型是 ApiResult<out T, out E>,它用密封类型表达一次请求的
完整结果:
ApiResult.Success<T>:请求成功,response保存业务数据;ApiResult.Failure.NetworkFailure:网络连接、超时等传输层异常;ApiResult.Failure.HttpFailure:HTTP 状态码失败,并保留状态码和错误值;ApiResult.Failure.ApiFailure:业务转换器解码出的接口错误;ApiResult.Failure.UnknownFailure:无法归类的原始异常。
调用方可以使用穷举 when,在一个结果分支中处理所有情况:
kotlin
when (val result = service.load()) {
is ApiResult.Success -> render(result.response)
is ApiResult.Failure.NetworkFailure -> showOffline(result.error)
is ApiResult.Failure.HttpFailure -> showHttpError(result.code)
is ApiResult.Failure.ApiFailure -> showApiError(result.error)
is ApiResult.Failure.UnknownFailure -> showUnknown(result.error)
}
本次 OpenHarmony 示例覆盖以下实际 API:
| EitherNet 能力 | 示例行为 |
|---|---|
ApiResult.success |
构造 Success(value=Po) 场景。 |
ApiResult.httpFailure |
构造 HTTP 503 场景并展示状态码。 |
ApiResult.apiFailure |
保留 quota exceeded 业务错误。 |
ApiResult.networkFailure |
使用 Okio IOException 表示连接超时。 |
ApiResult.unknownFailure |
保留 IllegalStateException 未知错误。 |
fold / successOrElse |
统一生成摘要和失败回退值。 |
retryWithExponentialBackoff |
前两次失败,第三次恢复成功。 |
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | ApiResult、fold、失败分类、回退和重试由 Kotlin 共享。 |
| 平台目标 | 为核心库和示例加入 ohosArm64,生成 libeithernet.so。 |
| 桥接稳定 | 使用少量 C ABI 函数和 UTF-8 JSON,不暴露 Kotlin 对象地址。 |
| UI 完整 | ArkTS 页面支持目录读取、场景切换、重试和公共检查。 |
| 可测试 | JVM 测试、Native 编译、ELF 依赖检查和 HAP 构建分别验收。 |
| 签名安全 | 源码只保留未签名工程,证书和密钥材料由开发者在外部目录配置。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
example/ohosApp是验证 KMP 共享结果模型的 DevEco Stage 宿主。它没有把EitherNet 的业务判断复制到 ArkTS,页面展示的五类结果、重试摘要和
10/10检查结果都来自 Kotlin/Native 动态库。
二、实现路线图
text
第 1 阶段:项目初始化 ── 盘点 ApiResult 公共 API、版本和示例边界
第 2 阶段:目标与依赖打通 ── 加入 ohosArm64、独立 example 和 Native 构建任务
第 3 阶段:结果与序列化 ── 建立五类结果场景、摘要、回退和 JSON 契约
第 4 阶段:原生桥接 ── Kotlin/Native C ABI、C++ N-API、类型检查和释放
第 5 阶段:ArkTS 宿主封装 ── JSON 解析、页面状态、重试按钮和错误展示
第 6 阶段:示例与验证 ── Native 依赖检查、HAP 构建、签名交接和效果图
每个阶段都使用真实产物作为下一阶段输入:先用 JVM 验证共享结果模型,再把相同
代码链接为 ARM64 动态库,随后由 CMake 和 N-API 加载到 Stage 工程,最后构建
未签名 HAP 并保留外置签名入口。
三、逐步实现过程
第 1 阶段:项目初始化
1.1 盘点公共 API 和工程边界
项目采用与参考 uuid 工程一致的分层方式:
text
eithernet/ KMP 核心库、ApiResult 和 ohosArm64 目标
example/shared/ 平台无关的 EitherNet 场景和 JVM 验收测试
example/nativeApp/ ohosArm64 Kotlin/Native 动态库
example/ohosApp/ DevEco Stage 工程、C++ N-API 和 ArkTS 页面
scripts/ Native 构建、依赖检查和签名交接脚本
docs/openharmony/ 验收清单和效果图
example 是独立 Gradle 工程,不把 DevEco 工程当作 Kotlin 子模块。这样可以
分别执行 Gradle 和 Hvigor,也可以把 ohosApp 复制到其他目录后在 DevEco Studio
中配置签名,而不污染源码工程。
1.2 固定工具链和版本矩阵
本项目使用以下版本约定:
| 项目 | 配置 | 用途 |
|---|---|---|
| Kotlin Multiplatform | 2.2.21-1.0.0 |
JVM、Kotlin/Native 和 KLIB |
| JDK | 21 | Gradle、Kotlin 编译和 Native 任务 |
| OpenHarmony 目标 | ohosArm64 |
ARM64 真机动态库 |
| kotlinx-coroutines | 1.10.2-1.0.0 |
挂起函数、重试和测试 |
| Okio | 3.16.4-1.0.0 |
Native 可用的 IO 异常类型 |
| ABI | arm64-v8a |
HAP 原生库架构 |
| 适配版本 | 2.1.0-ohos.1 |
示例和本地发布版本 |
执行 Gradle 脚本前先选择 JDK 21:
bash
export JAVA_HOME="/path/to/jdk-21"
export PATH="$JAVA_HOME/bin:$PATH"
java -version
即使 DevEco Studio 使用较新的 JBR,也要让 Gradle 使用 JDK 21。JBR Java 25
可能让 Kotlin 编译器在解析版本时报告 IllegalArgumentException: 25.0.2。
1.3 创建 OpenHarmony 示例目录
示例页面围绕五类结果、当前选中项、重试结果和共享检查组织:
text
标题区 EitherNet 工作台 / Kotlin Multiplatform + OpenHarmony
结果区 当前 API 结果、请求、摘要和 HTTP 状态码
目录区 Success、HttpFailure、ApiFailure、NetworkFailure、UnknownFailure
操作区 重新读取 / 运行重试
自检区 10/10 公共检查通过
目录点击只读取确定性的共享场景,不访问网络。重试按钮运行共享的挂起重试逻辑,
因此页面可以在没有后端服务的情况下验证 EitherNet 的结果分支和重试规则。
第 2 阶段:目标与依赖打通
2.1 加入 ohosArm64 目标
核心模块保留 JVM 和 Apple Native 目标,并增加 OpenHarmony ARM64 目标:
kotlin
kotlin {
jvm()
iosX64()
iosArm64()
iosSimulatorArm64()
ohosArm64()
sourceSets {
val nativeMain = create("nativeMain") { dependsOn(getByName("commonMain")) }
val nativeTest = create("nativeTest") { dependsOn(getByName("commonTest")) }
getByName("ohosArm64Main") { dependsOn(nativeMain) }
getByName("ohosArm64Test") { dependsOn(nativeTest) }
}
}
显式 nativeMain 层级让 OpenHarmony 复用与 Apple Native 相同的实现,同时避免
把只适用于 JVM 的 Retrofit 测试依赖带入 Native 编译。
2.2 Native focused build 的作用
example/nativeApp 构建一个受 linker map 约束的 shared library:
kotlin
kotlin {
ohosArm64 {
binaries.sharedLib {
baseName = "eithernet"
linkerOpts(
"--entry=0",
"--version-script=${project.file("src/ohosArm64Main/linker/shared-library.map")}",
)
linkerOpts("-lace_napi.z", "-luv", "-lhilog_ndk.z")
}
}
}
链接器只导出五个 C ABI 符号:
text
ApiCatalog
ApiGet
ApiRetry
ApiRunChecks
ApiFree
限制导出符号可以避免 Kotlin/Native 内部实现变成不受控 ABI,也让 C++ 桥接层的
职责和内存所有权更容易审查。
2.3 配置仓库和独立消费工程
根工程通过版本目录统一 Kotlin、coroutines 和 Okio 版本,example 通过项目
依赖消费共享模块:
kotlin
sourceSets {
commonMain.dependencies {
implementation(project(":shared"))
}
}
示例本身仍然是独立 Gradle 工程,并将版本写在 example/gradle.properties:
properties
eithernetVersion=2.1.0-ohos.1
coroutinesVersion=1.10.2-1.0.0
这样可以单独运行 example 测试,也可以把同一套共享代码作为独立消费者进行
验证。
2.4 通过构建产物消费共享库
prepareOhos 在 Native 链接成功后复制动态库和生成的 C 头文件:
kotlin
val prepareOhos by tasks.registering(Copy::class) {
dependsOn("linkDebugSharedOhosArm64")
from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) {
include("libeithernet.so")
into("libs/arm64-v8a")
}
from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) {
include("libeithernet_api.h")
into("src/main/cpp/include")
}
into(rootProject.layout.projectDirectory.dir("../example/ohosApp/entry"))
}
产物路径固定为:
text
example/ohosApp/entry/libs/arm64-v8a/libeithernet.so
example/ohosApp/entry/src/main/cpp/include/libeithernet_api.h
动态库和生成头文件属于本地构建产物,源码仓库通过 .gitignore 排除它们,
每台开发机都可以根据自己的 SDK 重新生成。
第 3 阶段:结果与序列化
3.1 为什么需要统一 JSON 契约
ArkTS、C++ 和 Kotlin/Native 不能直接共享 Kotlin 对象,因此边界使用 UTF-8 JSON:
text
EitherNetExamples
↓ ApiResult<String, String>
NativeBridge.kt
↓ NUL 结尾 UTF-8 JSON
N-API napi_create_string_utf8
↓ JavaScript string
EitherNetClient.ets
↓ JSON.parse
ArkUI 页面状态
页面只消费 JSON,不重新判断 ApiResult 的 Kotlin 类型。JVM 测试和设备页面因而
使用同一套结果场景、摘要和回退文本。
3.2 五类结果场景
共享示例在 EitherNetExamples 中定义五个确定性场景:
kotlin
private val scenarios = listOf(
Scenario("success", "GET /pandas", ApiResult.success("Po")),
Scenario("http-failure", "GET /pandas/503", ApiResult.httpFailure(503, "maintenance")),
Scenario("api-failure", "GET /pandas/quotas", ApiResult.apiFailure("quota exceeded")),
Scenario(
"network-failure",
"GET /pandas/offline",
ApiResult.networkFailure(IOException("connect timeout")),
),
Scenario(
"unknown-failure",
"GET /pandas/broken",
ApiResult.unknownFailure(IllegalStateException("unexpected json")),
),
)
每个场景被渲染为 ApiScenario,包含 id、请求路径、结果类型、HTTP 状态码、
摘要和 successOrElse 生成的回退文本。这样页面能展示完整结果,而不是只显示
一个成功或失败布尔值。
3.3 重试和 JSON 字段
重试示例使用 retryWithExponentialBackoff,前两次返回网络失败,第三次返回
Success("Po"):
kotlin
val result = retryWithExponentialBackoff<String, String>(
maxAttempts = 5,
initialDelay = 1.milliseconds,
delayFactor = 2.0,
maxDelay = 4.milliseconds,
jitterFactor = 0.0,
) {
transport.call()
}
返回给 ArkTS 的重试 JSON 形状为:
json
{
"attempts": 3,
"type": "Success",
"summary": "attempts=3, Success(value=Po)"
}
场景 JSON 形状为:
json
{
"id": "success",
"request": "GET /pandas",
"type": "Success",
"code": 0,
"summary": "Success(value=Po)",
"recovered": "Po"
}
Native 桥接中的字符串会进行 JSON 转义,避免错误信息中的引号、反斜杠或换行
破坏跨语言边界。
3.4 自检和错误边界
EitherNetExamples.runChecks() 覆盖十项检查:
- 五种确定性场景都存在;
Success可以通过fold得到值;- HTTP 失败暴露
503状态码; - API 失败保留解码后的错误体;
- 网络失败保留 Okio
IOException; - 未知失败保留原始
IllegalStateException; ApiException携带业务错误;- 重试在第三次尝试恢复;
successOrElse返回网络失败的回退值;- 非法场景下标会抛出
IllegalArgumentException。
Native 异常会转换为 {"error":"..."}。C++ 创建 ArkTS 字符串后立即调用
ApiFree,页面能够显示可读错误,同时不会泄漏 Kotlin/Native 堆内存。
第 4 阶段:原生桥接(技术难点)
4.1 Kotlin/Native 对象不能直接交给 ArkTS
Kotlin/Native 的 ApiResult、ApiScenario 和 RetryRun 都属于 Kotlin 运行时
对象。ArkTS 只能接收 JavaScript 值,C++ 也不能把 Kotlin 对象地址直接当成
JavaScript 对象。
最终采用三层桥接:
text
ArkTS
│ JSON string
▼
C++ N-API entry
│ const char*
▼
Kotlin/Native C ABI
│ EitherNetExamples
▼
UTF-8 JSON + explicit free
4.2 方案对比
| 方案 | 优点 | 缺点 | 选用 |
|---|---|---|---|
| 直接导出 Kotlin 对象 | 代码少 | ABI、生命周期和类型不可控 | ❌ |
| 只导出结果整数 | 实现简单 | 页面会重新实现结果映射 | ❌ |
| C ABI + JSON | 边界清晰、易扩展、易调试 | 有一次序列化开销 | ✅ |
| 在 ArkTS 重写完整模型 | 页面调用简单 | KMP 和 ArkTS 逻辑容易分叉 | ❌ |
4.3 Kotlin/Native 导出函数
NativeBridge.kt 只导出目录、读取、重试、检查和释放函数:
kotlin
@CName("ApiCatalog")
public fun catalogNative(): CPointer<ByteVar> = response {
EitherNetExamples.catalog().joinToString(prefix = "[", postfix = "]") { it.toJson() }
}
@CName("ApiGet")
public fun apiNative(index: Int): CPointer<ByteVar> =
response { EitherNetExamples.get(index).toJson() }
@CName("ApiRetry")
public fun retryNative(): CPointer<ByteVar> = response {
runBlocking { EitherNetExamples.retry() }.toJson()
}
@CName("ApiRunChecks")
public fun checksNative(): CPointer<ByteVar> = response {
val checks = runBlocking { EitherNetExamples.runChecks() }
"{\"passed\":true,\"checks\":[${checks.joinToString { quote(it) }}]}"
}
@CName("ApiFree")
public fun freeNative(pointer: CPointer<ByteVar>?) {
if (pointer != null) nativeHeap.free(pointer.rawValue)
}
返回值使用 Native heap 分配的、以 0 结尾的 C 字符串。每一块返回缓冲区都由
调用方在复制成 ArkTS 字符串后调用 ApiFree 释放。
4.4 C++ N-API 方法分发
C++ 注册四个 ArkTS 方法:
cpp
napi_property_descriptor methods[] = {
{"getCatalog", nullptr, Catalog, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"getApi", nullptr, GetApi, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"retry", nullptr, Retry, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"runChecks", nullptr, RunChecks, nullptr, nullptr, nullptr,
napi_default, nullptr},
};
getApi 会检查参数数量、有限数值、整数性和非负范围,再调用 ApiGet。所有
方法都遵循"调用 Native、创建 ArkTS 字符串、释放 Native 缓冲区"的顺序。
CMake 将 Kotlin/Native 动态库作为 imported library:
cmake
add_library(eithernet SHARED IMPORTED)
set_target_properties(eithernet PROPERTIES
IMPORTED_LOCATION
"${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/arm64-v8a/libeithernet.so")
add_library(entry SHARED napi_init.cpp)
target_include_directories(entry PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(entry PRIVATE eithernet libace_napi.z.so)
4.5 N-API 生命周期
text
ArkTS getApi(index)
│
▼
ReadIndex + argument check
│
▼
ApiGet(index)
│
▼
napi_create_string_utf8(...)
│
▼
ApiFree(nativeBuffer)
│
▼
return JS string
C++ 负责完成跨语言字符串转换和 Native 释放,ArkTS 页面不需要知道
Kotlin/Native 的堆实现。
第 5 阶段:ArkTS 宿主封装
5.1 ArkTS 调用 N-API
EitherNetClient.ets 从 libentry.so 导入 Native 模块,并在 JSON 边界完成
类型解析:
typescript
import eithernetNative from 'libentry.so';
export function catalog(): ApiScenario[] {
return JSON.parse(eithernetNative.getCatalog()) as ApiScenario[];
}
export function getApi(index: number): ApiScenario {
return JSON.parse(eithernetNative.getApi(index)) as ApiScenario;
}
export function retry(): RetryRun {
return JSON.parse(eithernetNative.retry()) as RetryRun;
}
ArkTS 接收到的是普通 JavaScript 对象,页面无需处理 C 指针或 Native 释放。
5.2 ArkUI 页面状态
Index.ets 保存当前场景、目录、重试结果、自检文本和错误文本:
typescript
@State private selected: ApiScenario = emptyScenario();
@State private scenarios: ApiScenario[] = [];
@State private status: string = '正在加载';
@State private errorText: string = '';
@State private retryText: string = '尚未执行重试';
@State private checkText: string = '尚未运行公共检查';
页面出现时调用 catalog() 和 runChecks(),目录行点击 getApi(index),重试
按钮调用 retry()。这些操作都在同一个 Native 结果链路中完成。
5.3 页面交互预设
页面提供三类操作:
- 重新读取:重新获取五类场景并选择第一项;
- 目录点击 :查看
Success、HttpFailure、ApiFailure、
NetworkFailure或UnknownFailure; - 运行重试:执行挂起的指数退避示例,预期第三次尝试成功。
这里没有额外的系统权限和传感器订阅,页面功能完全用于验证 EitherNet 的共享
结果模型、Native ABI 和 N-API 传输。
第 6 阶段:示例与验证
6.1 example 工程结构
text
example/
├── shared/
│ ├── src/commonMain/.../EitherNetExamples.kt
│ └── src/commonTest/.../EitherNetExamplesTest.kt
├── nativeApp/
│ ├── src/ohosArm64Main/.../NativeBridge.kt
│ └── src/ohosArm64Main/linker/shared-library.map
└── ohosApp/
├── AppScope/
├── entry/src/main/cpp/
│ ├── CMakeLists.txt
│ └── napi_init.cpp
└── entry/src/main/ets/
├── pages/Index.ets
└── eithernet/EitherNetClient.ets
shared 验证公共结果模型,nativeApp 产生 Native 动态库,ohosApp 负责
N-API 注册和 ArkUI 页面。三者的边界清晰,任何一层失败都能单独定位。
6.2 原生模块注册
napi_init.cpp 通过 napi_module_register 注册 entry 模块,TypeScript 类型
声明位于:
text
example/ohosApp/entry/src/main/cpp/types/libentry/index.d.ts
ArkTS 通过下面的模块名访问 C++ 导出的函数:
typescript
import eithernetNative from 'libentry.so';
6.3 Native 动态库准备
执行:
bash
export JAVA_HOME="/path/to/jdk-21"
./scripts/build-openharmony.sh
脚本依次执行根模块 JVM 检查、本地发布、示例共享测试、
linkDebugSharedOhosArm64 和 prepareOhos。成功后生成:
text
example/ohosApp/entry/libs/arm64-v8a/libeithernet.so
example/ohosApp/entry/src/main/cpp/include/libeithernet_api.h
还可以检查 ARM64 ELF 的强依赖:
bash
python3 scripts/check-native-deps.py \
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
example/ohosApp/entry/libs/arm64-v8a
6.4 构建、签名和安装
未配置签名时,可以在 DevEco Studio 构建未签名 HAP,或使用:
bash
DEVECO_HOME=/Applications/DevEco-Studio.app/Contents \
scripts/build-hap.sh /absolute/path/to/signing-project
需要签名时,先在源码目录之外准备工程:
bash
python3 scripts/prepare-signing-project.py /absolute/path/eithernet-signing
构建脚本会在签名边界提示确认配置。证书、profile、p12、别名和密码只保存在
外部目录,不提交到 AtomGit。
当前源码工程已验证未签名 HAP 产物:
text
example/ohosApp/entry/build/default/outputs/default/entry-default-unsigned.hap
设备安装需要针对设备配置签名。本文不把未签名构建写成已完成的签名真机验收。
四、完整代码对照
4.1 整体架构
text
EitherNet ApiResult
│ commonMain / nativeMain
▼
EitherNetExamples
│ Kotlin/Native
▼
ApiCatalog / ApiGet / ApiRetry / ApiRunChecks
│ C ABI + UTF-8 JSON
▼
libeithernet.so
│ C++ N-API
▼
libentry.so -> ArkTS -> ArkUI 页面
4.2 文件清单
| 文件 | 职责 |
|---|---|
eithernet/src/commonMain |
EitherNet 公共 API、结果类型和扩展函数。 |
eithernet/build.gradle.kts |
ohosArm64 目标和 Native source set。 |
example/shared/.../EitherNetExamples.kt |
五类结果、重试和十项公共检查。 |
example/shared/.../EitherNetExamplesTest.kt |
JVM 共享行为测试。 |
example/nativeApp/.../NativeBridge.kt |
C ABI、JSON 返回和内存释放。 |
example/nativeApp/.../shared-library.map |
限制 Native 导出符号。 |
example/ohosApp/.../napi_init.cpp |
N-API 方法注册、参数检查和释放。 |
example/ohosApp/.../EitherNetClient.ets |
JSON 解析和 ArkTS 类型门面。 |
example/ohosApp/.../Index.ets |
结果目录、重试和检查页面。 |
scripts/build-openharmony.sh |
测试、Native 链接和产物复制。 |
scripts/check-native-deps.py |
检查 ARM64 ELF 强依赖。 |
scripts/build-hap.sh |
外部签名工程的 HAP 构建。 |
docs/openharmony/VALIDATION.md |
自动检查和签名边界说明。 |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| Kotlin | ApiResult.success |
创建成功结果。 |
| Kotlin | ApiResult.httpFailure |
创建带状态码的 HTTP 失败。 |
| Kotlin | ApiResult.networkFailure |
创建网络失败。 |
| Kotlin | retryWithExponentialBackoff |
按指数退避执行重试。 |
| Native | ApiCatalog |
返回五类场景目录 JSON。 |
| Native | ApiGet |
返回指定场景 JSON。 |
| Native | ApiRetry |
返回重试摘要 JSON。 |
| Native | ApiRunChecks |
返回公共检查结果 JSON。 |
| Native | ApiFree |
释放 Native 返回缓冲区。 |
| N-API | getCatalog / getApi |
向 ArkTS 暴露目录和读取方法。 |
| ArkTS | retry / runChecks |
触发重试和共享检查。 |
4.4 ArkTS 与 Kotlin 的边界
ArkTS 只负责调用入口和界面状态:
typescript
this.selected = getApi(index);
const result: RetryRun = retry();
this.retryText = `${result.type}: ${result.summary}`;
Kotlin 负责结果含义和失败处理:
kotlin
private fun summary(result: ApiResult<String, String>): String =
result.fold(
onSuccess = { "Success(value=$it)" },
onNetworkFailure = { "NetworkFailure(${it.error.message})" },
onUnknownFailure = { "UnknownFailure(${it.error.message})" },
onHttpFailure = { "HttpFailure(code=${it.code}, error=${it.error})" },
onApiFailure = { "ApiFailure(${it.error})" },
)
两者之间只传输整数下标、UTF-8 JSON 和 JavaScript 基础值,不传输 Kotlin 对象、
ArkTS class 实例或未校验的动态结构。
五、关键决策说明
决策 1:把 ohosArm64 加入公共构建约定
只有真正链接 ohosArm64 动态库,才能证明 EitherNet 的共享 Kotlin 代码可以进入
OpenHarmony 运行时。页面单独运行无法替代这个验证。
决策 2:独立消费者必须通过构建产物消费
example 单独解析共享库,ohosApp 单独接收 .so 和头文件,避免根工程编译
通过却无法在 DevEco 中打包。
决策 3:JSON 作为跨语言数据契约
JSON 让 ArkTS、C++ 和 Kotlin/Native 的边界清楚可调试,并允许后续增加可信字段,
而不暴露 Kotlin 对象布局或 Native 内存细节。
决策 4:桥接层只开放五个 C ABI 入口
目录、读取、重试、检查和释放已经覆盖示例所需能力;减少 ABI 符号可以降低
Native 生命周期和兼容风险。
决策 5:结果场景使用确定性数据
示例不依赖外部网络服务,HTTP、业务、网络和未知失败都能稳定复现,方便在没有
后端环境或设备网络不稳定时完成 KMP 和 N-API 验收。
决策 6:把库验证和设备验证分开
JVM 测试验证结果规则,Native 链接验证 ABI,ELF 检查验证依赖,Hvigor 验证 HAP。
签名和设备安装属于外部环境步骤,不能用未签名构建替代。
六、测试与验证
6.1 测试环境
本次适配使用以下工具链约定:
- macOS;
- JDK 21;
- Kotlin Multiplatform
2.2.21-1.0.0; - OpenHarmony
ohosArm64Native 工具链; - DevEco Studio OpenHarmony SDK;
- ARM64
arm64-v8aHAP 配置。
签名证书、profile、p12 和真实设备序列号不属于源码仓库,也不写入文章。
6.2 静态检查与单元测试
bash
./gradlew :eithernet:jvmTest
./gradlew :eithernet:compileKotlinOhosArm64
./gradlew :eithernet:compileTestKotlinOhosArm64
(cd example && ./gradlew :shared:jvmTest)
测试覆盖五类结果、HTTP 状态码、错误体、Okio 异常、未知异常、ApiException、
第三次重试、回退值和非法下标。示例共享检查返回十项结果,页面显示 10/10。
6.3 原生桥接和 HAP 验证
bash
(cd example && ./gradlew :nativeApp:prepareOhos)
python3 scripts/check-native-deps.py \
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
example/ohosApp/entry/libs/arm64-v8a
已验证的关键结果是:
text
libeithernet.so: 0 unresolved strong imports
entry-default-unsigned.hap generated
DevEco 在没有 signingConfig 时会提示 No signingConfig found。这是当前源码
工程保持未签名的预期提示,不代表 Native 或 HAP 构建失败。
6.4 功能验证用例
用例 1:默认页面和公共检查
启动页面后加载五个 API 结果场景,并显示 10/10 公共检查通过。初始结果由
Native ApiCatalog() 和 ApiGet(0) 生成。
用例 2:五种结果目录
点击 Success、HttpFailure、ApiFailure、NetworkFailure 和 UnknownFailure,页面
分别显示请求路径、类型、摘要、HTTP 状态码和回退信息。
用例 3:运行重试
点击"运行重试",Native 层执行挂起的指数退避逻辑,预期显示
attempts=3, Success(value=Po)。
用例 4:非法下标
通过共享测试验证负数和超出范围的下标会抛出 IllegalArgumentException;C++ 层
同时拒绝非整数、负数和超出 int32 范围的输入。
用例 5:重复刷新和释放
多次点击"重新读取"和目录项,确认每次 Native 返回值都在 C++ 创建 ArkTS 字符串
后调用 ApiFree,页面不会保存裸指针。
6.5 验证结论
当前已完成 JVM 测试、ohosArm64 编译、Native 动态库准备、ARM64 依赖检查和
未签名 HAP 构建。效果图展示了五类结果目录、重试成功和 10/10 共享检查。
签名 HAP 的设备安装需要使用开发者自己的证书和 profile,属于外部签名流程;
源码工程没有把这一步伪装成已完成结果。
七、运行效果
7.1 效果图

效果图中可以看到:
- 页面标题为"EitherNet 工作台";
- 副标题说明 Kotlin Multiplatform 和 OpenHarmony;
- 当前结果为
Success,请求为GET /pandas; - 目录包含 Success、HttpFailure、ApiFailure、NetworkFailure 和 UnknownFailure;
- 页面显示"已加载 5 个 API 结果场景";
- 公共检查显示
10/10; - 重试摘要显示第三次尝试恢复成功;
- 页面底部说明数据由 Kotlin/Native 生成,经 N-API 提供给 ArkTS。
7.2 命令速查
bash
# 根工程测试、发布本地库和 OpenHarmony Native 准备
export JAVA_HOME="/path/to/jdk-21"
./scripts/build-openharmony.sh
# 单独运行共享测试
(cd example && ./gradlew :shared:jvmTest)
# 检查 ARM64 Native 依赖
python3 scripts/check-native-deps.py \
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
example/ohosApp/entry/libs/arm64-v8a
# 复制到外部签名工程
python3 scripts/prepare-signing-project.py /absolute/path/eithernet-signing
# 在已配置签名的工程中构建 HAP
DEVECO_HOME=/Applications/DevEco-Studio.app/Contents \
scripts/build-hap.sh /absolute/path/eithernet-signing
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配 :必须把共享代码真正编译成
ohosArm64动态库。 - N-API 不负责业务判断:C++ 只做类型检查、字符串转换和释放,结果语义放在 Kotlin。
- 挂起函数不能直接作为 C ABI :
ApiRetry在 Native 边界内使用runBlocking,再返回同步 JSON。 - JSON 字符串必须明确释放 :C++ 复制完成后立即调用
ApiFree,不能把 Native 指针交给 ArkTS。 - 签名配置和源码工程分离:没有 signingConfig 时构建未签名 HAP 是预期行为。
8.2 已知问题
- 当前示例只支持 OpenHarmony ARM64,未生成 x86、x86_64 或 32 位 ARM 产物;
./gradlew :eithernet:check在缺少本机 iOS Simulator SDK 时会失败,这不影响
已验证的 JVM、OpenHarmony Native 和 HAP 步骤;- 当前源码工程只保留未签名 HAP,设备安装需要外部签名工程;
ApiGet的示例数据是确定性场景,不代表真实后端接口或网络质量;js和wasmJs目标没有纳入本次 OpenHarmony 适配,相关消费者应使用上游实现。
8.3 未来优化方向
- 增加真实 HTTP 客户端的 OpenHarmony Native 集成示例;
- 将 JSON 手写编码替换为受控的跨平台序列化模块;
- 为 Compose Multiplatform 页面补充与 ArkTS 宿主相同的结果卡片示例;
- 增加 Native 错误码和更细粒度的异常分类;
- 在持续集成中加入
ohosArm64链接、ELF 依赖检查和未签名 HAP 构建任务; - 在具备外部签名和 ARM64 设备的环境中补充安装、启动和交互验收记录。
九、总结
9.1 核心难点回顾
本次适配真正需要处理的不是一个 ArkTS 页面,而是一条完整跨端链路:
text
EitherNet ApiResult
→ KMP common/native 共享代码
→ Kotlin/Native ARM64 动态库
→ C ABI JSON
→ C++ N-API
→ ArkTS JSON 门面
→ ArkUI 结果页面
9.2 封装层次
- KMP 层:定义
ApiResult、失败分类、fold、回退和重试; - Native 层:生成 ARM64 动态库,并通过 C ABI 输出有限入口;
- N-API 层:完成参数检查、字符串转换和内存释放;
- ArkTS 层:管理页面状态、目录点击、重试和错误展示;
- DevEco 层:完成 CMake、HAP、外部签名、安装和运行。
9.3 三条经验
- 先让共享结果模型在 JVM 和 Native 通过,再接入 ArkUI;
- 用 JSON 和少量 C ABI 代替跨语言对象传递;
- 把自动测试、Native 依赖检查、HAP 构建和签名设备验证分别记录,避免把"编译成功"
误认为"签名安装成功"。
9.4 适配成果
当前 EitherNet OpenHarmony 适配已完成:
ApiResult五类结果在共享代码中的确定性展示;fold、successOrElse和指数退避重试示例;- JVM 与 OpenHarmony ARM64 共用的十项检查;
- Kotlin/Native + C ABI + C++ N-API 桥接;
ApiCatalog、ApiGet、ApiRetry、ApiRunChecks、ApiFree五个导出入口;- DevEco Stage ArkTS 结果工作台和效果图;
- Native ELF 依赖检查和未签名 HAP 构建;
- 外部签名工程复制脚本和交互式签名边界;
- AtomGit 项目文档、中文 README 和 OpenHarmony 验收记录。
适配过程 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。