Skip to content

Kotlin 官方跨平台 IO (kotlinx-io) Kotlin 2.1.0+

源:kotlinx-io 官方文档

kotlinx-io 是 JetBrains 为 Kotlin 全平台生态量身打造的现代化 I/O 标准库。它不仅是 Okio 的官方演进版本,更是 Kotlin 2.1.0 后处理流式数据、二进制协议及多平台文件系统的核心基石。该库通过彻底摆脱 JVM java.io 的阻塞式流模型,引入了基于 Segment 链表 的零拷贝(Zero-copy)架构,为 KMP 项目提供了高性能且 API 统一的 I/O 解决方案。

依赖配置与版本

查看 Maven Central 最新版本

kotlin
dependencies {
    // 核心库:提供 Source, Sink, Buffer 等基础抽象
    implementation("org.jetbrains.kotlinx:kotlinx-io-core:0.6.0")
    
    // 字节串支持:提供 ByteString 及相关操作 (可选)
    implementation("org.jetbrains.kotlinx:kotlinx-io-bytestring:0.6.0")
}
groovy
dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-io-core:0.6.0'
    implementation 'org.jetbrains.kotlinx:kotlinx-io-bytestring:0.6.0'
}
toml
[versions]
kotlinx-io = "0.6.0"

[libraries]
kotlinx-io-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-io-core", version.ref = "kotlinx-io" }
kotlinx-io-bytestring = { group = "org.jetbrains.kotlinx", name = "kotlinx-io-bytestring", version.ref = "kotlinx-io" }

架构核心:Source、Sink 与 Buffer

kotlinx-io 抛弃了 Java IO 复杂的包装器模式,将其抽象为三个核心原语。

核心 API 签名

kotlin
public interface Source : Closeable {
    // 基础读取:将数据读取到 Buffer 中
    public fun read(sink: Buffer, byteCount: Long): Long
    
    // 扩展 API:读取各种基本类型
    public fun readByte(): Byte
    public fun readInt(): Int
    public fun readUtf8Line(): String?
    // ...
}
kotlin
public interface Sink : Closeable {
    // 基础写入:从 Buffer 中写入数据
    public fun write(source: Buffer, byteCount: Long)
    
    // 强制刷新缓冲区
    public fun flush()
    
    // 扩展 API:写入基本类型
    public fun writeByte(byte: Byte)
    public fun writeInt(int: Int)
    public fun writeUtf8(string: String)
}
kotlin
public class Buffer : Source, Sink {
    public var size: Long = 0L
    // 允许直接访问底层字节数据而无需拷贝
    public fun copy(): Buffer
    public fun clear()
}

底层原理:Segment 链表与零拷贝机制

kotlinx-io 高性能的核心在于其内存管理模型,它并没有使用单一的 ByteArray,而是采用了一个 双向循环链表 来管理一系列 Segment 对象。

Segment 的内存结构

每个 Segment 是一个固定大小(通常为 8192 字节)的缓冲区块,包含以下元数据:

  • data: 实际存储字节的数组。
  • pos: 当前读取位置的索引。
  • limit: 当前有效数据结束位置的索引。
  • shared: 标记该数据是否与其他 Segment 共享(用于零拷贝)。
深度剖析:零拷贝 (Zero-copy) 是如何实现的?

当你在两个 Buffer 之间移动大量数据(例如从 Buffer A 传输到 Buffer B)时,kotlinx-io 不会执行 System.arraycopy

  1. 所有权转移:如果传输的数据恰好覆盖了整个 Segment,库会直接将该 Segment 对象从 Buffer A 的链表中剔除,并插入到 Buffer B 的链表末尾。
  2. 字节码视角:这种操作仅仅是引用的重新赋值,时间复杂度为 $O(1)$,完全避免了 OOM 和 CPU 密集型的拷贝操作。
  3. 分片共享:对于非整块的数据,Segment 支持通过共享 data 数组并调整 pos / limit 来实现逻辑上的分片。

跨平台文件系统 (FileSystem) 实战

kotlinx-io 通过 SystemFileSystem 屏蔽了不同操作系统的文件操作差异,在 KMP 项目的 commonMain 中即可编写完整的文件逻辑。

多场景代码示例

kotlin
val path = Path("config.json")

// 写入逻辑
SystemFileSystem.sink(path).buffered().use { it.writeUtf8("""{"version": 1}""") }

// 读取逻辑
val content = SystemFileSystem.source(path).buffered().use { it.readUtf8() }
kotlin
val sourceDir = Path("data/logs")
val archiveFile = Path("archive/old_logs.log")

// 1. 创建多级目录 (类似 mkdir -p)
SystemFileSystem.createDirectories(sourceDir)

// 2. 原子性重命名 (底层封装了 renameat 或对应系统调用)
if (SystemFileSystem.exists(sourceDir)) {
    SystemFileSystem.atomicMove(sourceDir, archiveFile)
}

// 3. 递归遍历
SystemFileSystem.list(Path(".")).forEach { p ->
    println("File: ${p.name}, Size: ${SystemFileSystem.metadataOrNull(p)?.size}")
}
kotlin
import kotlinx.io.bytestring.*

// 类似 Okio 的 ByteString,用于处理不可变字节序列
val rawData = "DEADBEEF".decodeHex()
val base64 = rawData.toBase64String()

// 直接写入 Sink
sink.write(rawData)

工程实践:与 java.io 及 Okio 的选型对比

在不同的项目阶段,选择合适的 I/O 库至关重要。

kotlin
// 缺点:层层包装,样板代码多,异常处理不便
val file = File("data.bin")
val out = BufferedOutputStream(FileOutputStream(file))
out.write(42)
out.close()
kotlin
// 现状:功能最成熟,生态位极其稳固 (Retrofit/Coil)
// 建议:纯 Android 项目且已引入 Okio 时,无需强行迁移到 kotlinx-io
import okio.Path.Companion.toPath
val sink = FileSystem.SYSTEM.sink("test.txt".toPath())
kotlin
// 优点:KMP 原生支持,Kotlin 2.1+ 标准化
// 场景:新 KMP 项目、开发通用 Kotlin 库、WebAssembly 支持
import kotlinx.io.files.Path
val sink = SystemFileSystem.sink(Path("test.txt"))

核心准则与性能优化

生命周期管理:use 与 Flush

由于 Sink 是带缓冲的,必须调用 use 扩展函数或显式调用 close()。未关闭的 Sink 可能会导致数据残留在缓冲区内未被写入磁盘。

线程模型与协程

尽管 kotlinx-io 目前主要提供阻塞式 API,但在 Android/JVM 开发中,应始终配合协程调度器:

kotlin
suspend fun saveLargeFile(data: Buffer, path: Path) = withContext(Dispatchers.IO) {
    SystemFileSystem.sink(path).buffered().use { sink ->
        sink.write(data, data.size)
    }
}

R8/Proguard 配置

由于该库使用了内部的 Segment 池,混淆时需保留相关内部结构:

proguard
-keep class kotlinx.io.** { *; }

避免重复包装

buffered() 函数会创建一个新的缓冲区。如果一个 Source 已经是 BufferedSource 类型,不要重复调用 buffered(),否则会产生不必要的层级嵌套和性能损耗。