开源鸿蒙平台 KMP 三方库 kotlinx-io 适配全流程:从 Path 抽象到真机沙箱读写验证

欢迎加入 KMP/CMP 鸿蒙化社区:https://atomgit.com/CPF-KMP-CMP
适配后仓库地址(AtomGit):https://atomgit.com/oh-tpc/ohos_kotlinx-io
一、接着上一篇往下走
上一篇把 kotlinx-datetime 搬上 OpenHarmony,留下的结论是:KMP 库适配的难点几乎从不在 Kotlin 代码本身,而在库对平台能力做了哪些隐含假设。kotlinx-datetime 假设"系统会提供时区数据库",而鸿蒙沙箱里没有这个文件,于是 TimeZone.currentSystemDefault() 静默回退到了 UTC。
kotlinx-io 是同一类问题的另一个样本,而且更典型。它是 JetBrains 官方的多平台 IO 基础库,提供 Buffer、Source、Sink、Path、FileSystem 这一整套跨平台 IO 原语。它比 kotlinx-datetime 更底层——kotlinx-io-okio 是它与 Okio 之间的桥接模块,说明它已经处在 KMP 生态的地基位置。
它假设的是什么?假设"路径可以直接拿去 open()"。在桌面和服务器上这个假设成立,在 OpenHarmony 上不成立:应用跑在沙箱里,能碰的只有自己的数据目录。这个假设一旦失效,表现不是编译错误,而是运行时的权限拒绝——比时区错误更隐蔽,也更值得完整记录一遍。
二、先看清三层抽象
动手改之前必须理清 kotlinx-io 的分层。它的文件能力由三层组成,而这三层在适配时的处理方式完全不同。
第一层是 Path。 它是 expect class,职责比名字听起来轻得多——在 native 平台上,Path 内部只持有一个字符串,parent、name、isAbsolute 这些成员是通过内部的 dirnameImpl、basenameImpl、isAbsoluteImpl 等函数算出来的,用的是 Unix 风格分隔符。Path 本身不碰文件系统,它只做路径字符串的语义解析。这一点直接决定了 Path 层的适配成本极低。
第二层是 FileSystem / SystemFileSystem。 这才是真正干活的地方。FileSystem 是对外接口,提供 source、sink、exists、delete、createDirectories、atomicMove、metadataOrNull、list 等操作;SystemFileSystem 是默认实现。在 native 平台上,它的实现直接建立在 POSIX 接口之上——access、remove、mkdir、realpath、fopen/fread/fwrite、opendir/readdir。这里是适配的主战场,也是沙箱问题爆发的地方。
第三层是 Source / Sink 与 RawSource / RawSink。 RawSource / RawSink 是底层字节供应与接收接口,Source / Sink 是带缓冲的高层读写接口,buffered() 负责把前者包装成后者。这一层是纯逻辑,适配时基本不用碰。
分层清楚之后,工作量就能预估了:Path 层几乎不动,Source/Sink 层完全不动,FileSystem 层需要确认 POSIX 可用性——而真正要解决的业务问题在沙箱。
三、第一步:target 与 source set
和上一篇一样,前提是接入 HarmonyOS Kotlin 定制版——ohosArm64() 这个 target 不在 Kotlin 官方主线里,用官方插件会直接报 Unresolved reference。版本切换与插件仓库配置的细节见上一篇,
开发使用Dev Eco,主界面代码如下:

