系统 API 深度适配指南
在真实的 KMP 项目中,我们需要处理大量与操作系统紧密耦合的任务。本章将提供一套完整的工业级模版,涵盖文件系统、本地存储和线程调度。
文件系统深度适配
在共享层(Common)操作文件,需要针对不同平台的文件系统 API 进行封装。
expect fun readFile(path: String): String
expect fun getAppDataDir(): Stringimport java.io.File
actual fun getAppDataDir(): String = applicationContext.filesDir.absolutePath
actual fun readFile(path: String): String {
return File(path).readText()
}import platform.Foundation.*
actual fun getAppDataDir(): String {
return NSSearchPathForDirectoriesInDomains(
NSApplicationSupportDirectory, NSUserDomainMask, true
).first() as String
}
actual fun readFile(path: String): String {
val fileManager = NSFileManager.defaultManager
val data = fileManager.contentsAtPath(path) ?: return ""
return NSString.create(data = data, encoding = NSUTF8StringEncoding) as String
}import platform.posix.*
import kotlinx.cinterop.*
actual fun readFile(path: String): String {
val file = fopen(path, "r") ?: return ""
// 使用 C 标准库进行流式读取...
return "Content from Win32"
}文件系统统一抽象
API 核心签名说明
val FileSystem.SYSTEM: FileSystemclass Pathfun String.toPath(): Pathfun FileSystem.read(path: Path, reader: BufferedSource.() -> T): Tfun FileSystem.write(path: Path, writer: BufferedSink.() -> Unit)
标准代码块
import okio.FileSystem
import okio.Path.Companion.toPath
fun loadProfile(path: String): String {
val file = path.toPath()
return FileSystem.SYSTEM.read(file) { readUtf8() }
}
fun saveProfile(path: String, content: String) {
val file = path.toPath()
FileSystem.SYSTEM.write(file) { writeUtf8(content) }
}底层机制说明
FileSystem 在 commonMain 中暴露统一 API,调用端不依赖平台类型。 FileSystem.SYSTEM 是平台默认实现的入口,绑定发生在编译期。 JVM 目标使用基于 Java IO/NIO 的实现执行真实文件操作。 Native 目标通过 POSIX 或 Win32 系统调用完成读写与元数据访问。 这些实现由 Okio 的 expect/actual 层完成编译期选择与链接。 Path 是不可变路径对象,负责统一分隔符与规范化规则。 toPath() 只做语义转换,不会触碰真实文件系统。 read 会创建 Source 并包装为 BufferedSource,提供流式读取能力。 write 会创建 Sink 并包装为 BufferedSink,减少系统调用次数。 Buffer 由固定大小的 segment 组成,避免频繁分配大数组。 segment 使用池化复用以降低 GC 压力并提升吞吐。 读取过程按需拉取字节,避免一次性加载大文件内容。 写入过程在 flush 时集中落盘,降低系统调用频率与阻塞时间。 FileSystem.read/write 内部用 try-finally 保证资源关闭。 API 为纯 Kotlin 设计,共享层无需区分平台细节。 因此文件逻辑可以直接复用,并在每个平台获得一致语义。
依赖补充
kotlin {
sourceSets {
commonMain.dependencies {
implementation("com.squareup.okio:okio:3.9.0")
}
}
}kotlin {
sourceSets {
commonMain {
dependencies {
implementation "com.squareup.okio:okio:3.9.0"
}
}
}
}[versions]
okio = "3.9.0"
[libraries]
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }最新稳定版本查看链接:https://mvnrepository.com/artifact/com.squareup.okio/okio
本地持久化 (Key-Value)
对于简单的配置存储,各平台都有成熟的方案。
| 平台 | 底层 API | 推荐 KMP 库 |
|---|---|---|
| Android | SharedPreferences | [MultiplatformSettings] |
| iOS / Mac | NSUserDefaults | [MultiplatformSettings] |
| Windows | 注册表 (Registry) / JSON 文件 | 自定义实现 |
| Linux | 配置文件 (.ini/.json) | 自定义实现 |
工业级封装思路
不要在代码中到处写 expect/actual。建议定义一个 Settings 接口,在各平台初始化时注入。
线程调度与并发
KMP 的并发是初学者的噩梦。理解各平台的调度器是关键:
- Android: 基于
Handler/Looper的主线程。 - iOS / Mac: 基于
GCD (Grand Central Dispatch)的Main Queue。 - 桌面端: 通常由 UI 框架(如 Compose 或 Swing)管理事件分发线程。
跨平台 Dispatchers 获取
// commonMain
expect val Dispatchers.Main: CoroutineDispatcher
// iosMain
import platform . darwin . dispatch_get_main_queue
// ... 映射 GCD 到 Kotlin 协程调度器平台特有功能:UUID 与 硬件 ID
在进行设备注册或日志追踪时,硬件 ID 的获取是必不可少的:
- iOS: 受到严格限制,通常使用
identifierForVendor。 - Android: 推荐使用
Settings.Secure.ANDROID_ID。 - Windows: 可以通过 Win32 API 查询
SMBIOS序列号。
总结:架构设计的“下沉”原则
在处理这些差异时,请遵循以下原则:
- 最小化
expect范围:尽量只在最底层的 IO 处使用expect。 - 优先使用社区库:对于文件系统,优先考虑 [Okio] 的 KMP 版本;对于持久化,优先考虑 [Multiplatform Settings]。
- 类型安全包装:即使是调用原生的
void *,也要在 Kotlin 侧包装成强类型的封装类。