1. 简介

本文将带你深入理解 Okio —— 一个由 Square(也就是 OkHttp 的作者团队)开发的高效 I/O 库。它对 JVM 原生的 IO 功能做了大幅增强,尤其适合处理字节流、网络数据和文件操作。

如果你写过 Java 的 InputStreamOutputStream,那你一定知道它们有多啰嗦、性能多拉胯。Okio 正是为了解决这些问题而生:更简洁的 API、更好的性能、更少的内存拷贝,以及对 Kotlin 友好的设计。

一句话总结:Okio 是现代 Java/Kotlin 开发中处理 I/O 的首选工具库之一,尤其是在配合 OkHttp 使用时几乎无缝集成。


2. 依赖配置

使用前需要引入最新版本的依赖。撰写本文时,最新稳定版是 3.9.0

Maven

<dependency>
    <groupId>com.squareup.okio</groupId>
    <artifactId>okio</artifactId>
    <version>3.9.0</version>
</dependency>

Gradle (Kotlin DSL)

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

⚠️ 注意:Okio 从 3.x 起已全面转向 Kotlin 编写,但仍完美兼容 Java。建议在 Kotlin 项目中使用以获得最佳体验。


3. ByteString 与 Buffer

Okio 的两大核心数据结构是 ByteStringBuffer,它们分别代表不可变和可变的字节序列。

3.1 ByteString:不可变字节数组

可以类比为 String 之于字符,ByteString 就是字节的“字符串”——一旦创建就不能修改。

创建方式

// 直接构造
val byteString = ByteString.of(1, 2)

// 从 ByteArray 转换
val byteArray = byteArrayOf(72, 101, 108, 108, 111) // "Hello"
val byteString = byteArray.toByteString()

// 从 String 编码生成
val utf8Bytes = "Hello".encodeUtf8()
val utf32Bytes = "Hello".encode(Charsets.UTF_32)

转换输出

// 回到 ByteArray
val bytes = byteString.toByteArray()

// 转成 ByteBuffer
val buffer = byteString.asByteBuffer()

// 解码为字符串
val text = byteString.utf8()
val customText = byteString.string(Charsets.ISO_8859_1)

// 写入 OutputStream
byteString.write(outputStream)

实用功能

// Base64 编解码
val decoded = "SGVsbG8=".decodeBase64()!!
val encoded = byteString.base64()

// Hex 编解码
val hexDecoded = "48656c6c6f".decodeHex()
val hexString = byteString.hex()

// 哈希计算(返回新的 ByteString)
byteString.md5().hex()
byteString.sha1().base64()

✅ 提示:ByteString 是线程安全的,适合做缓存键或跨线程传递二进制数据。


3.2 Buffer:可变字节缓冲区

Buffer 是一个动态增长的字节队列,支持高效的读写操作。它是 Okio 中最常用的中间载体。

构造

val buffer = Buffer()

写入数据 ✍️

buffer.write(byteArray)
buffer.write(byteString)
buffer.write(anotherBuffer, 10) // 写 10 个字节

buffer.writeByte(65)           // 写单个字节
buffer.writeInt(123456789)     // 大端整数
buffer.writeIntLe(123456789)   // 小端整数
buffer.writeUtf8("Hello")      // UTF-8 字符串

buffer.readFrom(inputStream, 1024) // 从输入流读取最多 1024 字节

读取数据 🔍

val data = buffer.readByteArray()
val str = buffer.readByteString()

val b = buffer.readByte()
val i = buffer.readInt()
val ile = buffer.readIntLe()
val s = buffer.readUtf8()

buffer.writeTo(outputStream, 10) // 把前 10 字节写出到输出流

关键特性

  • ✅ 自动维护读写指针(read/write head)
  • ✅ 支持零拷贝拼接多个 Buffer
  • ✅ 可当作 FIFO 队列使用

常见踩坑点 ❌:

不要误以为 read*() 方法会重置位置。一旦读过,指针就前移了,后续读取是接着上次的位置继续的。

示例场景:

val length = buffer.readInt()  // 先读长度
val content = buffer.readUtf8(length) // 再读指定长度的内容

这种模式非常适合解析自定义协议包头。


4. Source 与 Sink:数据流动的抽象

