Skip to content

系统 API 深度适配指南

源:Kotlin Multiplatform 官方文档

在真实的 KMP 项目中,我们需要处理大量与操作系统紧密耦合的任务。本章将提供一套完整的工业级模版,涵盖文件系统、本地存储和线程调度。

文件系统深度适配

在共享层(Common)操作文件,需要针对不同平台的文件系统 API 进行封装。

kotlin
expect fun readFile(path: String): String
expect fun getAppDataDir(): String
kotlin
import java.io.File

actual fun getAppDataDir(): String = applicationContext.filesDir.absolutePath

actual fun readFile(path: String): String {
    return File(path).readText()
}
kotlin
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
}
kotlin
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: FileSystem
  • class Path
  • fun String.toPath(): Path
  • fun FileSystem.read(path: Path, reader: BufferedSource.() -> T): T
  • fun FileSystem.write(path: Path, writer: BufferedSink.() -> Unit)

标准代码块

kotlin
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
kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation("com.squareup.okio:okio:3.9.0")
        }
    }
}
groovy
kotlin {
    sourceSets {
        commonMain {
            dependencies {
                implementation "com.squareup.okio:okio:3.9.0"
            }
        }
    }
}
toml
[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 库
AndroidSharedPreferences[MultiplatformSettings]
iOS / MacNSUserDefaults[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 获取

kotlin
// 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 序列号。

总结:架构设计的“下沉”原则

在处理这些差异时,请遵循以下原则:

  1. 最小化 expect 范围:尽量只在最底层的 IO 处使用 expect
  2. 优先使用社区库:对于文件系统,优先考虑 [Okio] 的 KMP 版本;对于持久化,优先考虑 [Multiplatform Settings]。
  3. 类型安全包装:即使是调用原生的 void *,也要在 Kotlin 侧包装成强类型的封装类。