本文记录 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-01970-01-01T00:00:00ZUnix epoch 边界
template-12020-02-29T12:34:56.789Z闰日与毫秒精度
template-22038-01-19T03:14:07Z2038 时间边界
nowClock.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 Multiplatform2.2.21-1.0.0JVM、Kotlin/Native 和 KLIB
Gradle8.14.3根工程和 example 工程
JDK17 或更高Gradle、Kotlin 编译和 Native 任务
OpenHarmony 目标ohosArm64ARM64 真机动态库
ABIarm64-v8aHAP 原生库架构
DevEco Native SDKAPI 20 工具链CMake、Ninja、系统头文件和库
Native 依赖ace_napi.z、uv、hilog_ndk.zC 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() 覆盖八项检查:

  1. 三个确定性时间模板都可用;
  2. ISO 文本可以往返解析;
  3. 闰日样例的 epoch 毫秒值稳定;
  4. UTC 转换得到零偏移;
  5. 系统默认时区可以读取或安全回退;
  6. 系统当前时钟可用;
  7. 非法索引会被拒绝;
  8. 公共门面不依赖平台专属类型。

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.ktOpenHarmony 系统时区入口
core/tzdbOnFilesystem/src/internal/TzdbOnFilesystem.ktIANA tzdb 路径和文件读取
example/shared/.../DateTimeExamples.kt公共示例门面、当前时间和八项检查
example/nativeApp/.../NativeBridge.ktC ABI、JSON 返回和内存释放
example/nativeApp/.../shared-library.mapNative 导出符号白名单
example/ohosApp/.../CMakeLists.txt导入 .so 并链接 N-API
example/ohosApp/.../napi_init.cppN-API 导出和参数检查
example/ohosApp/.../DateTimeClient.etsJSON 解析和 ArkTS 类型定义
example/ohosApp/.../Index.etsArkUI 真机展示页面
scripts/build-openharmony.sh测试、Native 链接和产物复制
scripts/build-hap.sh已配置签名工程的 HAP 构建
docs/openharmony/VALIDATION.md自动检查和真机验收说明

4.3 关键 API 对照

层次API作用
KotlinInstant.parse解析 ISO-8601 文本
KotlinClock.System.now读取当前时间
KotlinTimeZone.currentSystemDefault读取系统默认时区
KotlinDateTimeExamples.runChecks执行八项公共检查
NativeDateTimeNow返回当前时间 JSON
NativeDateTimeGet返回确定性示例 JSON
N-APInowDateTime向 ArkTS 暴露当前时间
ArkTSrefresh更新页面和自检状态

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-v8a OpenHarmony 目标;
  • 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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配:必须把公共日期时间代码真正编译成 ohosArm64 动态库。
  2. N-API 不负责业务判断:C++ 只做类型检查、字符串转换和释放,时间和时区语义放在 Kotlin。
  3. .so 必须先生成再启动 Ninja:DevEco CMake 只能消费已经复制到 entry/libs/arm64-v8a 的文件。
  4. 生成头文件和动态库要成对更新:两者来自同一次 prepareOhos,避免 C ABI 声明和符号不一致。
  5. 权限不是时区数据库:OpenHarmony 页面可以启动,并不代表系统镜像一定包含完整 IANA tzdb,因此需要 UTC 回退。
  6. 签名配置与源码分离:无 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 三条经验

  1. 先让公共日期时间模型在 JVM 和 Native 通过,再接入 ArkUI;
  2. 用 JSON 和少量 C ABI 代替跨语言对象传递,先固定内存所有权;
  3. 把自动测试、Native 链接、HAP 构建和真机页面分别记录,避免把“编译成功”误认为“设备运行成功”。

9.4 适配成果

当前 kotlinx-datetime 已完成:

  • ohosArm64 Kotlin/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 工程一致的模块、脚本和验证边界。

参考文档

Logo

一站式 AI 云服务平台

更多推荐