这里只列 target 与 source set 的改动:
// kotlinx-io-core/build.gradle.kts
kotlin {
jvm()
js(IR) { nodejs() }
linuxX64()
macosArm64()
wasmWasi { nodejs() }
// 本次新增:OpenHarmony
ohosArm64()
sourceSets {
val commonMain by getting
val nativeMain by getting
val ohosArm64Main by creating {
dependsOn(nativeMain)
}
val ohosArm64Test by creating {
dependsOn(commonTest.get())
}
}
}
dependsOn(nativeMain) 这一步在 kotlinx-io 上比在 kotlinx-datetime 上更划算。因为 native 的 Path 与 SystemFileSystem 全部基于 POSIX 实现,而 OpenHarmony 内核本身就是 POSIX 兼容的,所以绝大部分实现可以直接继承,只在真正有差异的地方做覆盖。这也是为什么后面会发现:真正要改的代码少得出乎意料。
四、第二步:Path 层几乎不用改
补完 target 后先编译一次,让编译器报出缺失的 actual:
./gradlew :kotlinx-io-core:compileKotlinOhosArm64
kotlinx-io 的 native Path 依赖的是一组内部函数(取目录名、取文件名、判断是否绝对路径),这些在 nativeMain 里已经有 Unix 风格实现。继承之后,编译器报出的缺口通常很少。需要额外确认的只有路径分隔符——native 用 Unix 分隔符,OpenHarmony 一致,这里不需要特殊处理。
这一步的实际工作量比预想的小,原因第二节已经说明:Path 只做字符串语义解析,不碰文件系统。很多人在适配 KMP 库时会把注意力放在这类"看起来最像适配工作"的抽象类上,结果在错误的地方耗掉大量时间。
五、第三步:真正的坑在沙箱
target 接上、编译通过之后,先写一个最小验证跑一跑:
val fs = SystemFileSystem
val dir = Path("/data/local/tmp/kio_demo")
fs.createDirectories(dir)
val file = Path(dir, "hello.txt")
val sink = fs.sink(file).buffered()
sink.buffer.writeString("Hello OpenHarmony")
sink.close()
println(fs.exists(file))
结果不是编译失败,而是运行时抛异常——权限被拒绝。
原因不是代码写错了,而是 OpenHarmony 的应用沙箱限制:应用进程只能访问自己的数据目录,任何硬编码的绝对路径(/data/local/tmp、/tmp、/sdcard 都一样)都会被系统挡回来。这和第一篇里时区问题的结构完全同构:库假设"路径可以直接使用",平台却限制了可用范围。
解法也沿用同一个思路——把平台提供的基础路径从应用层传入 KMP 层,而不是让 KMP 层去猜。OpenHarmony 给每个应用分配了沙箱目录,应用层通过 Ability 的 Context 就能拿到自己的文件目录(context.filesDir),把它作为根路径传进来即可:
// ohosArm64Main
@CName("kio_write_demo_file")
fun writeDemoFile(sandbox: String, name: String, content: String): String {
val fs = SystemFileSystem
// 沙箱根由应用层传入,绝不硬编码绝对路径
val dir = Path(sandbox, "kio_demo")
fs.createDirectories(dir)
val file = Path(dir, name)
val sink = fs.sink(file).buffered()
sink.buffer.writeString(content)
sink.close()
val size = fs.metadataOrNull(file)?.size ?: 0L
return "写入成功: $file (${size} bytes)"
}
这里有两个细节值得单独说。
第一,用的是 SystemFileSystem.sink(path).buffered(),而不是 Path.sink()。后者从 0.8.0 起已经被标记为 ERROR 级废弃,新代码不应再使用——如果你的项目里还有这种写法,编译阶段就会直接报错。
第二,Path 的构造是 Path(base, vararg parts),会用系统路径分隔符自动拼接。所以 Path(sandbox, "kio_demo") 拼出来的就是沙箱内的合法路径,不需要手写斜杠,也避免了拼接错误。
改造之后,路径始终落在应用自己的沙箱内,权限问题消失,读写恢复正常。
六、第四步:导出、打包与真机验证
我启用最新版的模拟器新适配验证:
选择虚拟设备,下载安装最新版的HarmonyOS7.0.0的SDK。


创建对应机型:

