本文记录 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 / ohosX64 Gradle 变体),因此不需要提取内核——真实的引擎(读写流、事务复制、原子移动、安全删除、序列化器)被原样编译成 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 / bytesread 返回文件文本;readBytes 返回 Base64;写操作返回字节数。
entrieslist 返回 [{name, path, isDirectory, size}] 数组。
exists / isDirectory / mimeType / lastModifiedMsinfo / 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 上是 ArkUI DocumentViewPicker 的领地。适配面天然收敛在引擎层;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 MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
JDK21(org.gradle.java.home 指向 Temurin 21.0.12)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle9.8.0(沿用上游 wrapper;2.4.20 线支持 Gradle 9.x)
kotlinxcoroutines 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 product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.filekit
$env:JAVA_HOME="C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
源码集文件数依赖形态处置
commonMain9纯 Kotlin expect + MimeType + 异常整体编译(引擎门面)
nonWebMain8kotlinx-io + coroutines + serialization + androidx.annotation 注解整体编译(7 处调度器接缝 + 2 处注解剥离)
jvmAndNativeMain1kotlinx-io SystemFileSystem整体编译(2 处调度器接缝)
desktopMain2用户目录 expect仅 jvm 链路编译
ohosMain3(新增)kotlinx-io + platform.posix本仓新增,镜像 linux actual
jvmMain / jvmTest / nonWebTest / nativeTest7 / 4 / 8 / 1java.io、JNA、kotlin-testjvm 目标编译,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 方法覆盖内核
FileKitDispatchfilekitDispatch(req)init + 11 个文件操作
FileKitVersionfilekitVersion()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.tomlKotlin 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.ktohos actual 主体:OhosPath、stat/lstat/getcwd、内建 MIME 表、书签快照
filekit-core/src/ohosMain/.../FileKit.ohos.ktFileKit object actual:init 三连、沙箱默认目录、相册/压缩立场
example/nativeApp/build.gradle.kts双 ABI sharedLib、copyKnSo* 回填
nativeApp/.../Bridge.kt2 个 C ABI、12 op、runBlocking 包裹、MiniJson、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面
README.OpenHarmony_CN.md鸿蒙适配主文档(16 节)

4.3 关键 API 对照

