开源鸿蒙平台 KMP 三方库「FileKit」适配全流程
本文记录
FileKit接入 OpenHarmony 的完整过程,覆盖文件引擎库盘点、鸿蒙 kotlinx 制品线选型、真实引擎编译、ohosArm64/ohosX64目标、expect/actual 全面重建、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
FileKit是基于kotlinx-io+kotlinx-coroutines的 Kotlin Multiplatform 文件操作库:PlatformFile把各平台文件抽象成统一类型,围绕它提供readBytes/readString/write/writeString读写、copyTo事务复制、atomicMove原子移动、带符号链接纪律的递归删除、list目录列举、filesDir/cacheDir等标准目录、书签持久化与PlatformFileSerializer序列化。本次适配与社区早期路线(qrose / haze / PreCompose / voyager 的"零依赖内核提取")有本质区别:社区 Maven 上已经出现完整的 HarmonyOS kotlinx 制品线(coroutines 1.11.0-0.1.0-07、serialization 1.11.0-0.1.0-07、kotlinx-io-core 0.9.1-0.1.0-07,全部带ohosArm64/ohosX64Gradle 变体),因此不需要提取内核——真实的引擎(读写流、事务复制、原子移动、安全删除、序列化器)被原样编译成 OpenHarmony KLIB,一个完整的跨平台文件引擎运行在真机上,ArkUI 页面作为验收宿主。
项目地址: AtomGit/oh-tpc/FileKit.git
上游仓库: vinceglb/FileKit
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
FileKit 为 Kotlin Multiplatform 应用提供"对象 ↔ 文件系统"这一件事的完整答案:PlatformFile(path) 一行拿到跨平台文件句柄,readString / writeString / write(bytes) 覆盖文本与二进制读写,copyTo 走 prepareDestinationForWrite 目的地解析 + 缓冲流复制,atomicMove 在支持时走 rename 语义,delete(mustExist, recursively) 先用 lstat 识别符号链接、绝不跟随地解链接再递归清空目录,PlatformFileSerializer 把文件序列化成路径字符串,FileKit.filesDir / cacheDir / databasesDir / projectDir 给出平台标准目录,Apple 平台之外书签退化为绝对路径字节快照。KMP 项目把这些能力放在 commonMain 与 nonWebMain,由 Android、iOS、桌面、Linux、Windows、Web 共享。OpenHarmony 应用同样需要这套文件能力,但上游构建(Kotlin 2.4.20 / Gradle 9.8.0 / AGP 9.4.0 + build-logic 约定插件)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以显示按钮,却无法证明公共文件引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证文件引擎、协程运行时、kotlinx-io 文件层、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.4.20 / Gradle 9.8.0 / AGP 9.4.0 与 build-logic 约定插件复合构建、Compose 编译器、vanniktech 发布插件强耦合,与鸿蒙工具链无法共存于一个 Gradle 构建。 |
| expect/actual 面极大 | PlatformFile 本体 + 16 个 commonMain 扩展 expect + nonWebMain 约 25 个构造器/读写/删除/书签 expect + 序列化器 expect,ohos 目标必须全部补齐 actual,这是本批适配库里 expect 面最大的库。 |
| 依赖矩阵 | 引擎依赖 kotlinx-coroutines、kotlinx-serialization-core、kotlinx-io-core,每一个都要有 ohos 变体才能解析;androidx.annotation 没有 ohos 制品。 |
| 调度器语义不同 | 鸿蒙线 coroutines 把 Dispatchers.IO 标为 internal(FFRT 调度模型下没有独立 IO 池),而引擎共享代码 7 处直接使用 Dispatchers.IO。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:真实引擎的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。文件语义完全由上游原版实现负责,ArkTS 只负责调用 Native、解析 JSON 和展示结果。
1.2 库提供的能力
Demo 提供一个真实的文件引擎会话,对应桥接的全部入口:
- 初始化(init):页面启动时把 ArkTS
UIAbilityContext的filesDir/cacheDir注入FileKit.init(...),引擎拿到真实应用沙箱目录; - 写入(write / writeBytes):
PlatformFile.writeString(text)把文本框内容落盘,writeBytes走 Base64 通道写二进制; - 读取(read / readBytes):
readString()读回文本并回填文本框,readBytes()返回 Base64 与字节数; - 元信息(info / exists):
name/absolutePath/size/isDirectory/isRegularFile/mimeType/lastModifiedMs七字段信息卡; - 目录(list / mkdir):列出父目录全部条目(名称 / 路径 / 是否目录 / 大小),一键创建子目录;
- 删除(delete):
delete(recursively = true)递归删除,符号链接只解链接不跟随; - 复制 / 移动(copy / move):
copyTo事务复制、atomicMove原子移动,目的地是目录时自动落到目录/源文件名; - 版本握手:
FileKitVersion()返回{"name":"FileKit","version":"0.16.0-ohos.1"},来自真实 Native 调用而非静态文字。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
op / path | 回显本次操作与目标文件路径。 |
value / base64 / bytes | read 返回文件文本;readBytes 返回 Base64;写操作返回字节数。 |
entries | list 返回 [{name, path, isDirectory, size}] 数组。 |
exists / isDirectory / mimeType / lastModifiedMs | info / exists 的元信息字段。 |
引擎侧完整能力(缓冲流复制、prepareDestinationForWrite 目的地解析、atomicMove、符号链接纪律删除、PlatformFileSerializer、书签路径快照、四个标准目录、resolve / div 路径拼接近)全部在上游 commonMain / nonWebMain / jvmAndNativeMain 里原样随编译进入 .so,JVM 侧保留 68 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的引擎逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | filekit-core 的 commonMain(9 文件)、nonWebMain(8 文件)、jvmAndNativeMain(1 文件)原样编译,不提取、不重写;共享代码仅 2 处手术式修改(7 个 Dispatchers.IO 调用点换成调度器接缝、2 处 @IntRange 注解剥离)。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);filekit-dialogs / filekit-dialogs-compose / filekit-coil / sample/* / build-logic 留盘不进构建。 |
| 桥接稳定 | 使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 版本主体与上游完全一致,只把仓库换到 ohos 制品线(换线不换版),上游测试零改动全绿。 |
| UI 完整 | 单页覆盖全部桥接入口:路径、JSON 文本域、写入 / 读取 / 删除、信息 / 列表 / 目录、复制 / 移动 / 二进制、操作日志和版本握手。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。
filekit-dialogs-compose依赖 Compose、filekit-coil依赖 Coil,两者在鸿蒙线上都没有制品;系统文件选择器在 HarmonyOS 上是 ArkUIDocumentViewPicker的领地。适配面天然收敛在引擎层;ArkTS Demo 只是把桥暴露的文件闭环画出来。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与 expect/actual 边界、确认 kotlinx ohos 制品线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版、源集层级重建、serialization 插件补位
第 3 阶段:引擎补位 ── FileKitIoDispatcher 接缝、@IntRange 剥离、ohosMain 全套 actual(镜像 linux 先例)
第 4 阶段:原生桥接 ── @CName C ABI × 2、扁平 JSON 12 op、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 沙箱路径注入、JSON 文本域、三组操作按钮、信息卡、操作日志和版本握手
第 6 阶段:示例与验证 ── KLIB、双 ABI so、符号检查、68 项 JVM 交叉验证、HAP、签名、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
filekit-core/ 上游文件引擎(进 ohos 构建,jvm + ohosArm64 + ohosX64)
filekit-core/src/commonMain/ PlatformFile 本体 + 16 个扩展 expect + MimeType + 序列化器 expect(9 文件,原样编译)
filekit-core/src/nonWebMain/ 读写/复制/移动/删除/目录/书签/标准目录 expect + 共享引擎实现(8 文件,7 处调度器接缝)
filekit-core/src/jvmAndNativeMain/ 构造器/source/sink/exists/delete/atomicMove 共享 actual(1 文件,2 处调度器接缝)
filekit-core/src/ohosMain/ 新增:PlatformFile.ohos.kt / FileKit.ohos.kt / FileKitIoDispatcher.ohos.kt(全仓唯一新增库源码)
filekit-dialogs/ filekit-dialogs-compose/ filekit-coil/
sample/ build-logic/ 留盘不进 settings(对话框归 ArkTS DocumentViewPicker,Compose/Coil 无 ohos 制品)
example/nativeApp/ Kotlin/Native 动态库(Bridge.kt + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
docs/开源鸿蒙平台 CMP 三方库「FileKit」适配全流程.md
本文(效果图存于 docs/img/)
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :filekit-core 与 :example:nativeApp,rootProject.name = "ohos_filekit"。上游的 build-logic 约定插件复合构建、AGP、Compose 编译器、foojay 解析器、vanniktech 发布插件全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向 Temurin 21.0.12) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 9.8.0(沿用上游 wrapper;2.4.20 线支持 Gradle 9.x) |
| kotlinx | coroutines 1.11.0-0.1.0-07、serialization-core 1.11.0-0.1.0-07、kotlinx-io-core 0.9.1-0.1.0-07(ohos 制品线) |
| JVM 专属 | jna-platform 5.19.1、androidx-annotation 1.10.0(仅 jvmMain 解析) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.filekit |
$env:JAVA_HOME="C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
| 源码集 | 文件数 | 依赖形态 | 处置 |
|---|---|---|---|
commonMain | 9 | 纯 Kotlin expect + MimeType + 异常 | 整体编译(引擎门面) |
nonWebMain | 8 | kotlinx-io + coroutines + serialization + androidx.annotation 注解 | 整体编译(7 处调度器接缝 + 2 处注解剥离) |
jvmAndNativeMain | 1 | kotlinx-io SystemFileSystem | 整体编译(2 处调度器接缝) |
desktopMain | 2 | 用户目录 expect | 仅 jvm 链路编译 |
ohosMain | 3(新增) | kotlinx-io + platform.posix | 本仓新增,镜像 linux actual |
jvmMain / jvmTest / nonWebTest / nativeTest | 7 / 4 / 8 / 1 | java.io、JNA、kotlin-test | jvm 目标编译,ohos 测试任务全局禁用 |
expect 声明的盘点是本库适配的成败手。commonMain 里 PlatformFile 本体加 16 个扩展 expect(name / extension / size / lastModified / readBytes / readString / mimeType / parent / list ×2 等);nonWebMain 再加约 25 个(三个构造器、toKotlinxIoPath、source / sink、exists、createDirectories、atomicMove、delete、5 个书签入口、FileKit 四个标准目录与相册/压缩三件套);jvmAndNativeMain 还有 1 个内部 withPath。ohos 目标顺着默认层级模板挂到 nativeMain,天然继承 jvmAndNativeMain → nonWebMain → commonMain 全链 expect——actual 必须一次补齐,缺一个编译期就炸 No actuals for expect。
上游已有的 actual 实现给出了现成模板:jvmAndNativeMain 里 name / path / 构造器 / exists / source / sink / delete / atomicMove 等共享 actual 基于 kotlinx-io SystemFileSystem,ohos 直接继承;linuxMain 的 POSIX actual(stat / lstat / getcwd)成为 ohosMain 的镜像对象——鸿蒙 musl 底座同样是 POSIX。
1.4 页面边界
标题区 FILEKIT · OPENHARMONY / 版本握手行(KMP 文件引擎 · Kotlin/Native ohosArm64/ohosX64)
Tab 切换 文件操作 / 关于
握手行 FileKit v0.16.0-ohos.1 + init ✓ 徽章
路径区 目标文件路径输入框(默认 filesDir + /filekit_demo.txt,aboutToAppear 注入)
JSON 区 多行文本域(初始 {"hello":"FileKit","platform":"HarmonyOS","count":42})
操作区一 写入(深绿)/ 读取(蓝)/ 删除(红)
操作区二 信息 / 列出目录 / 新建目录
操作区三 复制到 .copy / 移动到 .moved / 写入二进制 / 读取二进制
信息卡 name、absolutePath、size、isDirectory、isRegularFile、mimeType、lastModifiedMs
状态行 最近一次操作结果(失败红字)
日志区 带时间戳的操作日志(最新在前,上限 100 条)
关于区 版本握手、库简介、架构分层、能力清单、已知限制、仓库与社区链接
页面启动时先调 FileKitVersion() 做握手,再取 getContext(this) 的 filesDir / cacheDir 注入 init 操作。
第 2 阶段:目标与依赖打通
2.1 制品线验证(本适配的成败手)
先查社区 Maven(https://maven.eazytec-cloud.com)上有没有 ohos 的 kotlinx 制品。用 .module 元数据逐个验证(KStore 适配时已完成同线验证,本仓直接沿用):
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-core:1.11.0-0.1.0-07
variants: 同上(jvm + ohos 双线齐全)
org.jetbrains.kotlinx:kotlinx-io-core:0.9.1-0.1.0-07
variants: 同上
三个关键结论:其一,三件套版本主体与上游声明逐字相同(coroutines 1.11.0、serialization 1.11.0、kotlinx-io 0.9.1),只是仓库换线——commonMain 里一行依赖坐标都不用改;其二,ohos 制品同时发布 jvm 变体,jvm 交叉验证可以用同一个版本号,不需要双线隔离;其三,Maven Central 上的同名制品没有 ohos 变体,版本必须带 -0.1.0-07 后缀才解析得到。
2.2 工具链钉版(为什么是 2.4.20-0.1.0-07)
社区此前批准的钉法有两条线:HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze,必须降 Gradle 8.14.1)与 2.4.20-0.1.0-07(KStore,Gradle 9.x 原样沿用)。coroutines 1.11.0-0.1.0-07 依赖 kotlin-stdlib 2.4.20-0.1.0-07(元数据 2.4.0),旧线 2.2.21 只能读到 2.3.0,编译期直接报:
Class 'kotlin.Unit' was compiled with an incompatible version of Kotlin.
The binary version of its metadata is 2.4.0, expected version is 2.3.0 or less.
工具链版本跟着依赖走:Kotlin 钉 2.4.20-0.1.0-07。额外好处:FileKit 上游源码本来就是 Kotlin 2.4.20 编的,钉版与上游同代,源码级兼容成本为零;Gradle 9.8.0 上游 wrapper 原样沿用(KStore 以 9.6.1 验证过该线对 Gradle 9.x 的兼容性,9.8.0 同代更无压力),不像 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()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
google()
mavenCentral()
}
}
rootProject.name = "ohos_filekit"
include(":filekit-core")
include(":example:nativeApp")
2.3 编译真实引擎
上游的目标声明全部藏在 build-logic 约定插件里,根构建替换后约定插件消失,filekit-core/build.gradle.kts 重写为自包含 KMP 模块,并把上游在约定插件里定制的源集层级原样重建:
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotlin.serialization) // 上游在 build-logic 里统一 apply,重写后必须模块级补回
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
compilerOptions {
freeCompilerArgs.add("-Xexpect-actual-classes") // PlatformFile 是 expect class
}
sourceSets {
val nonWebMain = create("nonWebMain") { dependsOn(commonMain.get()) }
val jvmAndNativeMain = create("jvmAndNativeMain") { dependsOn(nonWebMain) }
val desktopMain = create("desktopMain") { dependsOn(nonWebMain) }
jvmMain.get().dependsOn(desktopMain)
jvmMain.get().dependsOn(jvmAndNativeMain)
nativeMain.get().dependsOn(jvmAndNativeMain) // ohosMain 经默认模板挂在 nativeMain 下
val nonWebTest = create("nonWebTest") { dependsOn(commonTest.get()) }
jvmTest.get().dependsOn(nonWebTest)
commonMain.dependencies {
api(libs.kotlinx.io) // 0.9.1-0.1.0-07
implementation(libs.kotlinx.coroutines) // 1.11.0-0.1.0-07
implementation(libs.kotlinx.serialization.core)
}
jvmMain.dependencies {
implementation(libs.jna.platform)
implementation(libs.androidx.annotation) // JVM 专属,见 2.4
}
commonTest.dependencies {
implementation(kotlin("test"))
implementation(libs.kotlinx.coroutines.test)
}
nonWebTest.dependencies {
implementation(libs.kotlinx.serialization.json)
}
}
}
实测:
./gradlew :filekit-core:compileKotlinOhosArm64 --max-workers=1 # BUILD SUCCESSFUL
./gradlew :filekit-core:compileKotlinOhosX64 --max-workers=1 # BUILD SUCCESSFUL
引擎的全部共享文件一个没动(调度器接缝见第 3 阶段),只有 ohosMain 三个文件是新的。
2.4 测试依赖的处置:测试统一在 JVM 跑
上游测试分布在 nonWebTest(8 文件 + 资源)与 jvmTest(4 文件),依赖 kotlin-test、coroutines-test 与 serialization-json。处置是两条纪律的组合:测试统一在 JVM 跑(jvmTest),根构建全局禁用 ohos 测试任务:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
这样上游测试文件零改动保留,ohos 侧只承担编译与链接。
androidx.annotation 是另一处依赖纪律:它没有 ohos 制品,但 nonWebMain 的 compressImage expect 用了 @IntRange 装饰。处置是把注解从共享源码剥离(下游 JVM actual 通过 jvmMain 专属依赖保留自己的注解),库侧 commonMain 一行依赖坐标都不进 ohos 解析。
第 3 阶段:引擎补位(技术难点之一)
3.1 调度器接缝:Dispatchers.IO 在鸿蒙线上是 internal
引擎共享代码有 7 处 withContext(Dispatchers.IO)(readBytes / readString / write ×3 / writeString / delete / atomicMove),而鸿蒙线 coroutines 把 Dispatchers.IO 标为 internal——FFRT 调度模型下没有独立 IO 池。硬编会直接编译失败。处置是引入一个 8 行的调度器接缝(KStore 适配 StoreDispatcher 的同款思路):
// nonWebMain/.../FileKitIoDispatcher.kt
// Dispatchers.IO is internal in the HarmonyOS coroutines rebuild (the FFRT
// scheduler has no dedicated IO pool), so shared file operations dispatch
// through this seam and every compiled target picks the closest pool.
internal expect val FileKitIoDispatcher: CoroutineDispatcher
三个 actual 各归其位:jvmMain 是 Dispatchers.IO(与上游语义逐字一致),ohosMain 是 Dispatchers.Default(镜像上游 js/wasmJs 的选择),7 处调用点把 Dispatchers.IO 换成 FileKitIoDispatcher,import 各删两行。JVM 交叉验证跑的就是同一份共享代码,语义零漂移。
3.2 ohosMain 全套 actual:镜像 linux 先例
ohosMain 的三个文件是全仓唯一新增的库源码。PlatformFile.ohos.kt 镜像 linuxMain 的 POSIX actual:OhosPath 包装 kotlinx-io Path(对应上游 LinuxPath / WindowsPath);absolutePath 用 getcwd 解析相对路径;lastModified 用 stat 的 st_mtim;createdAt 返回 null——musl stat 的 st_ctim 是 inode 状态变更时间,当成创建时间是不诚实的,与 Linux actual 同一立场;deleteIfSymbolicLink 用 lstat 识别、unlink 解链接,绝不跟随目标;书签退化为绝对路径字节快照(mingw/linux 同款)。mimeType 是唯一分叉点:OpenHarmony 没有 freedesktop globs2 也没有 /etc/mime.types,Linux actual 的系统数据库读取在这里没有对应物,改为内置约 50 项扩展名表覆盖常见 web/媒体/文档类型,表外返回 null。
@Serializable(with = PlatformFileSerializer::class)
public actual class PlatformFile(
public val ohosPath: OhosPath,
) {
public actual override fun toString(): String = ohosPath.path.toString()
/* equals/hashCode 按路径字符串,companion object 补齐 expect */
}
public actual fun PlatformFile(path: Path): PlatformFile =
PlatformFile(ohosPath = OhosPath(path))
FileKit.ohos.kt 镜像 mingw/linux 的 FileKit object:保留 init(appId) / init(filesDir, cacheDir) / init(appId, filesDir, cacheDir) 三连(上游 Windows 示例从另一个模块的平台源集调用 FileKit.init,证明 actual 附加成员可跨模块使用);filesDir / cacheDir 默认落到 /data/storage/el2/base/haps/entry/{files,cache}(鸿蒙应用沙箱标准位置),非 entry 模块的宿主应该用 init 注入真实上下文目录——Demo 正是这么做的;saveImageToGallery / saveVideoToGallery 返回 Result.failure 并指引 ArkTS 侧用 photoAccessHelper;compressImage 抛 FileKitException(Windows/Linux actual 同款立场)。
第 4 阶段:桥协议建模
4.1 JSON 契约
io.github.vinceglb.filekit/**(上游引擎原样编译)
↓ PlatformFile 构造器 / 读写流 / 事务复制 / atomicMove / 符号链接纪律删除
Bridge.kt(@CName × 2,扁平 JSON 进出)
↓ UTF-8 JSON
N-API filekitDispatch() / filekitVersion()
↓
ArkUI Index.ets(单页滚动)
请求与响应:
请求 : {"op":"write","path":"/data/storage/.../filekit_demo.txt","text":"{\"hello\":\"FileKit\"}"}
{"op":"read","path":"..."}
{"op":"info","path":"..."} / {"op":"list","path":"..."} / {"op":"mkdir","path":"..."}
{"op":"delete","path":"...","recursively":"true"}
{"op":"copy","path":"...","destination":"..."} / {"op":"move",...}
{"op":"writeBytes","path":"...","base64":"..."} / {"op":"readBytes","path":"..."}
{"op":"init","filesDir":"...","cacheDir":"..."}
响应 : {"ok":true,"op":"...","path":"...",<op 专属字段>}
{"ok":false,"op":"...","path":"...","error":"..."}
两个设计取向:其一,12 个操作一口进 FileKitDispatch,ArkTS 侧一个 dispatch(op, extra) 帮手统一拼请求、刷状态、写日志;其二,read 的 value 是原生 JSON 字符串回填、readBytes 走 Base64 通道,读写两条路都不做双重转义。
4.2 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("FileKitDispatch")
fun fileKitDispatch(request: String): String = try {
/* op 分发 */
} catch (t: Throwable) {
err(op.ifBlank { "unknown" }, "", t.message ?: t.toString())
}
非法 JSON、缺 op / path / destination、不支持的 op、磁盘读写失败,全部走同一个 ok:false 通道,ArkTS 侧统一渲染成红字状态与日志。
suspend 调用统一用 runBlocking 收进同步 C ABI:鸿蒙线 Dispatchers.Main 的跨线程派发有已知问题(resaca 仓适配时记录过),桥全程不碰 Main,引擎的文件操作调度器在鸿蒙映射为 Default,天然绕开。
第 5 阶段:原生桥接(技术难点之二)
5.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libfilekit_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosfilekit.so)
│ FileKitDispatch / FileKitVersion
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制引擎语义,所以选择 C ABI + JSON。
5.2 Kotlin/Native 导出函数
@CName("FileKitDispatch")
fun fileKitDispatch(request: String): String // 12 op 一口进
@CName("FileKitVersion")
fun fileKitVersion(): String = """{"name":"FileKit","version":"0.16.0-ohos.1"}"""
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
FileKitDispatch | filekitDispatch(req) | init + 11 个文件操作 |
FileKitVersion | filekitVersion() | fork 版本握手(真机版本行的数据来源) |
5.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"filekitDispatch", nullptr, NapiFileKitDispatch, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"filekitVersion", nullptr, NapiFileKitVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosfilekit_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosfilekit_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。
5.4 CMake 和类型声明
add_library(ohosfilekit SHARED IMPORTED)
set_target_properties(ohosfilekit PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(filekit_napi SHARED napi_init.cpp)
target_link_libraries(filekit_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosfilekit)
OHOS_ARCH 为空或 libohosfilekit.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const filekitDispatch: (request: string) => string;
export const filekitVersion: () => string;
第 6 阶段:ArkUI 宿主
6.1 注入路径和触发操作
aboutToAppear(): void {
const ctx = getContext(this) as common.UIAbilityContext;
this.filesDir = ctx.filesDir;
this.cacheDir = ctx.cacheDir;
this.path = `${this.filesDir}/filekit_demo.txt`;
const info = JSON.parse(filekitVersion()) as VersionInfo; // 版本握手:真实 Native 调用
this.dispatch('init', { 'filesDir': this.filesDir, 'cacheDir': this.cacheDir });
}
private dispatch(op: string, extra?: Record<string, string>): void {
const resp = JSON.parse(filekitDispatch(JSON.stringify(req))) as FileKitResponse;
this.apply(resp); // @State 刷新状态行 + 时间戳日志
}
write 前不额外校验文本(写入的就是原文);read 把返回的 value 回填文本框;info 渲染七字段信息卡;list 把父目录条目渲染成名称 + [DIR]/大小 行。
6.2 页面交互
文件操作 Tab 自上而下:握手行、路径输入框(可改,便于演示多文件)、JSON 文本域、三组操作按钮(写入深绿 / 读取蓝 / 删除红;信息 / 列表 / 新建目录;复制 / 移动 / 写入二进制 / 读取二进制)、信息卡、状态行与操作日志。每条日志带 [HH:mm:ss] 时间戳,→ 行显示请求、← 行显示响应(如 ← write ok 42B),最新在前、上限 100 条。关于 Tab 展示版本握手行、库简介、ArkTS → libfilekit_napi.so(N-API)→ libohosfilekit.so(C ABI @CName)→ filekit-core Kotlin 引擎 → kotlinx-io SystemFileSystem → OpenHarmony POSIX 文件系统 六层说明、能力清单、已知限制与仓库 / 社区链接。
6.3 对比度和错误边界
主操作深绿按钮白字,读取蓝、删除红,语义与破坏性对齐;ok:false 时错误原文进状态行与日志。Native 调用本身抛错(理论不该发生)同样被 try/catch 收进日志区。
第 7 阶段:示例与验证
7.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosfilekit)+ copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ └── Bridge.kt @CName × 2 + 12 op 分发 + MiniJson(继承自 cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.filekit、versionName 0.16.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/libohosfilekit_api.h
├── src/main/cpp/types/libfilekit_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 验收页面(628 行)
7.2 构建、签名和安装
./gradlew :filekit-core:jvmTest --max-workers=1 # 68 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 --max-workers=1
./gradlew :example:nativeApp:linkDebugSharedOhosX64 --max-workers=1
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.filekit -m entry
四、完整代码对照
4.1 整体架构
io.github.vinceglb.filekit/**(上游引擎原样编译)
│ PlatformFile / 读写流 / copyTo 事务复制 / atomicMove / 符号链接纪律删除
io.github.vinceglb.filekit(jvmAndNativeMain 共享 actual)
│ 构造器 / source / sink / exists / delete / createDirectories
Bridge.kt + MiniJson
│ FileKitDispatch(12 op)/ FileKitVersion
libfilekit_napi.so
│ filekitDispatch / filekitVersion
Index.ets
│ JSON parse + @State 更新
▼
ArkUI 真机页面(单页滚动)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_filekit、dialogs/coil/sample 移出 |
gradle/libs.versions.toml | Kotlin 2.4.20-0.1.0-07 + ohos kotlinx 制品线钉版 + serialization 插件别名 |
build.gradle.kts(根) | 版本 0.16.0-ohos.1、全局禁用 Ohos*Test* 任务 |
filekit-core/build.gradle.kts | 自包含 KMP:jvm + ohosArm64 + ohosX64,源集层级重建,serialization 插件模块级补回 |
filekit-core/src/nonWebMain/.../FileKitIoDispatcher.kt | 调度器接缝 expect(8 行) |
filekit-core/src/ohosMain/.../PlatformFile.ohos.kt | ohos actual 主体:OhosPath、stat/lstat/getcwd、内建 MIME 表、书签快照 |
filekit-core/src/ohosMain/.../FileKit.ohos.kt | FileKit object actual:init 三连、沙箱默认目录、相册/压缩立场 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 2 个 C ABI、12 op、runBlocking 包裹、MiniJson、错误通道 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(16 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | PlatformFile(path) / PlatformFile(base, child) | 按路径构造(ohos 包装 OhosPath(Path)) |
| 引擎 | readBytes / readString / write / writeString | 读写四件套,suspend,调度器接缝派发 |
| 引擎 | copyTo | prepareDestinationForWrite 目的地解析 + 缓冲流复制 |
| 引擎 | atomicMove | 原子移动,目的地为目录时落到 目录/源文件名 |
| 引擎 | delete(mustExist, recursively) | lstat 识别符号链接只解链接,递归清空后删除 |
| 引擎 | list / createDirectories / exists / isDirectory | 目录与存在性 |
| 引擎 | FileKit.filesDir / cacheDir / databasesDir / projectDir | 标准目录(ohos 沙箱默认 + init 注入) |
| 引擎 | PlatformFileSerializer | 文件 ↔ 路径字符串序列化 |
| 引擎 | bookmarkData / fromBookmarkData | 书签(ohos 为绝对路径字节快照) |
| Native | FileKitDispatch / FileKitVersion | C ABI JSON 导出 |
| N-API | filekitDispatch(req) / filekitVersion() | 向 ArkTS 暴露能力 |
| ArkTS | dispatch(op) | 触发操作并刷新 UI |
4.4 ArkTS 与 Kotlin 的边界
const res = JSON.parse(filekitDispatch(JSON.stringify({
op: 'copy',
path: context.filesDir + '/filekit_demo.txt',
destination: context.filesDir + '/backup/filekit_demo.txt',
})));
// res.ok === true;目的地是目录时引擎自动落到 目录/源文件名
when (op) {
"copy" -> runBlocking {
PlatformFile(path).copyTo(PlatformFile(destination)) // 上游事务复制
}
"move" -> runBlocking {
PlatformFile(path).atomicMove(PlatformFile(destination))
}
"info" -> { /* name/size/isDirectory/mimeType/lastModifiedMs 七字段 */ }
}
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:真编译引擎 + 补 ohos actual,而不是提取零依赖内核
早期仓库抽内核,是因为 ohos 上没有它们依赖的 kotlinx 制品。本次适配先验证制品线(coroutines / serialization / kotlinx-io 全部存在与上游同版本的 ohos 变体),障碍已消失——再抽内核等于把文件引擎手抄一遍,反而引入漂移风险。真编译让事务复制、原子移动、符号链接纪律、序列化器全部是上游原件;ohos 侧只补 PlatformFile 的 POSIX actual(镜像上游 linux 实现),引擎逻辑一行不抄。
决策 2:工具链升到 Kotlin 2.4.20-0.1.0-07,Gradle 沿用 9.8.0
coroutines 1.11.0-0.1.0-07 基于 stdlib 2.4.20 构建,旧钉法 2.2.21 读不了 2.4 元数据;上游本来就是 Kotlin 2.4.20,2.4.20-0.1.0-07 与之同代零降级。Gradle 方面 2.4.20 线支持 9.x(KStore 以 9.6.1 验证),上游 wrapper 9.8.0 原样保留——只有 2.2.21 旧线才必须降 8.14.1,两条工具链线的经验不能互相套用。
决策 3:根构建替换(cmp_qrose 模式),dialogs/compose/coil 留盘
上游根构建(build-logic 约定插件 / AGP / Compose 编译器 / vanniktech 发布)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。filekit-dialogs 是系统选择器封装,HarmonyOS 上选文件是 ArkUI DocumentViewPicker 的领地,硬编进 Kotlin/Native 既做不到也没有意义;filekit-dialogs-compose / filekit-coil 依赖 Compose / Coil,鸿蒙线上没有制品——三者留盘不进 settings.gradle.kts,它们的存在方式只有这一种。
决策 4:调度器接缝而不是改引擎并发逻辑
Dispatchers.IO 在鸿蒙线是 internal,但引擎的并发语义(文件操作离开调用线程)必须保留。处置不是删掉 withContext,而是把"用哪个调度器"抽成 internal expect:JVM actual 逐字还原 Dispatchers.IO,ohos actual 映射 Default(与上游 js/wasmJs 的选择一致)。7 处调用点换名字,0 行逻辑改动,68 项 JVM 用例证明语义没漂。
决策 5:jvmTest 交叉验证 + 测试零改动
引擎逻辑在共享源码集,JVM 与 ohos 跑同一份代码;68 项 jvmTest 跑的就是上游原测试文件(含删除纪律、序列化、书封信封、缩放工具),是"内核没漂"的闸门。测试依赖统一 JVM 解析 + 根构建禁用 Ohos*Test*——测试文件一个字节不动,交叉验证的有效性才成立。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x86_64)、JDK 21(Temurin 21.0.12)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。本机 Gradle 校验按仓库规范执行:单模块单目标、--max-workers=1、顺序运行,不跑仓库级 assemble/check/build。
6.2 JVM 交叉验证
./gradlew :filekit-core:jvmTest --max-workers=1
68 项用例全部通过(上游原测试文件,零改动):
| 测试类 | 用例数 | 钉住什么 |
|---|---|---|
PlatformFileNonWebTest | 12 | nonWeb 引擎门面:读写、目录、父级、绝对路径 |
PlatformFileDeletionTest(JVM) | 25 | 删除纪律:mustExist、递归、符号链接不跟随、目录残留失败 |
ResizeUtilsTest | 17 | 图像缩放计算 |
FileKitUserDirectoriesJvmTest | 6 | JVM 标准目录解析 |
PlatformFileSerializationTest | 3 | PlatformFileSerializer 往返 |
PlatformFileJvmTest | 3 | JVM 平台行为 |
MacOsBookmarkEnvelopeTest | 2 | 书信访封编解码 |
依赖换线没有 API 漂移:coroutines 1.11.0、serialization 1.11.0、kotlinx-io 0.9.1 换成 -0.1.0-07 线后版本主体不变,上游测试照跑全绿——这正是"换线不换版"的直接证据。
6.3 HAP 验证
:filekit-core:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
:example:nativeApp:linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: FileKitDispatch / FileKitVersion 均为全局 T 符号(双 ABI)
DevEco 自动签名 + assembleHap: finished
install bundle successfully
start ability successfully
libohosfilekit.so(debugShared 未 strip)arm64 约 7.4 MiB、x86_64 约 6.9 MiB,连同 Konan libc++_shared.so(约 1.8 MiB)一起由 Gradle copy 任务回填 entry/libs/<abi>/,CMake POST_BUILD 再保证三件套同目录。
6.4 功能用例
- 启动后握手行出现
FileKit v0.16.0-ohos.1+init ✓(来自真实 Native 调用,非静态文字); init自动注入真机沙箱目录,路径框默认/data/storage/el2/base/haps/entry/files/filekit_demo.txt;- 默认 JSON
{"hello":"FileKit","platform":"HarmonyOS","count":42}点写入,返回ok:true与字节数; - 点读取,
value原样返回写入时的对象回填文本框,字段无转义损伤; - 杀掉应用重启后再点读取,值仍在——落盘持久化成立,不是内存缓存;
- 点删除返回
ok:true,重启后再读报文件不存在; - 信息卡展示 name / absolutePath / size / isRegularFile / mimeType(按扩展名给
text/plain)/ lastModifiedMs; - 列出目录把父目录条目渲染成名称 +
[DIR]/大小 行; - 复制 / 移动分别走
copyTo/atomicMove,目的地为目录时自动落到目录/源文件名; - 写入二进制 / 读取二进制走 Base64 通道,返回字节数;
- 每步操作的操作日志按时间戳滚动留痕,失败操作红字展示
error原文。
6.5 验证结论
文件引擎、协程运行时、kotlinx-io 文件层、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的文件会话:版本握手、沙箱目录注入、元信息查询与目录列举(见下节效果图),写入/读取/删除/复制/移动全链路可经页面按钮逐一验收。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/FileKit.git
cd FileKit
export JAVA_HOME="/path/to/jdk-21"
./gradlew :filekit-core:compileKotlinOhosArm64 :filekit-core:compileKotlinOhosX64 --max-workers=1
./gradlew :filekit-core:jvmTest --max-workers=1 # 68 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64 --max-workers=1
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.filekit -m entry
hdc shell hilog | grep FileKitNapi
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:filekit-core真正编译成ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- 上游在 build-logic 约定插件里统一 apply
org.jetbrains.kotlin.plugin.serialization,根构建替换时这行最容易丢:丢了编译期不报错,@Serializable类只是没生成serializer(),直到运行期才炸——本仓改为模块级显式声明; - 鸿蒙线 coroutines 把
Dispatchers.IO标为 internal,引擎共享代码 7 处直接使用——用FileKitIoDispatcher接缝替换,JVM actual 逐字还原Dispatchers.IO、ohos actual 映射Default; androidx.annotation没有 ohos 制品:@IntRange从共享 expect 剥离时容易漏掉第二个便利重载上的同名注解,症状是Illegal annotation class 'IntRange'——两处都要删;- expect/actual 面大的库先盘点再动刀:ohos 挂在默认层级模板
nativeMain下,会继承jvmAndNativeMain → nonWebMain → commonMain全链 expect,actual 少补一个编译期就炸;共享 actual(构造器/source/sink/delete 等)在jvmAndNativeMain里现成可用,只有 POSIX 部分需要自己写; - Gradle 9.8.0 发行包经 services.gradle.org 下载会重定向到部分网络不可达的主机——用国内镜像(如腾讯云)直接灌进
~/.gradle/wrapper/dists并把 wrappernetworkTimeout调大即可; llvm-nm不在 Kotlin/Native 预编译目录的bin里,在~/.konan/dependencies/cpf-llvm-19-*/bin下;- 桥模块要自带
kotlinx-coroutines依赖:引擎侧是implementation不透传,runBlocking在桥的编译类路径上必须显式声明(KStore 同款注释); - 链接期
'+fp' is not a recognized feature for this target是 LLVM 对 ohos 目标的良性告警,不影响产物; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); build-profile.json5的signingConfigs仓库内保持空数组,DevEco 勾选自动签名后回填的带material配置与证书绝不进入仓库;HAP 缺当前 ABI 的 so 会报安装失败9568347,abiFilters必须双 ABI、每个 ABI 三件套缺一不可。
8.2 已知问题
filekit-dialogs/filekit-dialogs-compose/filekit-coil不进 ohos 构建:系统选文件 / 存文件在 HarmonyOS 上建议直接用 ArkTSDocumentViewPicker;Compose / Coil 鸿蒙制品线落地后可按本仓模式回接;saveImageToGallery/saveVideoToGallery在 ohos 返回Result.failure(指引 ArkTSphotoAccessHelper),compressImage抛FileKitException(Windows/Linux actual 同款立场);createdAt()返回 null:muslst_ctim是状态变更时间,报告成创建时间是不诚实的(与 Linux actual 一致);mimeType()依赖内置约 50 项扩展名表:OpenHarmony 没有 freedesktop MIME 数据库,表外类型返回 null;filesDir/cacheDir默认假定entry模块沙箱(/data/storage/el2/base/haps/entry),其它模块布局的宿主应调用FileKit.init(filesDir, cacheDir)注入真实上下文目录;- 上游 android / apple / js / wasm 目标不参与本 fork 构建(源文件留在树里),发布链路一并剥离,本 fork 不发 Maven Central;
- 本地 ktlint jar 缺失,格式检查留给 CI。
8.3 未来优化方向
- 把
filekit-dialogs的选文件 / 存文件能力经 ArkTS 混合封装(NAPI 回调或.har)接回统一 API; - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; info补充createdAt(等 statx 或系统能力)与权限位;- 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。
九、总结
9.1 核心难点回顾
上游 filekit-core(commonMain + nonWebMain + jvmAndNativeMain)
→ 验证 ohos kotlinx 制品线(coroutines / serialization / kotlinx-io)
→ 工具链对齐 Kotlin 2.4.20-0.1.0-07(Gradle 9.8.0 沿用)
→ 根构建替换(cmp_qrose 模式,serialization 插件模块级补回)
→ 源集层级重建 + FileKitIoDispatcher 接缝 + @IntRange 剥离
→ ohosArm64 / ohosX64 KLIB + 3 个 ohos actual 文件
→ C ABI(@CName × 2,12 op)+ 扁平 JSON
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON 解析
→ ArkUI 真机单页验收(init / 读写 / 删除 / 信息 / 列表 / 复制 / 移动 / 二进制)
9.2 封装层次
- 引擎层:上游原版
PlatformFile、读写流、copyTo事务复制、atomicMove、符号链接纪律删除、PlatformFileSerializer; - 目录层:上游原版
FileKit.filesDir/cacheDir/databasesDir/projectDir契约,ohos actual 提供沙箱默认与 init 注入; - Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、JSON 解析、操作分发、日志与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 先验证生态制品线再定适配路线:kotlinx ohos 制品补齐后,文件类库的正确答案就是"真编译引擎 + 补 POSIX actual"——路线选择必须基于当天的制品可达性,而不是历史结论;
- 工具链版本跟着依赖走:Kotlin 钉版由 coroutines 制品线的 stdlib 元数据反推(2.4.20-0.1.0-07),Gradle 经验按工具链线区分(2.4.20 线留 9.x,2.2.21 线降 8.14.1);
- expect/actual 面大的库,接缝要切在"共享实现"与"平台行为"之间:本仓只动了调度器与一个注解两处共享代码,其余全部在新增的 ohosMain 里落地——手术面越小,"上游测试零改动全绿"作为交叉验证的效力越强。
9.4 适配成果
当前适配已完成:
- FileKit
0.16.0真实文件引擎的双 ABI Kotlin/Native 编译(上游共享源码零重写,仅 7 处调度器调用点换接缝、2 处注解剥离;新增 3 个 ohos actual 源文件); - HarmonyOS kotlinx 制品线在文件操作库上的完整落地(coroutines / serialization-core
1.11.0-0.1.0-07+ kotlinx-io-core0.9.1-0.1.0-07); - 2 个 C ABI 符号、12 个引擎操作的 JSON 桥接和 C++ N-API 包装层(
llvm-nm验证全局 T 符号); - DevEco Stage ArkUI 单页验收页面(init、写入、读取、二进制、信息、列表、目录、删除、复制、移动、版本握手、操作日志);
- 68 项 JVM 交叉验证全绿(上游原测试文件)+ HAP 构建(双 ABI 三件套 so)、签名、设备安装和真机验证;
- AtomGit 项目文档、OpenHarmony README(中英双份)和本地效果图。
适配过程 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)