如果说 ByteStringBuffer 是“静态数据”,那么 SourceSink 就是“流动的数据”。

这两个接口的设计灵感来自 Unix 的管道思想:

  • Source:你能从中读数据(类似 InputStream
  • Sink:你能往里写数据(类似 OutputStream

4.1 Sink:数据出口

Sink 是一个接口,表示任意可写入的目标。

基本用法

val sink = buildSink()
val buffer = Buffer().writeUtf8("Hello")
sink.write(buffer, buffer.size)
sink.close()

标准实现

类型 说明
OutputStreamSink 包装 java.io.OutputStream
Buffer 自身也可作为 Sink
GzipSink 边写边压缩
CipherSink 加密写入

包装为 BufferedSink(推荐)

val sink = outputStream.sink()
val bufferedSink = sink.buffer()

bufferedSink.writeUtf8("Hello, World!")
bufferedSink.flush() // ⚠️ 必须 flush 或 close 才真正落盘

✅ 优势:提供 writeInt()writeUtf8Line() 等便捷方法,无需手动转成 Buffer。

装饰器模式应用

val fileSink = FileSystem.SYSTEM.sink("data.gz".toPath())
val gzipSink = GzipSink(fileSink)
val buffered = gzipSink.buffer()

buffered.writeUtf8("Compressed content")
buffered.close() // close 会自动 flush 并关闭所有层

⚠️ 重要:压缩类 Sink(如 GzipSink)必须调用 close()flush() 才能输出完整数据块,否则可能丢失尾部内容!


4.2 Source:数据入口

Sink 对应,Source 表示可读取的数据源。

基本读取

val source = buildSource()
val buffer = Buffer()
val bytesRead = source.read(buffer, 1024) // 最多读 1024 字节

返回值 -1 表示 EOF。

标准实现

类型 说明
InputStreamSource 包装 java.io.InputStream
Buffer 自身也可作为 Source
GzipSource 边读边解压

推荐使用 BufferedSource

val source = inputStream.source()
val bufferedSource = source.buffer()

val text = bufferedSource.readUtf8()        // 读全部
val line = bufferedSource.readUtf8Line()    // 读一行
val intVal = bufferedSource.readInt()       // 读整数

解压缩示例

val source = FileSystem.SYSTEM.source("data.gz".toPath())
val gzipSource = GzipSource(source)
val content = gzipSource.buffer().readUtf8()
gzipSource.close()

✅ 技巧:BufferedSource 支持 peeking(预读不移动指针),可用于协议探测:

val peek = bufferedSource.peek()
if (peek.readByte() == 0x1F) {
    // 可能是 GZIP 文件
}

5. FileSystem:统一文件系统抽象

Okio 提供了跨平台的文件系统抽象,让本地文件操作也变得优雅。

获取系统文件系统

val fs = FileSystem.SYSTEM

常见操作

// 列出当前目录
val paths = fs.list(Path("/tmp"))

// 读取文件内容
val source = fs.source(Path("config.json"))
val content = source.buffer().readUtf8()

// 写入文件
val sink = fs.sink(Path("log.txt"))
sink.buffer().writeUtf8("New log entry\n").close()

✅ 优势:

  • 统一 API,屏蔽平台差异
  • 自动处理资源释放(配合 try-with-resources)
  • 与 Source/Sink 完美集成

⚠️ 注意路径写法:

"README.md".toPath() // Kotlin 扩展函数
Paths.get("README.md") // Java NIO

6. 总结

Okio 不只是一个“更好用的 IO 工具包”,它的设计理念深刻影响了现代 Java/Kotlin 的 I/O 编程方式:

  • ByteString:轻量、安全的不可变字节容器
  • Buffer:高性能可变缓冲,支持链式读写
  • Source / Sink:清晰的流向抽象,易于组合
  • ✅ 装饰器模式支持压缩、加密等中间处理
  • ✅ 与 java.io 无缝互操作
  • ✅ 为 OkHttp 提供底层支撑,网络开发必备

🚀 建议:哪怕你只在项目中用它来读写文件或处理响应体,也能显著提升代码可读性和健壮性。

所有示例代码均已开源在 GitHub:
👉 https://github.com/baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-io


原始标题:Okio Overview