层次API作用
引擎PlatformFile(path) / PlatformFile(base, child)按路径构造(ohos 包装 OhosPath(Path))
引擎readBytes / readString / write / writeString读写四件套,suspend,调度器接缝派发
引擎copyToprepareDestinationForWrite 目的地解析 + 缓冲流复制
引擎atomicMove原子移动,目的地为目录时落到 目录/源文件名
引擎delete(mustExist, recursively)lstat 识别符号链接只解链接,递归清空后删除
引擎list / createDirectories / exists / isDirectory目录与存在性
引擎FileKit.filesDir / cacheDir / databasesDir / projectDir标准目录(ohos 沙箱默认 + init 注入)
引擎PlatformFileSerializer文件 ↔ 路径字符串序列化
引擎bookmarkData / fromBookmarkData书签(ohos 为绝对路径字节快照)
NativeFileKitDispatch / FileKitVersionC ABI JSON 导出
N-APIfilekitDispatch(req) / filekitVersion()向 ArkTS 暴露能力
ArkTSdispatch(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 项用例全部通过(上游原测试文件,零改动):

测试类用例数钉住什么
PlatformFileNonWebTest12nonWeb 引擎门面:读写、目录、父级、绝对路径
PlatformFileDeletionTest(JVM)25删除纪律:mustExist、递归、符号链接不跟随、目录残留失败
ResizeUtilsTest17图像缩放计算
FileKitUserDirectoriesJvmTest6JVM 标准目录解析
PlatformFileSerializationTest3PlatformFileSerializer 往返
PlatformFileJvmTest3JVM 平台行为
MacOsBookmarkEnvelopeTest2书信访封编解码

依赖换线没有 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 功能用例

  1. 启动后握手行出现 FileKit v0.16.0-ohos.1 + init ✓(来自真实 Native 调用,非静态文字);
  2. init 自动注入真机沙箱目录,路径框默认 /data/storage/el2/base/haps/entry/files/filekit_demo.txt;
  3. 默认 JSON {"hello":"FileKit","platform":"HarmonyOS","count":42} 点写入,返回 ok:true 与字节数;
  4. 点读取,value 原样返回写入时的对象回填文本框,字段无转义损伤;
  5. 杀掉应用重启后再点读取,值仍在——落盘持久化成立,不是内存缓存;
  6. 点删除返回 ok:true,重启后再读报文件不存在;
  7. 信息卡展示 name / absolutePath / size / isRegularFile / mimeType(按扩展名给 text/plain)/ lastModifiedMs;
  8. 列出目录把父目录条目渲染成名称 + [DIR]/大小 行;
  9. 复制 / 移动分别走 copyTo / atomicMove,目的地为目录时自动落到 目录/源文件名;
  10. 写入二进制 / 读取二进制走 Base64 通道,返回字节数;
  11. 每步操作的操作日志按时间戳滚动留痕,失败操作红字展示 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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :filekit-core 真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. 上游在 build-logic 约定插件里统一 apply org.jetbrains.kotlin.plugin.serialization,根构建替换时这行最容易丢:丢了编译期不报错,@Serializable 类只是没生成 serializer(),直到运行期才炸——本仓改为模块级显式声明;
  4. 鸿蒙线 coroutines 把 Dispatchers.IO 标为 internal,引擎共享代码 7 处直接使用——用 FileKitIoDispatcher 接缝替换,JVM actual 逐字还原 Dispatchers.IO、ohos actual 映射 Default;
  5. androidx.annotation 没有 ohos 制品:@IntRange 从共享 expect 剥离时容易漏掉第二个便利重载上的同名注解,症状是 Illegal annotation class 'IntRange'——两处都要删;
  6. expect/actual 面大的库先盘点再动刀:ohos 挂在默认层级模板 nativeMain 下,会继承 jvmAndNativeMain → nonWebMain → commonMain 全链 expect,actual 少补一个编译期就炸;共享 actual(构造器/source/sink/delete 等)在 jvmAndNativeMain 里现成可用,只有 POSIX 部分需要自己写;
  7. Gradle 9.8.0 发行包经 services.gradle.org 下载会重定向到部分网络不可达的主机——用国内镜像(如腾讯云)直接灌进 ~/.gradle/wrapper/dists 并把 wrapper networkTimeout 调大即可;
  8. llvm-nm 不在 Kotlin/Native 预编译目录的 bin 里,在 ~/.konan/dependencies/cpf-llvm-19-*/bin 下;
  9. 桥模块要自带 kotlinx-coroutines 依赖:引擎侧是 implementation 不透传,runBlocking 在桥的编译类路径上必须显式声明(KStore 同款注释);
  10. 链接期 '+fp' is not a recognized feature for this target 是 LLVM 对 ohos 目标的良性告警,不影响产物;
  11. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  12. build-profile.json5 的 signingConfigs 仓库内保持空数组,DevEco 勾选自动签名后回填的带 material 配置与证书绝不进入仓库;HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI、每个 ABI 三件套缺一不可。

8.2 已知问题

  • filekit-dialogs / filekit-dialogs-compose / filekit-coil 不进 ohos 构建:系统选文件 / 存文件在 HarmonyOS 上建议直接用 ArkTS DocumentViewPicker;Compose / Coil 鸿蒙制品线落地后可按本仓模式回接;
  • saveImageToGallery / saveVideoToGallery 在 ohos 返回 Result.failure(指引 ArkTS photoAccessHelper),compressImage 抛 FileKitException(Windows/Linux actual 同款立场);
  • createdAt() 返回 null:musl st_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 三条经验

  1. 先验证生态制品线再定适配路线:kotlinx ohos 制品补齐后,文件类库的正确答案就是"真编译引擎 + 补 POSIX actual"——路线选择必须基于当天的制品可达性,而不是历史结论;
  2. 工具链版本跟着依赖走:Kotlin 钉版由 coroutines 制品线的 stdlib 元数据反推(2.4.20-0.1.0-07),Gradle 经验按工具链线区分(2.4.20 线留 9.x,2.2.21 线降 8.14.1);
  3. 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-core 0.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。

参考文档

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