Skip to content

IO 与文件操作

在 Kotlin 中,IO 操作分为两个流派:一是基于 Java 标准库 (java.io) 的 Kotlin 扩展函数,适用于纯 JVM/Android 项目;二是基于 Okio 的现代化 IO 库,它是 KMP 和高性能 Android 开发的首选。

Kotlin 标准库 IO 扩展

Kotlin 为 java.io.FileInputStreamOutputStream 提供了大量实用的扩展函数,极大地简化了样板代码。

资源自动关闭:use

usetry-with-resources 的 Kotlin 版本。它适用于任何实现了 AutoCloseable 的对象,确保块执行完毕或发生异常时自动关闭资源。

kotlin
FileInputStream("data.txt").use { input ->
    // 读取逻辑
    val data = input.readBytes()
} // input 在此自动关闭
kotlin
FileInputStream input = null;
try {
    input = new FileInputStream("data.txt");
    // ...
} finally {
    if (input != null) input.close();
}

文件读写简化

Kotlin 允许你像操作字符串一样操作文件内容。

性能警示

readTextreadBytes 会将文件内容一次性加载到内存。对于超过几 MB 的大文件,严禁使用这些方法,应使用流式处理(forEachLineinputStream)。

kotlin
val file = File("config.json")

// 1. 写入文本 (覆盖)
file.writeText("{ \"id\": 1 }")

// 2. 追加文本
file.appendText("\n// End")

// 3. 逐行读取 (内存安全)
file.forEachLine { line ->
    if (line.startsWith("ERROR")) println(line)
}

目录遍历:FileTreeWalk

walk() 提供了一种惰性遍历文件树的方法。

kotlin
File("src").walk()
    .filter { it.isFile && it.extension == "kt" }
    .forEach { println("Found Kotlin file: ${it.name}") }

流拷贝:copyTo

将输入流的数据传输到输出流,支持设置缓冲区大小。

kotlin
input.use { ins ->
    output.use { outs ->
        // 自动缓冲拷贝
        ins.copyTo(outs) 
    }
}

现代化路径操作:Path (NIO.2) Kotlin 1.5+

虽然 java.io.File 依然常用,但 Java 7 引入的 java.nio.file.Path 提供了更严谨的文件系统模型(支持符号链接、文件属性、原子操作)。Kotlin 标准库为 Path 提供了与 File 几乎一致的扩展 API,强烈建议在新代码中优先使用 Path

路径拼接与创建

Kotlin 重载了 / 操作符,使得路径拼接非常直观。

kotlin
import kotlin.io.path.*
import java.nio.file.Path

// 获取 Path 对象
val baseDir: Path = Path("src/main")

// 使用 / 拼接路径 (无需处理分隔符问题)
val configFile = baseDir / "resources" / "config.json"

// 创建目录 (如果不存才创建)
if (baseDir.notExists()) {
    baseDir.createDirectories()
}

文件读写与操作

绝大多数 File 的扩展函数都有对应的 Path 版本。

kotlin
// 写入
configFile.writeText("""{"version": 1}""")

// 读取
val content = configFile.readText()

// 移动 (支持原子操作)
val backup = baseDir / "backup.json"
configFile.moveTo(backup, overwrite = true)

// 属性读取
println("Last modified: ${configFile.getLastModifiedTime()}")
println("Size: ${configFile.fileSize()}")

File vs Path

  • File: 也就是 java.io.File,API 较老,部分操作在出错时仅返回 false 而不抛出异常。
  • Path: java.nio.file.Path,API 更现代,异常处理更明确,支持原子性操作(Atomic Move)。

现代 IO 标准:Okio

源:Square Okio

在 Android 和 KMP 开发中,java.io API 显得陈旧且难以测试。Okio 是一个基于 Segment 机制的高性能 IO 库,它也是 Retrofit 和 Coil 的底层基石。

依赖配置

kotlin
implementation("com.squareup.okio:okio:3.9.0")

核心概念:Source 与 Sink

  • Source:等同于 InputStream (读取)。
  • Sink:等同于 OutputStream (写入)。
  • Buffer:Okio 的核心,一个智能的、可自动扩容的字节容器。

读写实战

Okio 的强大之处在于它极易使用的 API 和对二进制数据的原生支持。

kotlin
fun writeData(file: File) {
    // 1. 获取 Sink 并包装为 BufferedSink
    file.sink().buffer().use { sink ->
        sink.writeUtf8("Hello Okio")
        sink.writeInt(42)
        sink.writeHex("DEADBEEF")
    } // 自动 flush 并 close
}

fun readData(file: File) {
    // 2. 获取 Source 并包装为 BufferedSource
    file.source().buffer().use { source ->
        val text = source.readUtf8Line()
        val num = source.readInt()
        val hex = source.readByteString(4) // 此时 hex.hex() == "deadbeef"
    }
}

为什么选择 Okio?

  1. 内存优化:Okio 使用 Segment 池技术复用内存,大幅减少 IO 过程中的 GC 压力。
  2. KMP 支持:Okio 提供了 FileSystem API,使得文件操作代码可以在 Android、iOS 和 JVM 间完全共享。
  3. ByteString:提供了不可变的字节串处理能力,非常适合处理网络协议、哈希计算和 Base64 编解码。
kotlin
// 计算 MD5 和 Base64
val md5 = ByteString.encodeUtf8("hello").md5().hex()
val base64 = ByteString.encodeUtf8("hello").base64()

核心工程准则

  1. 优先使用 use:永远不要手动调用 close(),除非你在编写极其底层的库。
  2. 大文件流式处理:避免使用 readText() 读取未知大小的文件,这会导致 OOM。
  3. 拥抱 Okio:在 Android 项目中,尤其是涉及网络、缓存或复杂二进制解析时,Okio 是比 java.io 更优的选择。
  4. IO 线程隔离:无论使用哪个库,文件 IO 都是阻塞操作,必须在 Dispatchers.IO 中执行。