导出与打包的链路和上一篇完全一致:Kotlin 侧用 @CName 指定导出符号,C++ 侧用 NAPI 注册模块,产物 .so 放进 HAR 的 libs/arm64-v8a/。细节不再重复,这里只补一个验证顺序——先确认符号,再排查调用:
llvm-nm -D libkmpio.so | findstr kio_write
看到 T kio_write_demo_file,说明 Kotlin/Native 侧的导出是成功的。如果符号在、但 ArkTS 侧 import 不到,问题一定在 NAPI 注册层,按"符号名 → 模块名(nm_modname)→ Index.d.ts 声明 → oh-package.json5 的 main 入口"这个顺序逐一核对即可。
ArkTS 侧把沙箱路径取出来传进去:
import { writeDemoFile } from 'ohos_kmpio';
import { common } from '@kit.AbilityKit';
@Entry
@Component
struct Index {
@State result: string = '--';
aboutToAppear() {
const ctx = getContext(this) as common.UIAbilityContext;
// 沙箱目录由应用层提供,KMP 层不自行猜路径
const sandbox: string = ctx.filesDir;
this.result = writeDemoFile(sandbox, 'hello.txt', 'Hello OpenHarmony');
}
build() {
Column({ space: 12 }) {
Text('kotlinx-io on OpenHarmony')
.fontSize(18).fontWeight(FontWeight.Bold)
Text(this.result)
.fontSize(15).fontColor('#0A59F7')
}
.width('100%').height('100%')
.justifyContent(FlexAlign.Center)
}
}
真机运行后,页面显示写入成功及文件字节数;再进 hdc shell 到该沙箱目录下,能看到实际生成的文件。
运行效果如下:


为了确认不是"碰巧能跑",我做了两组对照:把内容换成中文再写一次,读回来的字节数随内容长度正确变化;把文件名换成一个已存在的名字重复写入,文件被正确覆盖而不是追加。两组结果都符合预期。
七、踩坑清单
坑一:ohosArm64() 报未定义。 和上一篇同一个原因,还在用 Kotlin 官方主线插件。这个 target 只在 HarmonyOS Kotlin 定制版里存在。
坑二:运行时权限拒绝。 最容易误判成代码问题的一类。根因是硬编码了沙箱外的绝对路径。不要试图通过申请更宽的权限去绕过它,正确做法是从应用层把 context.filesDir 传进来。
坑三:Path.sink() / Path.source() 编译报错。 这两个扩展从 0.8.0 起是 ERROR 级废弃,换成 SystemFileSystem.sink(path).buffered() 与 SystemFileSystem.source(path).buffered()。
坑四:把调试时看到的绝对路径硬编码进代码。 沙箱路径在不同设备、不同调试与正式环境下并不一致,必须始终从 Context 动态获取。
坑五:改完代码产物没更新。 与上一篇相同,Kotlin/Native 编译缓存比较激进,遇到产物与代码不一致时先 ./gradlew clean 再构建。
八、小结
kotlinx-io 的适配比 kotlinx-datetime 更能说明一件事:KMP 库适配的工作量,几乎与代码体量无关,而与"库对平台做了多少隐含假设"强相关。 kotlinx-io 的源码规模远大于 kotlinx-datetime,但真正需要改的地方反而更集中——Path 层几乎不动,Source/Sink 层完全不动,全部注意力都落在 FileSystem 层的沙箱边界上。
两篇下来,这套方法论已经可以复用了:先看清分层 → 用编译器暴露缺口 → 找出库对平台的隐含假设 → 把平台能力从应用层注入 KMP 层。 下一篇我打算按这个路子处理 okio,它的 native 实现更厚,正好可以检验这套方法在更复杂的库上是否同样成立。
欢迎加入 KMP/CMP 鸿蒙化社区,一起共建 OpenHarmony 跨平台生态:
https://atomgit.com/CPF-KMP-CMP
适配后仓库地址(AtomGit):
https://atomgit.com/oh-tpc/ohos_kotlinx-io
推荐使用码道进行 KMP/CMP 工程的代码补全与适配辅助,专属邀请入口:
https://developer.huawei.com/codeartsco.html?source=dmzntgwatomgit1&sourcead=dmzntgwatomgiths
环境信息:DevEco Studio 26.0.0 Release / HarmonyOS Kotlin 2.2.21-1.0.0 / Gradle 8.14.1 / JDK 21 / 真机 ROM 6.1+ / kotlinx-io-core 0.9.1
更多推荐




所有评论(0)