开源鸿蒙平台 KMP_CMP 三方库「TestMints」适配全流程
本文记录
TestMints接入 OpenHarmony 的完整过程,覆盖 KMP/CMP 工程盘点、ohosArm64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 交互示例、DevEco Studio 构建和验证。本次适配保留 TestMints 的核心
testTemplate语义:由setup准备场景,exercise执行行为,verifyAnd校验结果,teardown记录收尾,并把同一份 Kotlin 代码编译到 OpenHarmony ARM64。ArkUI 页面不是只显示一组静态校验按钮,而是让用户选择一个业务场景、输入执行次数、运行场景并查看结构化生命周期日志。
项目地址: AtomGit/oh-tpc/testmints
上游仓库: robertfmurdock/testmints
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做开源鸿蒙平台 KMP/CMP 适配
TestMints 是一个面向 Kotlin 测试代码可读性的轻量库。它把测试中的准备、执行、断言和收尾组织成可读的模板,既提供标准同步模板,也提供异步模块和日志模块。原项目面向多个 Kotlin 平台,OpenHarmony 适配的重点是让共享 Kotlin 逻辑可以在 ohosArm64 上编译,并由一个真实的 ArkUI 页面调用。
如果只把页面重新写成 ArkTS,页面虽然可以显示一个“通过”文本,却无法证明 TestMints 的 testTemplate、exercise、verifyAnd 和 teardown 已经在 OpenHarmony Native 侧执行。完整适配需要解决下面几个问题:
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 现有 KMP 工程默认覆盖 JVM、JS、Wasm、Apple、Linux 和 Windows 等目标,需要增加 ohosArm64()。 |
| 依赖兼容 | OpenHarmony 目标使用兼容构件,Coroutines、Serialization 和 Kotlin Logging 需要定向替换版本。 |
| 代码消费边界 | DevEco 工程不能直接把 Kotlin 源码当作 ArkTS 模块,需要先生成 libtestmints.so 和 C 头文件。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin data class,必须通过 C ABI、C++ N-API 和 JSON 传递结果。 |
| 交互验证不足 | 单纯调用 runChecks 只能验证固定断言,不能体现用户配置场景和完整生命周期。 |
| 构建链路较长 | Gradle、Kotlin/Native、CMake、Hvigor、HAP 和设备安装需要分别定位问题。 |
| 签名材料隔离 | 证书、Profile、p12 和密码属于本地交付配置,不应进入源码或文章。 |
因此,本项目把适配边界分成三层:KMP 共享层负责调用 TestMints API,Kotlin/Native 层负责输出稳定 JSON,C++ N-API 层负责给 ArkTS 注册方法,ArkUI 页面负责收集输入并呈现结果。
1.2 库提供的能力
TestMints 的标准模块提供 testTemplate 及其 DSL 操作符:
testTemplate(sharedSetup = ..., sharedTeardown = ...):创建带共享上下文的测试模板;exercise { ... }:执行被测行为并返回结果;verify { ... }:消费执行结果并完成断言;verifyAnd { ... }:在示例中串联结果校验;teardown { ... }:记录单场景收尾;sharedTeardown:模板级收尾,在场景结束后执行;async模块:为协程场景提供对应的异步模板;mint-logs模块:将测试事件输出到结构化日志。
本次 OpenHarmony 示例选择标准同步模板,因为它能在一个短流程中展示完整生命周期。示例页面使用以下场景模型:
| 字段 | 含义 |
|---|---|
title | 用户输入或预设的业务场景名称,空值会规范化为“未命名场景” |
quantity | 1 到 99 的演示输入,页面称为“执行次数” |
output | 示例行为计算出的结果,当前实现为 quantity * 2 |
events | setup、exercise、verify、teardown 和 template teardown 的有序日志 |
这里的 quantity 是演示场景输入,exercise 当前计算一次 quantity * 2,并没有在 Kotlin 侧循环执行 N 次真实业务操作。文章和页面都把它作为可重复的行为输入展示,避免误解为已经接入某个真实订单服务。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | TestMints 的模板执行、结果校验和 teardown 由 Kotlin 共享代码完成。 |
| 平台目标 | 为库模块和示例加入 ohosArm64,生成 ARM64 KLIB 和 libtestmints.so。 |
| 依赖兼容 | 仅对 ohosArm64 configuration 替换 OpenHarmony 兼容构件,不影响已有平台。 |
| 桥接稳定 | 通过少量 C ABI 入口返回 UTF-8 JSON,避免跨语言传递 Kotlin 对象。 |
| UI 完整 | 页面支持预设场景、文本输入、加减数量、执行按钮、结果卡和生命周期日志。 |
| 可测试 | JVM 测试、Native 链接、ELF 依赖检查和 DevEco 构建分别验收。 |
| 签名安全 | 文章只给出签名入口和占位路径,证书和密码由开发者本机配置。 |
| 仓库规范 | 项目、文章和示例链接统一使用 AtomGit。 |
example/ohosApp是一个真实的 OpenHarmony 宿主工程。页面点击执行后,ArkTS 调用 N-API,N-API 再调用 Kotlin/Native 中的 TestMints 代码。这样验证的是共享库功能在 OpenHarmony 上的运行链路,而不是在页面中复制一套相似的判断逻辑。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点 TestMints 模块、API 和示例边界
第 2 阶段:目标与依赖打通 ── 加入 ohosArm64、版本替换和独立 example 工程
第 3 阶段:共享场景模型 ── 用 testTemplate 封装场景结果和生命周期事件
第 4 阶段:原生桥接 ── Kotlin/Native C ABI、C++ N-API、JSON 和内存释放
第 5 阶段:ArkUI 交互页面 ── 预设、输入、数量步进、执行结果和错误状态
第 6 阶段:构建与验证 ── Native 动态库、依赖检查、DevEco 构建和效果图
每个阶段都使用真实产物作为下一阶段输入:先在 JVM 中验证 TestMintsExamples,再把共享代码链接为 ARM64 动态库,随后由 CMake 和 N-API 加载到 Stage 工程,最后在 ArkUI 页面中执行一个用户配置的行为场景。
三、逐步实现过程
第 1 阶段:项目初始化
1.1 盘点公共 API 和工程边界
仓库保留原有 libraries 组织,并新增 OpenHarmony 示例:
testmints/
├── libraries/
│ ├── standard/ 标准同步 testTemplate
│ ├── async/ 协程场景模板
│ ├── report/ 公共报告类型
│ └── mint-logs/ 结构化日志能力
├── convention-plugins/ KMP 构建约定和 OpenHarmony 依赖替换
├── example/
│ ├── shared/ 共享场景门面和 JVM 测试
│ ├── nativeApp/ ohosArm64 Kotlin/Native 动态库
│ └── ohosApp/ DevEco Stage 工程和 ArkUI 页面
├── scripts/ 构建、依赖检查和签名工程辅助脚本
└── docs/openharmony/ 验证记录和适配文章
example 是独立 Gradle 工程,example/ohosApp 是独立 DevEco 工程。两者通过 prepareOhos 任务交换动态库和头文件,不把 DevEco 工程强行纳入 Kotlin settings。
1.2 固定工具链和版本矩阵
| 项目 | 配置 | 用途 |
|---|---|---|
| Kotlin Multiplatform | 2.2.21-1.0.0 | 编译 JVM、Kotlin/Native 和 KLIB |
| JDK | 21 | Gradle、Kotlin 编译和 Native 任务 |
| OpenHarmony 目标 | ohosArm64 | ARM64 真机 Native 库 |
| KotlinPoet KSP | 2.2.0 | 项目现有代码生成工具链 |
| Coroutines OHOS | 1.10.2-1.0.0 | OpenHarmony 目标兼容构件 |
| Serialization OHOS | 1.9.1-1.0.0 | OpenHarmony 目标兼容构件 |
| Kotlin Logging OHOS | 8.0.4-1.0.0-02 | OpenHarmony 目标兼容构件 |
| HAP ABI | arm64-v8a | CMake 导入 Kotlin/Native 动态库 |
构建前选择 JDK 21:
export JAVA_HOME="/path/to/jdk-21"
java -version
OpenHarmony 兼容构件只在 ohosArm64 configuration 中替换,JVM、JS 和其他原有目标继续使用项目默认依赖版本。
1.3 创建 OpenHarmony 示例目录
页面围绕一个可操作的“行为场景工作台”组织,而不是只显示验证文本:
顶部 TestMints 三方库 / Kotlin Multiplatform / OpenHarmony
配置区 快速选择、场景名称、执行次数、执行按钮
结果区 PASSED、场景标题、INPUT、OUTPUT、BRIDGE
日志区 setup、exercise、verify、teardown、template teardown
状态区 已完成场景和错误信息
默认场景为“订单回访”,默认输入为 3,点击执行后输出 6。页面还提供“消息同步”和“资料校验”两个预设,方便在设备上切换场景并重复执行。
第 2 阶段:目标与依赖打通
2.1 加入 ohosArm64 目标
库的 OpenHarmony 目标由 convention-plugins/src/main/kotlin/com/zegreatrob/testmints/plugins/platforms.gradle.kts 统一配置。为了兼容旧版插件功能测试,约定插件通过反射检查 ohosArm64 方法是否存在;正式 KMP 模块则直接调用目标:
kotlin {
jvm()
ohosArm64()
sourceSets {
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
example/shared 同时保留 jvm() 和 ohosArm64()。它通过 kotlin.srcDirs 引入 libraries/report 与 libraries/standard 的 commonMain,从而让示例直接执行仓库当前的 TestMints 实现。
2.2 OpenHarmony 依赖定向替换
不同目标的构件发布节奏不完全一致,因此不能把 OHOS 版本写成全局强制版本。当前约定只处理名称以 ohosArm64 开头的 configuration:
configurations.configureEach {
if (name.startsWith("ohosArm64")) {
resolutionStrategy.dependencySubstitution {
substitute(module("org.jetbrains.kotlinx:kotlinx-coroutines-core"))
.using(module("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2-1.0.0"))
substitute(module("org.jetbrains.kotlinx:kotlinx-serialization-json"))
.using(module("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.1-1.0.0"))
substitute(module("io.github.oshai:kotlin-logging"))
.using(module("io.github.oshai:kotlin-logging:8.0.4-1.0.0-02"))
}
}
}
实际配置还覆盖这些库的目标后缀构件,例如 *-ohosarm64、Coroutines Test 和 Serialization Core。这样 OpenHarmony Native 编译能解析兼容构件,同时不改变原项目已有平台的依赖解析。
2.3 配置独立消费工程
example/settings.gradle.kts 使用 OpenHarmony 社区 Maven、Maven Central 和 Gradle Plugin Portal:
pluginManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
mavenLocal()
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
mavenCentral()
}
}
共享示例没有复制 TestMints 源码,而是通过 example/shared/build.gradle.kts 引入公共源码目录:
sourceSets {
commonMain {
kotlin.srcDirs(
"../../libraries/report/src/commonMain/kotlin",
"../../libraries/standard/src/commonMain/kotlin",
)
}
}
2.4 通过构建产物消费共享库
example/nativeApp 将共享示例链接成动态库,并由 prepareOhos 复制两个产物:
kotlin {
ohosArm64 {
binaries.sharedLib {
baseName = "testmints"
linkerOpts(
"--entry=0",
"--version-script=${project.file("src/ohosArm64Main/linker/shared-library.map")}",
)
}
}
}
val prepareOhos by tasks.registering(Copy::class) {
dependsOn("linkDebugSharedOhosArm64")
from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) {
include("libtestmints.so")
into("libs/arm64-v8a")
}
from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) {
include("libtestmints_api.h")
into("src/main/cpp/include")
}
into(rootProject.layout.projectDirectory.dir("../example/ohosApp/entry"))
}
动态库和生成头文件属于构建产物。DevEco 工程只消费本机根据 SDK 和源码重新生成的文件,不把二进制复制流程隐藏在页面代码中。
第 3 阶段:共享场景模型
3.1 为什么需要共享场景门面
TestMints 本身是测试 DSL,ArkUI 页面需要一个稳定的、适合跨语言传输的业务结果。因此在 example/shared/src/commonMain 增加 TestMintsExamples 门面:
ArkTS 输入 title + quantity
↓
TestMintsRunScenario
↓
TestMintsExamples.runScenario
↓
testTemplate(sharedSetup, sharedTeardown)
↓
exercise → verifyAnd → teardown
↓
ScenarioResult(title, quantity, output, events)
页面只知道场景结果字段,不需要了解 TestMints 内部模板对象的泛型和生命周期实现。
3.2 ScenarioResult 和输入规范化
public object TestMintsExamples {
public data class ScenarioResult(
val title: String,
val quantity: Int,
val output: Int,
val events: List<String>,
)
public fun normalizeScenarioInput(rawTitle: String, rawQuantity: Int): Pair<String, Int> {
val title = rawTitle.trim().ifEmpty { "未命名场景" }
val quantity = rawQuantity.coerceIn(1, 99)
return title to quantity
}
}
标题先 trim,空标题使用默认文案;数量限制在 1..99,防止页面输入异常值进入 Native 层。页面在调用前也做一次限制,但共享层仍然保留最终边界,确保其他调用方也得到相同结果。
3.3 用 TestMints 组织完整生命周期
核心场景实现如下:
public fun runScenario(rawTitle: String, rawQuantity: Int): ScenarioResult {
val title = rawTitle.trim().ifEmpty { "未命名场景" }
val quantity = rawQuantity.coerceIn(1, 99)
val events = mutableListOf<String>()
val template = testTemplate(
sharedSetup = {
events += "setup · 准备「$title」"
title
},
sharedTeardown = {
events += "template teardown · 场景已收尾"
},
)
var output = 0
template() exercise {
events += "exercise · 执行 $quantity 次"
output = quantity * 2
output
} verifyAnd { result ->
events += "verify · 检查结果 $result"
check(result == quantity * 2) { "场景结果不符合预期" }
} teardown {
events += "teardown · 记录执行摘要"
}
return ScenarioResult(title, quantity, output, events)
}
默认输入 订单回访 和 3 的结果是:
title = 订单回访
quantity = 3
output = 6
events = 5 条有序生命周期日志
需要特别说明的是,日志文本中的“执行 3 次”是场景输入的业务表达;当前 exercise 代码只执行一次模板动作并计算 3 * 2。如接入真实业务,后续可以把这里替换为循环调用、批处理请求或仓储操作。
3.4 固定检查与错误边界
runChecks() 使用同一个 testTemplate 验证共享库可以在 OpenHarmony Native 侧执行:
public fun runChecks(): List<String> {
var observed = false
val template = testTemplate(sharedSetup = { Unit })
template() exercise { 42 } verify { result ->
check(result == 42) { "testmints exercise returned $result" }
observed = true
}
check(observed) { "testmints verify callback was not invoked" }
return listOf(
"standard test template executes on OpenHarmony",
"verify callback observes the exercise result",
)
}
Native 包装层捕获异常并返回 {"error":"..."}。这样 ArkTS 可以把错误显示在页面底部,而不是让一个 Kotlin/Native 异常直接穿过 C ABI。
第 4 阶段:原生桥接(技术难点)
4.1 Kotlin/Native 对象不能直接交给 ArkTS
ScenarioResult 是 Kotlin data class,events 还是 Kotlin 集合。ArkTS 只能安全接收 JavaScript 值或字符串,C++ 也不能把 Kotlin 对象地址直接包装成 JS 对象。因此本项目选择 UTF-8 JSON 作为唯一跨语言结果格式:
ArkTS runScenario(title, quantity)
│
▼
C++ N-API 参数检查
│
▼
Kotlin/Native TestMintsRunScenario
│
▼
Native heap UTF-8 JSON
│
▼
napi_create_string_utf8 + TestMintsFree
│
▼
ArkTS JSON.parse
4.2 方案对比
| 方案 | 优点 | 缺点 | 选用 |
|---|---|---|---|
| 直接导出 Kotlin 对象 | 代码少 | ABI、生命周期和类型不可控 | ❌ |
| 只导出整数结果 | 实现简单 | 页面无法展示场景标题和有序日志 | ❌ |
| C ABI + JSON | 边界清晰、易调试、可扩展 | 有一次序列化开销 | ✅ |
| 在 ArkTS 重写 TestMints 生命周期 | 页面调用简单 | 共享库与页面逻辑容易分叉 | ❌ |
4.3 Kotlin/Native C ABI
NativeBridge.kt 只开放三个符号:
@CName("TestMintsRunChecks")
public fun runChecksNative(): CPointer<ByteVar> = response {
val checkJson = TestMintsExamples.runChecks()
.joinToString(separator = ",") { quote(it) }
"{\"passed\":true,\"checks\":[$checkJson]}"
}
@CName("TestMintsRunScenario")
public fun runScenarioNative(
title: CPointer<ByteVar>?,
quantity: Int,
): CPointer<ByteVar> = response {
val result = TestMintsExamples.runScenario(title?.toKString().orEmpty(), quantity)
val eventJson = result.events.joinToString(separator = ",") { quote(it) }
"{\"passed\":true,\"title\":${quote(result.title)}," +
"\"quantity\":${result.quantity},\"output\":${result.output}," +
"\"events\":[$eventJson]}"
}
@CName("TestMintsFree")
public fun freeNative(pointer: CPointer<ByteVar>?) {
if (pointer != null) nativeHeap.free(pointer.rawValue)
}
返回字符串由 Native heap 分配,并以 0 结尾。每次 N-API 创建 JavaScript 字符串后,必须调用 TestMintsFree 释放对应缓冲区。
4.4 linker map 限制导出符号
example/nativeApp/src/ohosArm64Main/linker/shared-library.map 显式导出上述三个符号。这样动态库的公共边界不会随着 Kotlin/Native 内部实现变化而扩大:
{
global:
TestMintsRunChecks;
TestMintsRunScenario;
TestMintsFree;
local: *;
};
这是一次容易遗漏的配置。没有 linker map 导出时,Native 库可能能够生成,但 CMake 或 N-API 在加载时找不到函数;修改导出后需要重新执行 linkDebugSharedOhosArm64。
4.5 C++ N-API 方法分发
napi_init.cpp 注册 runChecks 和 runScenario 两个 ArkTS 方法:
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor methods[] = {
{"runChecks", nullptr, RunChecks, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"runScenario", nullptr, RunScenario, nullptr, nullptr, nullptr,
napi_default, nullptr},
};
if (napi_define_properties(env, exports, 2, methods) != napi_ok) {
napi_throw_error(env, nullptr, "Unable to register TestMints exports");
return nullptr;
}
return exports;
}
RunScenario 检查参数数量,读取不超过 255 字节的标题和一个 int32 数量,再调用 TestMintsRunScenario。成功创建 JS 字符串后立即释放 Native 缓冲区;创建失败时也先释放再抛错。
4.6 CMake 导入动态库
DevEco 工程把 Kotlin/Native 产物作为 imported library,并与 N-API 系统库链接:
add_library(testmints SHARED IMPORTED)
set_target_properties(testmints PROPERTIES
IMPORTED_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/arm64-v8a/libtestmints.so")
add_library(entry SHARED napi_init.cpp)
target_include_directories(entry PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(entry PRIVATE testmints libace_napi.z.so)
页面侧类型声明位于 entry/src/main/cpp/types/libentry/index.d.ts,调用方式为:
import { runScenario } from '../testmints/TestMintsClient';
const json = runScenario('订单回访', 3);
const result = JSON.parse(json);
第 5 阶段:ArkUI 交互页面
5.1 页面状态和调用封装
TestMintsClient.ets 将 Native 字符串解析成页面使用的结果类型:
import { runScenario as nativeRunScenario } from 'libentry.so';
export interface ScenarioResult {
passed: boolean;
title: string;
quantity: number;
output: number;
events: string[];
}
export function runScenario(title: string, quantity: number): ScenarioResult {
return JSON.parse(nativeRunScenario(title, quantity)) as ScenarioResult;
}
Index.ets 保存场景名称、数量文本、执行状态、错误文本和最后一次结果。页面输入先在 ArkTS 中规范化,Native 共享层再次规范化,保证入口和核心逻辑都具备边界保护。
5.2 页面交互预设
页面提供三个预设:
- 订单回访:适合展示默认效果,输入
3时输出6; - 消息同步:用于模拟同步类场景;
- 资料校验:用于模拟校验类场景。
点击预设只改变场景名称和状态文案;点击“执行 TestMints 场景”才会真正经过 N-API 调用。数量可以通过输入框或 -、+ 按钮调整,并始终限制在 1..99。
5.3 结果卡和生命周期日志
结果卡显示四类信息:
PASSED TestMints 场景执行成功
INPUT 3 次
OUTPUT 6
BRIDGE N-API
日志区域按共享 Kotlin 代码写入的顺序展示:
setup · 准备「订单回访」
exercise · 执行 3 次
verify · 检查结果 6
teardown · 记录执行摘要
template teardown · 场景已收尾
其中 setup 和 template teardown 来自 testTemplate 的共享生命周期,exercise、verify 和 teardown 来自单个场景。日志展示让用户可以观察 TestMints 的执行顺序,而不是只看到一个布尔值。
5.4 错误状态
Native 返回错误 JSON 或 N-API 抛出异常时,页面把状态改为“场景执行失败”,并显示错误文本;旧结果会被清空。空标题、非数字输入和越界数量会在执行前被规范化,不会直接导致页面崩溃。
第 6 阶段:示例与验证
6.1 example 工程结构
example/
├── shared/
│ ├── src/commonMain/kotlin/com/zegreatrob/testmints/example/
│ │ └── TestMintsExamples.kt
│ └── src/commonTest/kotlin/com/zegreatrob/testmints/example/
│ └── TestMintsExamplesTest.kt
├── nativeApp/
│ ├── src/ohosArm64Main/kotlin/.../NativeBridge.kt
│ ├── src/ohosArm64Main/linker/shared-library.map
│ └── build.gradle.kts
└── ohosApp/
└── entry/
├── src/main/cpp/CMakeLists.txt
├── src/main/cpp/napi_init.cpp
└── src/main/ets/
├── pages/Index.ets
└── testmints/TestMintsClient.ets
shared 验证 TestMints 语义,nativeApp 生成 OpenHarmony ARM64 库,ohosApp 负责 ArkUI 页面和动态库加载。每一层都可以单独定位故障。
6.2 原生模块注册
napi_init.cpp 通过 napi_module_register 注册 entry 模块。此前最容易出现的两个问题是:
napi_define_properties仍然传1,导致第二个runScenario方法没有注册;- Kotlin linker map 没有导出
TestMintsRunScenario或TestMintsFree,导致链接或加载失败。
当前实现传入方法数量 2,并显式导出三个 C ABI 符号。ArkTS 通过 libentry.so 的类型声明调用它们。
6.3 Native 动态库准备
执行根工程脚本:
export JAVA_HOME="/path/to/jdk-21"
./scripts/build-openharmony.sh
脚本依次运行 libraries 的 standard:jvmTest、async:jvmTest,再运行 example 的 shared:jvmTest 和 nativeApp:prepareOhos。成功后应生成:
example/ohosApp/entry/libs/arm64-v8a/libtestmints.so
example/ohosApp/entry/src/main/cpp/include/libtestmints_api.h
6.4 ARM64 依赖检查
使用 OpenHarmony Native SDK 的 LLVM 工具检查动态库:
python3 scripts/check-native-deps.py \
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
example/ohosApp/entry/libs/arm64-v8a
期望看到:
libtestmints.so: 0 unresolved strong imports
这个检查只验证 ELF 架构、系统导出和强未解析符号,不等同于签名或设备功能验收。
6.5 HAP 构建与签名入口
打开 example/ohosApp 后可以在 DevEco Studio 中同步并构建未签名 HAP。若要生成签名工程,使用仓库脚本创建隔离副本:
python3 scripts/prepare-signing-project.py /absolute/path/testmints-signing
./scripts/build-hap.sh /absolute/path/testmints-signing
签名证书、Profile、p12 和密码应由开发者在本机 DevEco 工程配置。本文不包含任何真实签名材料,也不把本地 build-profile.json5 当作可复制配置。没有配置证书时,只能确认源码、Native 库和 DevEco 工程可以构建,不能据此声称完成签名安装。
四、完整代码对照
4.1 整体架构
用户输入场景名称和数量
│
▼
ArkUI Index.ets
│ runScenario(title, quantity)
▼
TestMintsClient.ets / libentry.so
│ C++ N-API
▼
NativeBridge.kt
│ TestMintsExamples.runScenario
▼
libraries/standard testTemplate
│ ScenarioResult → JSON
▼
ArkTS JSON.parse → 结果卡和生命周期日志
4.2 文件清单
| 文件 | 作用 |
|---|---|
convention-plugins/src/main/kotlin/.../platforms.gradle.kts | 声明 ohosArm64 和定向依赖替换 |
example/shared/build.gradle.kts | 共享示例的 JVM/Native 目标和 TestMints 源码目录 |
example/shared/src/commonMain/.../TestMintsExamples.kt | 场景模型和 testTemplate 生命周期 |
example/nativeApp/.../NativeBridge.kt | C ABI JSON 输出和 Native 内存释放 |
example/nativeApp/.../shared-library.map | 动态库导出边界 |
example/nativeApp/build.gradle.kts | ARM64 动态库和 prepareOhos 任务 |
example/ohosApp/entry/src/main/cpp/napi_init.cpp | N-API 方法注册和参数检查 |
example/ohosApp/entry/src/main/cpp/CMakeLists.txt | 导入 libtestmints.so 并链接 N-API |
example/ohosApp/entry/src/main/ets/pages/Index.ets | 交互页面、结果卡和日志列表 |
example/ohosApp/entry/src/main/ets/testmints/TestMintsClient.ets | ArkTS 结果类型和 JSON 解析 |
scripts/build-openharmony.sh | JVM 测试和 Native 产物准备 |
scripts/check-native-deps.py | ARM64 ELF 依赖检查 |
4.3 关键 API 对照
| Kotlin/Native 符号 | ArkTS 方法 | 作用 |
|---|---|---|
TestMintsRunChecks | runChecks | 执行固定的共享模板自检 |
TestMintsRunScenario | runScenario | 执行用户配置的场景并返回 JSON |
TestMintsFree | 内部调用 | 释放 Native heap 字符串 |
示例结果 JSON:
{
"passed": true,
"title": "订单回访",
"quantity": 3,
"output": 6,
"events": [
"setup · 准备「订单回访」",
"exercise · 执行 3 次",
"verify · 检查结果 6",
"teardown · 记录执行摘要",
"template teardown · 场景已收尾"
]
}
4.4 ArkTS 与 Kotlin 的边界
ArkTS 负责输入收集、按钮状态、结果展示和错误文案;Kotlin 负责 TestMints 模板和结果语义;C++ 只负责类型检查、函数分发、字符串转换和内存释放。这个边界避免在 ArkTS 中重复实现 verify 规则,也让 JVM 测试与设备页面使用同一份场景代码。
五、关键决策说明
决策 1:把 ohosArm64 加入公共构建约定
OpenHarmony 适配必须让共享 Kotlin 代码真正生成 Native 目标。只增加一个 DevEco 页面不能证明三方库可用,因此目标配置放在构建约定中,并让库模块和示例共同复用。
决策 2:只对 OHOS configuration 替换依赖
OpenHarmony 兼容构件与 JVM/JS 构件的发布坐标不同。按 configuration 替换可以缩小影响范围,避免为了一个新目标改变其他平台的解析结果。
决策 3:用场景门面承接 TestMints DSL
测试 DSL 的泛型模板对象不适合直接跨语言。TestMintsExamples 把它收敛成 ScenarioResult,同时保留 setup、exercise、verify 和 teardown 的执行证据。
决策 4:JSON 作为跨语言数据契约
场景结果包含字符串、整数和数组。JSON 可读、易调试,ArkTS 解析简单;C ABI 只需要管理一块明确释放的 UTF-8 缓冲区。
决策 5:交互页面采用真实业务语境
“订单回访”“消息同步”“资料校验”比单纯的“运行测试”更能体现库的使用方式。用户可以修改场景名称和数量,观察同一套模板如何组织业务行为与结果。
决策 6:把库验证、Native 验证和签名验证分开
JVM 通过说明共享逻辑正确,ELF 检查说明 ARM64 动态库依赖完整,DevEco 构建说明工程可打包。签名和真机安装还需要本地证书、Profile、设备和授权条件,不能用前面的结果替代。
六、测试与验证
6.1 测试环境
建议使用:
| 环境 | 要求 |
|---|---|
| macOS | 能运行 Gradle、DevEco Studio 和 OpenHarmony Native SDK |
| JDK | 21 |
| DevEco Studio | 与本机 OpenHarmony SDK 匹配的版本 |
| Native SDK | 包含 llvm-nm、llvm-readelf 和 AArch64 sysroot |
| 设备 | ARM64 OpenHarmony 设备;是否支持完整功能由设备能力决定 |
6.2 静态检查与单元测试
export JAVA_HOME="/path/to/jdk-21"
./scripts/build-openharmony.sh
该命令覆盖:
libraries:standard:jvmTest;libraries:async:jvmTest;example:shared:jvmTest;example:nativeApp:prepareOhos。
TestMintsExamplesTest 至少验证两件事:固定模板自检返回两条检查,以及“消息同步”输入 4 时输出 8、结果通过且包含生命周期事件。
6.3 原生桥接和 HAP 验证
在 Native 产物生成后运行 check-native-deps.py,确认 libtestmints.so 是 AArch64 ELF 且无未解析强符号。随后在 DevEco Studio 打开 example/ohosApp,同步 CMake 和 ArkTS,构建 HAP。
N-API 侧重点包括:
runChecks和runScenario都已注册;napi_define_properties的数量为2;TestMintsRunScenario、TestMintsRunChecks、TestMintsFree都在 linker map 中;- JS 字符串创建成功和失败路径都会释放 Native 缓冲区;
- 标题和数量参数类型不正确时返回可读异常。
6.4 功能验证用例
用例 1:默认场景执行
进入页面,默认显示“订单回访”和 3。点击“执行 TestMints 场景”,确认状态变为已完成,结果卡显示 PASSED、INPUT 3 次、OUTPUT 6,并出现五条生命周期日志。
用例 2:切换预设
依次点击“消息同步”和“资料校验”,确认场景标题更新,执行后日志中的 setup 文案使用新的标题。
用例 3:调整数量
使用 -、+ 或文本输入把数量改为 4,执行后确认输出为 8。输入空值、负数或超过 99 时,页面将数量规范化到有效范围。
用例 4:空标题
清空场景名称并执行,确认结果标题为“未命名场景”,而不是出现空卡片或 Native 异常。
用例 5:重复执行
连续点击执行按钮,确认每次返回新的结果和完整日志,不会把上一次的事件继续追加到下一次结果中。
用例 6:Native 错误显示
当 Native 返回错误 JSON 或 N-API 抛出异常时,确认页面显示“场景执行失败”和错误文本,并清空旧结果。
6.5 验证结论
根据项目中的构建脚本和验证记录,JVM 测试、Native 动态库准备及 ELF 依赖检查具备明确命令入口,示例页面具备场景执行和结构化结果展示能力。本文附带的效果图来自用户提供的运行截图,展示了 订单回访、输入 3、输出 6、PASSED 和生命周期日志。
本文不把未在当前工作流中执行的签名 HAP、设备安装或现场操作结果写成已完成事实。需要真机交付时,应在配置好本地签名材料后按第 6 阶段命令继续验证。
七、运行效果
7.1 效果图

图中页面展示了一个完整的人机交互场景:
- 顶部显示
TestMints 三方库和 Kotlin Multiplatform / OpenHarmony 说明; - 预设场景选择为“订单回访”;
- 用户输入执行次数
3并点击执行; - 结果卡显示标题、
INPUT 3 次、OUTPUT 6、BRIDGE N-API; - 状态标签显示
PASSED; - 日志区域按顺序显示 setup、exercise、verify、teardown 和 template teardown。
这里的 6 来自示例 exercise 的 quantity * 2 计算,重点是展示 TestMints 生命周期从 Kotlin/Native 传到 ArkUI 的完整路径。
7.2 命令速查
# 根工程测试和 OpenHarmony Native 准备
export JAVA_HOME="/path/to/jdk-21"
./scripts/build-openharmony.sh
# 单独运行共享示例测试
(cd example && ./gradlew :shared:jvmTest)
# 检查 ARM64 动态库依赖
python3 scripts/check-native-deps.py \
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
example/ohosApp/entry/libs/arm64-v8a
# 生成独立签名工程,按需配置证书后构建 HAP
python3 scripts/prepare-signing-project.py /absolute/path/testmints-signing
./scripts/build-hap.sh /absolute/path/testmints-signing
# 配置设备和签名后安装,设备序列号与 HAP 路径按本机实际情况填写
hdc -t <设备序列号> install -r <signed-hap-path>
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配:共享 TestMints 代码必须实际编译成
ohosArm64动态库。 - OpenHarmony 依赖不能全局替换:只对
ohosArm64configuration 做替换,才能保持原平台构建稳定。 - N-API 注册数量必须准确:同时增加
runChecks和runScenario时,napi_define_properties必须传2。 - 导出符号必须和头文件一致:linker map 缺少任一 C ABI 符号时,CMake 链接或运行时加载会失败。
- Native 字符串必须成对释放:创建 JS 字符串后立即调用
TestMintsFree,错误路径也不能遗漏。 - 输入语义要和实现一致:示例中的“执行次数”目前参与
quantity * 2演示计算,不代表真实循环执行业务 N 次。 - 签名和构建是两个阶段:没有证书、Profile 和密码时,不能把 unsigned HAP 当作可安装交付物。
8.2 已知问题
- 当前示例使用同步
testTemplate,尚未在 ArkUI 页面中展示async模块的协程交互; runScenario的结果序列化使用轻量手写 JSON 转义,字段增加时需要同步维护转义逻辑;- Native 动态库和生成头文件需要每台开发机重新准备,不能直接依赖其他机器的构建产物;
- 签名材料和设备安装依赖本地 DevEco 配置,仓库不提供通用证书;
- 示例页面是单页面工作台,多页面产品需要在业务层集中管理 Native 调用和状态缓存。
8.3 未来优化方向
- 增加
async场景预设,展示协程 TestMints 的执行与错误处理; - 将
ScenarioResult改为正式的跨平台序列化模型,减少手写 JSON 维护成本; - 为页面增加可折叠日志、耗时统计和失败步骤定位;
- 增加真实业务适配示例,让
exercise调用仓储、网络或本地数据操作; - 在持续集成中加入
ohosArm64编译、Native 依赖检查和未签名 HAP 构建; - 在配置好授权的设备流水线中补充签名、安装和启动验收。
九、总结
9.1 核心难点回顾
TestMints 的 OpenHarmony 适配不是把一个测试按钮搬到 ArkUI,而是一条完整跨端链路:
ArkUI 场景输入
→ C++ N-API 参数检查
→ Kotlin/Native C ABI
→ TestMints testTemplate
→ ScenarioResult 和 JSON
→ ArkTS 结果卡与生命周期日志
9.2 封装层次
- KMP 层:复用 TestMints 标准模块的
testTemplate、exercise、verifyAnd和teardown; - 示例共享层:把模板执行收敛为
ScenarioResult,提供输入规范化和事件日志; - Native 层:生成 ARM64 动态库,通过三个 C ABI 函数输出 JSON;
- N-API 层:完成参数读取、字符串转换、方法注册和内存释放;
- ArkTS 层:管理用户输入、按钮交互、JSON 解析、结果展示和错误状态;
- DevEco 层:完成 CMake、Hvigor、HAP 和后续签名安装。
9.3 三条经验
- 先在 JVM 验证共享 TestMints 语义,再接入 Kotlin/Native 和 ArkUI;
- 用稳定 JSON 和少量 C ABI 代替跨语言对象传递;
- 把共享逻辑、Native 依赖、HAP 构建、签名和设备安装分别记录,避免把“编译通过”误认为“完成真机交付”。
9.4 适配成果
当前 TestMints OpenHarmony 示例已经具备:
ohosArm64KMP 目标和 OHOS 兼容依赖替换;TestMintsExamples共享场景门面;setup、exercise、verifyAnd、teardown和模板级 teardown 的可视化日志;- Kotlin/Native + C ABI + C++ N-API 桥接;
runChecks和runScenario双方法注册;- ARM64 动态库、CMake 导入和 Native 依赖检查;
- 包含预设、输入、数量步进、执行、结果和日志的人机交互页面;
- 本地效果图和 AtomGit 项目链接;
- 可由开发者继续配置签名并在真机上完成交付验收的工程入口。
适配过程 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)