开源鸿蒙平台 KMP/CMP 三方库「kotlinx-datetime」适配全流程
本文记录
kotlinx-datetime接入 OpenHarmony 的完整过程,覆盖 KMP/CMP 工程盘点、ohosArm64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、系统时区、签名 HAP 和验收。本次适配保留
kotlinx-datetime的公共日期时间 API,将同一份 Kotlin 代码编译到 OpenHarmony ARM64,再通过 C ABI、C++ N-API 和 ArkTS 页面展示当前时间、epoch 毫秒、时区与共享自检结果。页面只负责调用宿主能力和呈现状态,日期时间解析、格式化、时区查找与检查逻辑仍由 KMP 代码负责。
项目地址: AtomGit/oh-tpc/kotlinx-datetime
开发工具: 华为云码道
一、背景
1.1 为什么做开源鸿蒙平台 KMP/CMP 适配
kotlinx-datetime 是 Kotlin Multiplatform 日期时间库。它提供 Instant、Clock、LocalDate、LocalDateTime、TimeZone、UtcOffset 以及格式化和序列化能力。原项目已经覆盖 JVM、JS、Wasm、Apple、Linux、Windows 等目标,但 OpenHarmony 不能直接复用其他平台的 Kotlin/Native 构建产物。
如果只在 ArkTS 页面里重新写一个“当前时间”展示,页面可以显示文本,却不能证明 KMP 公共 API、Native 动态库、时区数据库和 DevEco 工程已经连通。一次完整适配需要同时解决这些问题:
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | KMP 模块默认没有 OpenHarmony 目标,必须增加 ohosArm64() 才能生成 KLIB 和 .so。 |
| 工具链不一致 | Kotlin/Native、OpenHarmony LLVM、Native SDK、Gradle 和 Hvigor 版本需要能够共同工作。 |
| 时区来源不同 | OpenHarmony 系统镜像的 IANA tzdb 目录和 Linux、Darwin 目录不完全相同。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin data class,必须经过 C ABI、C++ N-API 和 JSON。 |
| 内存生命周期 | Kotlin/Native 返回的字符串必须由 C++ 复制到 ArkTS 后及时释放。 |
| 交付链路复杂 | Native 动态库、CMake、HAP、签名、ARM64 依赖和设备安装要分别验证。 |
因此,本项目把适配边界放在三个地方:Kotlin/Native 平台实现、C ABI/N-API 桥接层和 ArkUI 示例层。日期时间模型、解析和自检逻辑保留在 KMP 公共代码中。
1.2 库提供的能力
kotlinx-datetime 公共 API 提供以下能力:
Instant:表示时间线上的一个瞬间,支持 ISO-8601 文本和 epoch 转换;Clock.System.now():读取系统当前时间;LocalDate、LocalTime和LocalDateTime:处理没有时区或已经转换到本地的日期时间;TimeZone:解析命名时区、读取已知时区并获取系统默认时区;UtcOffset:表示 UTC 偏移量;DateTimePeriod、DatePeriod和日期时间单位运算;DateTimeFormat与序列化支持;- Kotlin Multiplatform 公共源集可复用的解析、格式化和边界检查逻辑。
示例门面 DateTimeExamples 为所有宿主提供同一组可验证数据:
| 示例 | 输入 | 目的 |
|---|---|---|
template-0 | 1970-01-01T00:00:00Z | Unix epoch 边界 |
template-1 | 2020-02-29T12:34:56.789Z | 闰日与毫秒精度 |
template-2 | 2038-01-19T03:14:07Z | 2038 时间边界 |
now | Clock.System.now() | 设备当前时间 |
每条记录包含 id、ISO 文本、epochMilliseconds 和 timeZone。未知或不可读取的系统时区不会破坏示例,示例门面会在显示层安全回退到 UTC;业务主动请求命名时区时仍然使用库原有的错误语义。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | Instant、Clock、时区解析、JSON 字段和八项自检由 Kotlin 共享。 |
| 平台目标 | 为核心模块和示例加入 ohosArm64,生成 libkotlinx_datetime.so。 |
| 桥接稳定 | 只导出少量 C ABI 函数,通过 N-API 返回不可变 JSON 字符串。 |
| UI 完整 | 页面展示当前时间、epoch 毫秒、时区、重新读取按钮和检查状态。 |
| 可测试 | JVM 测试、Native 编译、ELF 依赖、HAP 构建和真机页面分别验收。 |
| 签名安全 | 源码只保留签名工程入口,证书、profile 和密码由开发者本机配置。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面是独立的真机验收宿主。其他 KMP/CMP 应用可以直接复用
kotlinx.datetime公共 API,再自行决定页面布局、数据缓存和业务错误展示方式。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点 KMP API、时区实现和 OpenHarmony 示例边界
第 2 阶段:目标与依赖打通 ── 加入 ohosArm64、独立 example 工程和 Native 构建任务
第 3 阶段:时间与序列化 ── 建立 DateTimeExample、时区回退、JSON 和公共自检
第 4 阶段:原生桥接 ── Kotlin/Native C ABI、C++ N-API、参数检查和内存释放
第 5 阶段:系统能力封装 ── Clock、时区目录、ArkUI 页面状态和生命周期
第 6 阶段:示例与验证 ── HAP 构建、手动签名、设备安装和效果图
每个阶段都使用真实产物作为下一阶段输入:先用 JVM 验证共享门面,再把同一份代码链接为 ARM64 动态库,随后由 CMake 和 N-API 加载到 Stage 工程,最后安装签名 HAP 观察页面上的 Native 数据。
三、逐步实现过程
第 1 阶段:项目初始化
1.1 盘点公共 API 和工程边界
项目采用与参考 OpenHarmony KMP/CMP 工程一致的分层:
kotlinx-datetime/ KMP 日期时间核心库
core/ohosArm64/ OpenHarmony 时区平台实现
timezones/full/ 完整时区数据库变体
example/shared/ 公共示例门面和 JVM 验收测试
example/nativeApp/ ohosArm64 Kotlin/Native 动态库
example/ohosApp/ DevEco Stage 工程和 ArkUI 页面
scripts/ Native、HAP 和签名工程辅助脚本
docs/openharmony/ 架构和真机验证说明
example 是独立 Gradle 工程,不把 DevEco 工程当作 Kotlin 子模块。它通过 example/settings.gradle.kts 的 includeBuild("..") 使用当前仓库源码,Gradle 和 Hvigor 可以分别执行;复制 example/ohosApp 到仓库外后,也可以在 DevEco Studio 中独立配置签名。
核心工程只新增 OpenHarmony 平台源集,不改变 Instant、TimeZone 等现有公共类的使用方式。这样已有 CMP 业务只需要把依赖和 Native 交付加入自己的 OpenHarmony 工程,不需要重写日期时间逻辑。
1.2 固定工具链和版本矩阵
本项目使用以下版本约定:
| 项目 | 配置 | 用途 |
|---|---|---|
| Kotlin Multiplatform | 2.2.21-1.0.0 | JVM、Kotlin/Native 和 KLIB |
| Gradle | 8.14.3 | 根工程和 example 工程 |
| JDK | 17 或更高 | Gradle、Kotlin 编译和 Native 任务 |
| OpenHarmony 目标 | ohosArm64 | ARM64 真机动态库 |
| ABI | arm64-v8a | HAP 原生库架构 |
| DevEco Native SDK | API 20 工具链 | CMake、Ninja、系统头文件和库 |
| Native 依赖 | ace_napi.z、uv、hilog_ndk.z | C ABI/N-API 宿主链接 |
执行 Gradle 脚本前先选择 JDK 17 或更高版本:
export JAVA_HOME="/path/to/jdk-17"
java -version
已验证构建使用的是 JDK 17。DevEco Studio 与命令行 Hvigor 应使用同一套 OpenHarmony SDK,避免 CMake 找到不同架构或不同 API 级别的系统库。
1.3 创建 OpenHarmony 示例目录
页面围绕“当前时间、epoch 毫秒、时区、Native 来源和公共自检”组织:
标题区 Kotlinx Datetime 工作台 / Kotlin Multiplatform / OpenHarmony
数据区 当前 ISO 时间和 now 标识
指标区 EPOCH MS、TIME ZONE、libkotlinx_datetime.so
操作区 重新读取 / 读取当前
自检区 8/8 公共检查通过
“重新读取”会重新读取当前时间并运行公共检查;“读取当前”只请求一次 Native 时钟。页面不直接实现日期时间解析,也不在 ArkTS 中复制时区目录判断。
第 2 阶段:目标与依赖打通
2.1 加入 ohosArm64 目标
核心和示例共享模块都加入 OpenHarmony 目标:
kotlin {
jvm()
ohosArm64()
sourceSets {
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
core/ohosArm64/src/internal/TimeZoneNative.kt 提供 OpenHarmony 平台的系统默认时区入口,core/tzdbOnFilesystem 扩展 IANA 数据库目录查找。公共的 commonMain 和 commonKotlinMain 继续承载解析、格式化和日期运算。
2.2 Native focused build 的作用
example/nativeApp 只构建当前示例需要的 OpenHarmony shared library:
kotlin {
ohosArm64 {
binaries.sharedLib {
baseName = "kotlinx_datetime"
linkerOpts(
"--entry=0",
"--version-script=${project.file("src/ohosArm64Main/linker/shared-library.map")}",
)
linkerOpts("-lace_napi.z", "-luv", "-lhilog_ndk.z")
}
}
}
链接脚本只导出五个 C ABI 符号:
DateTimeCatalog
DateTimeGet
DateTimeNow
DateTimeRunChecks
DateTimeFree
这样 ArkTS 只能通过明确的边界获取目录、单条数据、当前时间和自检结果,Kotlin/Native 内部的类布局和运行时符号不会变成不受控 ABI。
2.3 配置仓库和独立消费工程
example/settings.gradle.kts 配置 Kotlin、Maven Central、Gradle Plugin Portal 和 OpenHarmony 社区 Maven:
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()
}
}
includeBuild("..")
示例共享模块通过项目依赖消费当前源码:
sourceSets {
commonMain.dependencies {
api("org.jetbrains.kotlinx:kotlinx-datetime:${property("datetimeVersion")}")
}
}
在同一仓库开发时,includeBuild("..") 使 example 直接使用当前 checkout;在业务工程中,也可以使用发布到 AtomGit 对应 Maven 仓库的版本,并保持原有 org.jetbrains.kotlinx:kotlinx-datetime 坐标。
2.4 通过构建产物消费共享库
prepareOhos 在 Native 链接成功后复制动态库和 C 头文件:
val prepareOhos by tasks.registering(Copy::class) {
dependsOn("linkDebugSharedOhosArm64")
from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) {
include("libkotlinx_datetime.so")
into("libs/arm64-v8a")
}
from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) {
include("libkotlinx_datetime_api.h")
into("src/main/cpp/include")
}
into(rootProject.layout.projectDirectory.dir("../example/ohosApp/entry"))
}
生成的 .so、头文件和 HAP 属于构建产物,按参考项目规范由 .gitignore 排除。每台开发机都可以根据自身 OpenHarmony SDK 从源码生成匹配的文件。
第 3 阶段:时间与序列化
3.1 为什么需要统一 JSON 契约
ArkTS、C++ 和 Kotlin/Native 不能直接共享 Kotlin 对象,因此边界使用 UTF-8 JSON:
ArkTS nowDateTime()
↓ N-API
C++ DateTimeNow()
↓ C ABI
Kotlin DateTimeExamples.now()
↓ DateTimeExample.toJson()
UTF-8 JSON string
↓ napi_create_string_utf8 + DateTimeFree
ArkUI 页面
页面只消费 JSON,不复制 Instant 的解析规则、epoch 转换规则和时区回退规则。这样 JVM 测试、Native 动态库和设备页面使用同一份 Kotlin 逻辑。
3.2 DateTimeExample 和 DateTimeExamples
公共示例门面位于 example/shared/src/commonMain:
public data class DateTimeExample(
val id: String,
val value: String,
val epochMilliseconds: Long,
val timeZone: String,
)
public object DateTimeExamples {
private val templates = listOf(
"1970-01-01T00:00:00Z",
"2020-02-29T12:34:56.789Z",
"2038-01-19T03:14:07Z",
)
public fun catalog(): List<DateTimeExample> = templates.mapIndexed { index, value ->
example("template-$index", Instant.parse(value))
}
public fun now(): DateTimeExample = example("now", Clock.System.now())
}
example 会把 Instant 转换为稳定的文本和 epoch 毫秒,并读取 TimeZone.currentSystemDefault()。如果极简系统镜像没有完整 tzdb,示例显示层使用 UTC 回退,避免页面因为系统配置缺失无法启动。
3.3 时间模型和 JSON
JSON 边界保持字段稳定:
{
"id": "now",
"value": "2026-09-27T04:43:00.794160Z",
"epochMilliseconds": 1790484180794,
"timeZone": "UTC"
}
Kotlin 使用 quote 对双引号、反斜杠和控制字符做转义,Native 返回以 0 结尾的 UTF-8 缓冲区。页面客户端将字符串解析为 DateTimeExample,不把未校验的动态字段继续传播到 ArkUI。
3.4 自检和错误边界
DateTimeExamples.runChecks() 覆盖八项检查:
- 三个确定性时间模板都可用;
- ISO 文本可以往返解析;
- 闰日样例的 epoch 毫秒值稳定;
- UTC 转换得到零偏移;
- 系统默认时区可以读取或安全回退;
- 系统当前时钟可用;
- 非法索引会被拒绝;
- 公共门面不依赖平台专属类型。
Native 异常会转换为 {"error":"..."}。C++ 创建 ArkTS 字符串后立即调用 DateTimeFree,页面可以显示可读错误,同时不会泄漏 Kotlin/Native 堆内存。
第 4 阶段:原生桥接(技术难点)
4.1 Kotlin/Native 对象不能直接交给 ArkTS
Kotlin/Native 的 Instant、TimeZone 和 DateTimeExample 都属于 Kotlin 运行时对象。ArkTS 只能接收 JavaScript 值,C++ 也不能把 Kotlin 对象地址直接当成 JavaScript 对象。
最终采用三层桥接:
ArkTS
│ JSON string
▼
C++ N-API entry
│ const char*
▼
Kotlin/Native C ABI
│ DateTimeExamples
▼
UTF-8 JSON + explicit free
4.2 方案对比
| 方案 | 优点 | 缺点 | 选用 |
|---|---|---|---|
| 直接导出 Kotlin 对象 | 代码少 | ABI、生命周期和类型不可控 | ❌ |
| 只导出 epoch 数字 | 实现简单 | 页面会重新实现 ISO 和时区展示 | ❌ |
| C ABI + JSON | 边界清晰、易调试、易扩展 | 有一次序列化开销 | ✅ |
| 在 ArkTS 重写日期时间库 | 页面调用直接 | KMP 与 ArkTS 逻辑容易分叉 | ❌ |
4.3 Kotlin/Native 导出函数
Native bridge 导出五个入口:
@CName("DateTimeCatalog")
public fun catalogNative(): CPointer<ByteVar> = response {
DateTimeExamples.catalog().joinToString(prefix = "[", postfix = "]") { it.toJson() }
}
@CName("DateTimeGet")
public fun dateTimeNative(index: Int): CPointer<ByteVar> =
response { DateTimeExamples.get(index).toJson() }
@CName("DateTimeNow")
public fun nowNative(): CPointer<ByteVar> =
response { DateTimeExamples.now().toJson() }
@CName("DateTimeRunChecks")
public fun checksNative(): CPointer<ByteVar> = response { ... }
@CName("DateTimeFree")
public fun freeNative(pointer: CPointer<ByteVar>?) {
if (pointer != null) nativeHeap.free(pointer.rawValue)
}
返回值使用 Native heap 分配的、以 0 结尾的 C 字符串。每一块返回缓冲区都由 DateTimeFree 释放,ArkTS 永远不会保存 Native 指针。
4.4 C++ N-API 方法分发
C++ 注册四个 ArkTS 方法:
napi_property_descriptor methods[] = {
{"getCatalog", nullptr, Catalog, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"getDateTime", nullptr, GetDateTime, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"nowDateTime", nullptr, NowDateTime, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"runChecks", nullptr, RunChecks, nullptr, nullptr, nullptr,
napi_default, nullptr},
};
getDateTime 会检查参数数量、整数类型、非负范围和 int32_t 上限,再调用 DateTimeGet。当前时间和公共检查都遵循“调用 Native、创建 ArkTS 字符串、释放 Native 缓冲区”的顺序。
CMake 将 Kotlin/Native 动态库作为 imported library:
add_library(kotlinx_datetime SHARED IMPORTED)
set_target_properties(kotlinx_datetime PROPERTIES
IMPORTED_LOCATION
"${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/arm64-v8a/libkotlinx_datetime.so")
add_library(entry SHARED napi_init.cpp)
target_include_directories(entry PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(entry PRIVATE kotlinx_datetime libace_napi.z.so)
4.5 N-API 生命周期
ArkTS nowDateTime()
│
▼
DateTimeNow()
│
▼
napi_create_string_utf8(...)
│
▼
DateTimeFree(nativeBuffer)
│
▼
return JS string
C++ 负责完成跨语言字符串转换和 Native 释放,ArkTS 页面不需要知道 Kotlin/Native 的堆实现。Native 模块通过构造函数注册 entry,ArkTS 加载 libentry.so 后即可调用这些方法。
第 5 阶段:系统能力封装
5.1 ArkTS 调用 Kotlin/Native 时间能力
本项目的系统能力不是额外的传感器 API,而是通过 Native 读取系统时钟和系统时区。ArkTS 只调用 DateTimeClient.ets:
import kotlinx_datetimeNative from 'libentry.so';
export function nowDateTime(): DateTimeExample {
return parse<DateTimeExample>(kotlinx_datetimeNative.nowDateTime());
}
export function runChecks(): DateTimeChecks {
return parse<DateTimeChecks>(kotlinx_datetimeNative.runChecks());
}
当前时间由 Kotlin/Native 的 Clock.System.now() 产生,系统默认时区由 TimeZone.currentSystemDefault() 解析。页面不使用 JavaScript Date 替换 Kotlin 结果。
5.2 时区目录和系统能力
OpenHarmony 的 IANA tzdb 路径可能随系统镜像变化,适配按顺序检查:
/system/usr/share/zoneinfo
/system/etc/zoneinfo
/data/service/el1/public/for-all-apps/zoneinfo
/data/service/el1/public/for-all-apps/etc/zoneinfo
/etc/zoneinfo
/usr/share/zoneinfo
同时支持 TZ 环境变量和 /etc/localtime 回退。时区文件读取发生在 Kotlin/Native 侧,不需要给 ArkTS 增加一个重复的时区数据库。
5.3 ArkUI 页面状态
Index.ets 保存当前时间、自检状态和错误文本:
@State private selected: DateTimeExample = emptyDateTime();
@State private status: string = '正在加载';
@State private errorText: string = '';
aboutToAppear(): void {
this.refresh();
}
refresh 同时请求 nowDateTime() 和 runChecks();readCurrent 只更新当前值。页面将 epochMilliseconds、timeZone 和动态库名称放在同一张卡片中,便于确认数据来自 Native,而不是页面常量。
5.4 页面交互预设
页面提供两个操作按钮:
- 重新读取:重新读取当前时间并运行八项公共检查;
- 读取当前:再次调用 Kotlin/Native 当前时钟,不修改公共检查结果。
页面底部显示“数据由 Kotlin/Native 生成,经 N-API 提供给 ArkTS”,用于明确展示跨语言链路。时区不可读时显示 UTC,并保留原库对命名时区请求的错误行为。
第 6 阶段:示例与验证
6.1 example 工程结构
example/
├── shared/
│ ├── src/commonMain/.../DateTimeExamples.kt
│ └── src/commonTest/.../DateTimeExamplesTest.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/
├── datetime/DateTimeClient.ets
└── pages/Index.ets
shared 验证公共门面,nativeApp 产生 Native 动态库,ohosApp 负责 ArkUI 页面和宿主模块。三者边界清晰,任何一层失败都能单独定位。
6.2 原生模块注册
napi_init.cpp 通过 napi_module_register 注册 entry 模块,ArkTS 类型声明位于:
example/ohosApp/entry/src/main/cpp/types/libentry/index.d.ts
ArkTS 使用:
import kotlinx_datetimeNative from 'libentry.so';
getCatalog、getDateTime、nowDateTime 和 runChecks 都返回 JSON 文本,再由 DateTimeClient.ets 解析为类型化对象。
6.3 Native 动态库准备
执行:
export JAVA_HOME="/path/to/jdk-17"
./scripts/build-openharmony.sh
脚本依次执行示例 JVM 测试、compileKotlinOhosArm64、linkDebugSharedOhosArm64 和 prepareOhos。成功后生成:
example/ohosApp/entry/libs/arm64-v8a/libkotlinx_datetime.so
example/ohosApp/entry/src/main/cpp/include/libkotlinx_datetime_api.h
还可以检查 ARM64 ELF 的强依赖:
python3 scripts/check-native-deps.py \
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
example/ohosApp/entry/libs/arm64-v8a
6.4 构建、签名和安装
未配置签名时,DevEco 仍可构建未签名 HAP;真机安装需要签名 HAP。先把工程复制到仓库外:
python3 scripts/prepare-signing-project.py /absolute/path/kotlinx-datetime-signing
配置证书、profile 和 keystore 后执行:
DEVECO_HOME=/Applications/DevEco-Studio.app/Contents \
scripts/build-hap.sh /absolute/path/kotlinx-datetime-signing
直接使用当前 DevEco 工程构建也可以执行:
cd example/ohosApp
/Applications/DevEco-Studio.app/Contents/tools/node/bin/node \
/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw.js \
--mode module -p module=entry@default -p product=default \
-p requiredDeviceType=phone assembleHap --analyze=normal \
--parallel --incremental --daemon
没有签名配置时的产物为:
entry/build/default/outputs/default/entry-default-unsigned.hap
签名证书、profile、p12 和密码只保存在本机,不要提交到 AtomGit。安装已签名 HAP:
hdc list targets
hdc -t <设备序列号> install -r \
/absolute/path/kotlinx-datetime-signing/entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b org.jetbrains.kotlinx.datetime.sample
四、完整代码对照
4.1 整体架构
Kotlinx Datetime commonMain
│ Instant / Clock / TimeZone
▼
core/ohosArm64 + tzdbOnFilesystem
│ Kotlin/Native
▼
libkotlinx_datetime.so
│ DateTimeNow / DateTimeGet / DateTimeRunChecks
▼
C++ N-API libentry.so
│ JSON string
▼
ArkTS DateTimeClient + Index.ets
4.2 文件清单
| 文件 | 职责 |
|---|---|
core/ohosArm64/src/internal/TimeZoneNative.kt | OpenHarmony 系统时区入口 |
core/tzdbOnFilesystem/src/internal/TzdbOnFilesystem.kt | IANA tzdb 路径和文件读取 |
example/shared/.../DateTimeExamples.kt | 公共示例门面、当前时间和八项检查 |
example/nativeApp/.../NativeBridge.kt | C ABI、JSON 返回和内存释放 |
example/nativeApp/.../shared-library.map | Native 导出符号白名单 |
example/ohosApp/.../CMakeLists.txt | 导入 .so 并链接 N-API |
example/ohosApp/.../napi_init.cpp | N-API 导出和参数检查 |
example/ohosApp/.../DateTimeClient.ets | JSON 解析和 ArkTS 类型定义 |
example/ohosApp/.../Index.ets | ArkUI 真机展示页面 |
scripts/build-openharmony.sh | 测试、Native 链接和产物复制 |
scripts/build-hap.sh | 已配置签名工程的 HAP 构建 |
docs/openharmony/VALIDATION.md | 自动检查和真机验收说明 |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| Kotlin | Instant.parse | 解析 ISO-8601 文本 |
| Kotlin | Clock.System.now | 读取当前时间 |
| Kotlin | TimeZone.currentSystemDefault | 读取系统默认时区 |
| Kotlin | DateTimeExamples.runChecks | 执行八项公共检查 |
| Native | DateTimeNow | 返回当前时间 JSON |
| Native | DateTimeGet | 返回确定性示例 JSON |
| N-API | nowDateTime | 向 ArkTS 暴露当前时间 |
| ArkTS | refresh | 更新页面和自检状态 |
4.4 ArkTS 与 Kotlin 的边界
ArkTS 只负责宿主页面状态:
this.selected = nowDateTime();
const checks: DateTimeChecks = runChecks();
this.status = checks.passed ? `${checks.checks.length}/8 公共检查通过` : '公共检查失败';
Kotlin 负责时间值和时区语义:
public fun now(): DateTimeExample = example("now", Clock.System.now())
两者之间只传输 JSON,不传输 Instant、TimeZone 对象、ArkTS class 实例或未校验的动态结构。
五、关键决策说明
决策 1:把 ohosArm64 加入公共构建约定
OpenHarmony 真机只能加载对应 ABI 的 Native 库。只有真正链接 ohosArm64,才能证明公共日期时间代码可以进入 OpenHarmony 运行时。
决策 2:独立消费者必须通过构建产物消费
example 单独解析当前 kotlinx-datetime,ohosApp 单独接收 .so 和头文件,避免根工程编译通过却无法在 DevEco 中打包。
决策 3:JSON 作为跨语言数据契约
JSON 让 ArkTS、C++ 和 Kotlin/Native 的边界清楚可调试,并允许后续增加可信字段而不暴露内部对象布局。
决策 4:桥接层只开放五个 C ABI 入口
目录、单条记录、当前时间、自检和释放已经覆盖示例所需能力。减少 ABI 符号可以降低 Native 生命周期和兼容风险。
决策 5:系统时区读取放在 Kotlin/Native
时区数据库是日期时间库的实现细节,不应在 ArkTS 重复维护路径和解析规则。OpenHarmony 只新增平台入口,解析器仍然复用 KMP 代码。
决策 6:把库验证和设备验证分开
JVM 测试验证公共逻辑,Native 链接验证 ABI,Hvigor 验证 HAP,真机验证系统时钟和时区。每一层都有明确的失败边界。
六、测试与验证
6.1 测试环境
本次构建验证使用:
- macOS ARM64;
- JDK 17;
- Kotlin Multiplatform
2.2.21-1.0.0; - Gradle
8.14.3; - DevEco Studio 及 OpenHarmony ARM64 Native SDK;
arm64-v8aOpenHarmony 目标;- DevEco
hvigorw assembleHap。
当前记录的是编译、Native 链接和页面效果验证;生产证书由使用者在本机手动配置。
6.2 静态检查与单元测试
./gradlew check
(cd example && ./gradlew :shared:jvmTest)
示例测试覆盖确定性模板数量、ISO 往返、epoch 毫秒、UTC 转换、非法索引、系统默认时区和当前时钟。八项检查由 Native 动态库再次执行并在 ArkUI 页面显示。
6.3 原生桥接和 HAP 验证
./scripts/build-openharmony.sh
python3 scripts/check-native-deps.py \
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
example/ohosApp/entry/libs/arm64-v8a
验证重点:
:kotlinx-datetime:compileKotlinOhosArm64 BUILD SUCCESSFUL
:nativeApp:prepareOhos BUILD SUCCESSFUL
libkotlinx_datetime.so 生成成功
libkotlinx_datetime_api.h 生成成功
hvigor assembleHap BUILD SUCCESSFUL
当前工程未配置签名,因此 Hvigor 会提示没有 signingConfig,并输出 entry-default-unsigned.hap;签名由使用者在 DevEco Studio 中手动完成。
6.4 功能验证用例
用例 1:默认页面和自检
启动应用后页面显示 Kotlinx Datetime 工作台、当前 ISO 时间和 8/8 公共检查通过。初始数据由 Native DateTimeNow() 和 DateTimeRunChecks() 生成。
用例 2:重新读取
点击“重新读取”,页面重新读取系统时间并执行八项公共检查。检查通过时状态文本保持绿色,时间值和 epoch 毫秒会随调用更新。
用例 3:读取当前
点击“读取当前”,页面只调用 nowDateTime()。这可以单独确认 Clock.System.now()、C ABI、N-API 和 ArkTS 的返回链路。
用例 4:时区回退
在系统能读取 IANA tzdb 的设备上显示设备默认时区;在极简系统镜像上显示 UTC。UTC 回退不会影响 Instant 的 ISO 文本和 epoch 毫秒。
用例 5:非法索引
通过 Native getDateTime(-1) 或越界索引时,C++ 先拒绝不合法参数,Kotlin 对合法但越界的索引返回错误 JSON,页面不会因为 Native 异常崩溃。
用例 6:重复读取和页面生命周期
连续点击两个按钮,确认没有 Native 内存持续增长;离开页面后不保留 Kotlin 指针。每次返回字符串都在 C++ 复制完成后调用 DateTimeFree。
6.5 验证结论
自动测试、Kotlin/Native ARM64 编译、ELF 依赖检查、Hvigor HAP 构建和 ArkUI 页面效果均已完成。效果图中的当前时间、epoch 毫秒、UTC、libkotlinx_datetime.so 和 8/8 公共检查通过,说明从 KMP 公共代码到 OpenHarmony 页面的一整条链路已经打通。
七、运行效果
7.1 真机效果图

效果图中可以看到:
- 页面标题为“Kotlinx Datetime 工作台”;
- 副标题说明 Kotlin Multiplatform / OpenHarmony;
- 当前记录的
id为now; - ISO 时间由 Kotlin/Native 的
Clock.System.now()生成; EPOCH MS与 ISO 文本对应;TIME ZONE显示UTC,表示当前运行环境走了安全时区回退;SOURCE显示libkotlinx_datetime.so,说明页面数据来自 Native 动态库;- 底部显示
8/8 公共检查通过; - 页面底部明确说明数据由 Kotlin/Native 生成,经 N-API 提供给 ArkTS。
7.2 命令速查
# 根工程测试和 OpenHarmony Native 准备
export JAVA_HOME="/path/to/jdk-17"
./scripts/build-openharmony.sh
# 单独运行共享测试
(cd example && ./gradlew :shared:jvmTest)
# 设备依赖检查
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/kotlinx-datetime-signing
# 在已配置签名的工程中构建 HAP
DEVECO_HOME=/Applications/DevEco-Studio.app/Contents \
scripts/build-hap.sh /absolute/path/kotlinx-datetime-signing
# 安装和启动已签名 HAP
hdc -t <设备序列号> install -r \
/absolute/path/kotlinx-datetime-signing/entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b org.jetbrains.kotlinx.datetime.sample
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配:必须把公共日期时间代码真正编译成
ohosArm64动态库。 - N-API 不负责业务判断:C++ 只做类型检查、字符串转换和释放,时间和时区语义放在 Kotlin。
.so必须先生成再启动 Ninja:DevEco CMake 只能消费已经复制到entry/libs/arm64-v8a的文件。- 生成头文件和动态库要成对更新:两者来自同一次
prepareOhos,避免 C ABI 声明和符号不一致。 - 权限不是时区数据库:OpenHarmony 页面可以启动,并不代表系统镜像一定包含完整 IANA tzdb,因此需要 UTC 回退。
- 签名配置与源码分离:无 signingConfig 时可以生成 unsigned HAP,但真机安装必须使用匹配 bundle 的签名包。
8.2 已知问题
- 当前交付只包含
arm64-v8a,不支持 32 位或 x86 模拟器; - 不同 OpenHarmony 系统镜像的 tzdb 目录可能不同,无法读取时会回退到 UTC;
- 当前示例通过手写 JSON 传递少量字段,生产业务可以替换为更完整的序列化契约;
- Kotlin/Native 和 OpenHarmony 工具链版本必须匹配,升级 Kotlin 或 DevEco 后需要重新验证;
- 当前工程没有提交签名证书,生产 HAP 需要使用者本机配置 profile 和 keystore。
8.3 未来优化方向
- 增加
ohosArm64的平台集成测试,覆盖更多系统镜像的时区路径; - 将 Native JSON 返回封装为统一的序列化模块,减少桥接文件中的手写转义;
- 为 Compose Multiplatform 页面提供日期时间卡片和时区选择示例;
- 增加设备时区、UTC 回退和 tzdb 缺失的诊断信息;
- 在持续集成中加入 Native 链接、ELF 依赖检查和未签名 HAP 构建;
- 为
catalog、getDateTime和错误 JSON 增加 ArkTS 侧的契约测试。
九、总结
9.1 核心难点回顾
本次适配真正需要处理的不是一个“读取当前时间”调用,而是一条完整跨端链路:
OpenHarmony system clock and tzdb
→ Kotlin/Native ohosArm64
→ C ABI
→ C++ N-API
→ ArkTS DateTimeClient
→ ArkUI 页面
9.2 封装层次
- KMP 层:提供
Instant、Clock、TimeZone和稳定的公共解析行为; - OpenHarmony Native 层:提供系统时区入口并生成 ARM64 动态库;
- C ABI 层:输出有限的 JSON 字符串入口和显式释放函数;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面生命周期、按钮交互、JSON 解析和错误展示;
- DevEco 层:完成 CMake、HAP、签名、安装和运行。
9.3 三条经验
- 先让公共日期时间模型在 JVM 和 Native 通过,再接入 ArkUI;
- 用 JSON 和少量 C ABI 代替跨语言对象传递,先固定内存所有权;
- 把自动测试、Native 链接、HAP 构建和真机页面分别记录,避免把“编译成功”误认为“设备运行成功”。
9.4 适配成果
当前 kotlinx-datetime 已完成:
ohosArm64Kotlin/Native 目标;- OpenHarmony 系统时区和 IANA tzdb 路径适配;
Instant、Clock.System.now()、TimeZone.currentSystemDefault()的 Native 运行链路;- Kotlin/Native + C ABI + C++ N-API 桥接;
- ArkUI 工作台页面和八项公共自检;
- Native 动态库、C 头文件和 HAP 构建脚本;
- ARM64 ELF 依赖检查和仓库外签名工程准备;
- 中英文 OpenHarmony 文档、AtomGit 链接和效果图;
- 与参考 KMP/CMP 工程一致的模块、脚本和验证边界。
参考文档
更多推荐



所有评论(0)