IO 与文件操作
在 Kotlin 中,IO 操作分为两个流派:一是基于 Java 标准库 (java.io) 的 Kotlin 扩展函数,适用于纯 JVM/Android 项目;二是基于 Okio 的现代化 IO 库,它是 KMP 和高性能 Android 开发的首选。
Kotlin 标准库 IO 扩展
Kotlin 为 java.io.File、InputStream 和 OutputStream 提供了大量实用的扩展函数,极大地简化了样板代码。
资源自动关闭:use
use 是 try-with-resources 的 Kotlin 版本。它适用于任何实现了 AutoCloseable 的对象,确保块执行完毕或发生异常时自动关闭资源。
FileInputStream("data.txt").use { input ->
// 读取逻辑
val data = input.readBytes()
} // input 在此自动关闭FileInputStream input = null;
try {
input = new FileInputStream("data.txt");
// ...
} finally {
if (input != null) input.close();
}文件读写简化
Kotlin 允许你像操作字符串一样操作文件内容。
性能警示
readText 和 readBytes 会将文件内容一次性加载到内存。对于超过几 MB 的大文件,严禁使用这些方法,应使用流式处理(forEachLine 或 inputStream)。
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() 提供了一种惰性遍历文件树的方法。
File("src").walk()
.filter { it.isFile && it.extension == "kt" }
.forEach { println("Found Kotlin file: ${it.name}") }流拷贝:copyTo
将输入流的数据传输到输出流,支持设置缓冲区大小。
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 重载了 / 操作符,使得路径拼接非常直观。
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 版本。
// 写入
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
在 Android 和 KMP 开发中,java.io API 显得陈旧且难以测试。Okio 是一个基于 Segment 机制的高性能 IO 库,它也是 Retrofit 和 Coil 的底层基石。
依赖配置
implementation("com.squareup.okio:okio:3.9.0")核心概念:Source 与 Sink
Source:等同于InputStream(读取)。Sink:等同于OutputStream(写入)。Buffer:Okio 的核心,一个智能的、可自动扩容的字节容器。
读写实战
Okio 的强大之处在于它极易使用的 API 和对二进制数据的原生支持。
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?
- 内存优化:Okio 使用
Segment池技术复用内存,大幅减少 IO 过程中的 GC 压力。 - KMP 支持:Okio 提供了
FileSystemAPI,使得文件操作代码可以在 Android、iOS 和 JVM 间完全共享。 - ByteString:提供了不可变的字节串处理能力,非常适合处理网络协议、哈希计算和 Base64 编解码。
// 计算 MD5 和 Base64
val md5 = ByteString.encodeUtf8("hello").md5().hex()
val base64 = ByteString.encodeUtf8("hello").base64()核心工程准则
- 优先使用 use:永远不要手动调用
close(),除非你在编写极其底层的库。 - 大文件流式处理:避免使用
readText()读取未知大小的文件,这会导致 OOM。 - 拥抱 Okio:在 Android 项目中,尤其是涉及网络、缓存或复杂二进制解析时,Okio 是比
java.io更优的选择。 - IO 线程隔离:无论使用哪个库,文件 IO 都是阻塞操作,必须在
Dispatchers.IO中执行。