1. 简介
本文将带你深入理解 Okio —— 一个由 Square(也就是 OkHttp 的作者团队)开发的高效 I/O 库。它对 JVM 原生的 IO 功能做了大幅增强,尤其适合处理字节流、网络数据和文件操作。
如果你写过 Java 的 InputStream
和 OutputStream
,那你一定知道它们有多啰嗦、性能多拉胯。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 的两大核心数据结构是 ByteString
和 Buffer
,它们分别代表不可变和可变的字节序列。
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:数据流动的抽象
如果说 ByteString
和 Buffer
是“静态数据”,那么 Source
和 Sink
就是“流动的数据”。
这两个接口的设计灵感来自 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