Okio 使用教程:从入门到精通

Okio 使用教程:从入门到精通

先来看一张 Okio 的整体架构图,建立直觉:

第一章:为什么要用 Okio?

在学 API 之前,先看看 Java 原生 IO 有哪些痛点。

Java IO 的典型写法(读取一个文件的所有行):

java 复制代码
// Java IO:冗长、容易出错、资源管理繁琐
List<String> lines = new ArrayList<>();
BufferedReader reader = null;
try {
    reader = new BufferedReader(
        new InputStreamReader(
            new FileInputStream("data.txt"), "UTF-8"
        )
    );
    String line;
    while ((line = reader.readLine()) != null) {
        lines.add(line);
    }
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (reader != null) {
        try { reader.close(); } catch (IOException ignored) {}
    }
}

Okio 的等效写法:

java 复制代码
// Okio:简洁、安全、链式
try (BufferedSource source = Okio.buffer(Okio.source(new File("data.txt")))) {
    String line;
    while ((line = source.readUtf8Line()) != null) {
        lines.add(line);
    }
}

核心差异体现在:冗余的包装层消失了、编码声明消失了、finally 消失了


第二章:入门 --- 基本读写

添加依赖

groovy 复制代码
// build.gradle (Kotlin/Java 项目)
implementation("com.squareup.okio:okio:3.9.0")

2.1 写文件

java 复制代码
import okio.*;
import java.io.File;

// ✅ Okio 写法
File file = new File("output.txt");
try (BufferedSink sink = Okio.buffer(Okio.sink(file))) {
    sink.writeUtf8("Hello, Okio!\n");
    sink.writeUtf8("第二行内容\n");
    // try-with-resources 会自动 flush + close
}

// ❌ Java IO 等效写法(需要6行)
try (PrintWriter pw = new PrintWriter(
        new OutputStreamWriter(
            new FileOutputStream(file), "UTF-8"))) {
    pw.println("Hello, Java IO!");
    pw.println("第二行内容");
}

2.2 读文件

java 复制代码
// ✅ Okio 读文件
try (BufferedSource source = Okio.buffer(Okio.source(new File("output.txt")))) {
    // 一次性读取全部内容为 String
    String content = source.readUtf8();
    System.out.println(content);
}

// ✅ 逐行读取
try (BufferedSource source = Okio.buffer(Okio.source(new File("output.txt")))) {
    String line;
    while ((line = source.readUtf8Line()) != null) {
        System.out.println(line);
    }
}

2.3 读写字节数组

java 复制代码
// ✅ 写入字节
byte[] bytes = {0x48, 0x65, 0x6C, 0x6C, 0x6F}; // "Hello"
try (BufferedSink sink = Okio.buffer(Okio.sink(new File("bytes.bin")))) {
    sink.write(bytes);
    sink.writeInt(42);      // 写一个 int(4字节,大端)
    sink.writeLong(1000L);  // 写一个 long(8字节)
}

// ✅ 读取字节
try (BufferedSource source = Okio.buffer(Okio.source(new File("bytes.bin")))) {
    byte[] buf = source.readByteArray(5);  // 读5个字节
    int n = source.readInt();              // 读4字节 int
    long l = source.readLong();            // 读8字节 long
    System.out.println(new String(buf) + ", " + n + ", " + l);
    // 输出: Hello, 42, 1000
}

第三章:核心概念 --- Buffer(零拷贝的秘密)

Buffer 是 Okio 的心脏,理解它才能理解 Okio 的性能优势。

Buffer 直接使用示例:

java 复制代码
// Buffer 同时实现了 BufferedSource 和 BufferedSink,可以直接在内存里读写
Buffer buffer = new Buffer();

// 写入
buffer.writeUtf8("Hello");
buffer.writeInt(42);
buffer.writeByte(0xFF);

System.out.println(buffer.size()); // 10 (5 + 4 + 1)

// 读取(消费型------读了就没了)
System.out.println(buffer.readUtf8(5)); // "Hello"
System.out.println(buffer.readInt());   // 42

// 对比 Java NIO ByteBuffer:需要手动管理 position/limit/flip,非常容易出错
java.nio.ByteBuffer bb = java.nio.ByteBuffer.allocate(64);
bb.put("Hello".getBytes());
bb.putInt(42);
bb.flip(); // 忘记 flip 是 NIO 最常见 bug!
byte[] dst = new byte[5];
bb.get(dst);
System.out.println(new String(dst)); // "Hello"
System.out.println(bb.getInt());     // 42

第四章:进阶 --- Source 和 Sink 的装饰器模式

Okio 和 Java IO 一样支持流的包装,但更直观:

4.1 GZip 压缩/解压

java 复制代码
// ✅ Okio:写 GZip 压缩文件
File gzFile = new File("data.gz");
try (BufferedSink sink = Okio.buffer(new GzipSink(Okio.sink(gzFile)))) {
    sink.writeUtf8("这是要压缩的大文本内容...\n");
    sink.writeUtf8("Okio 内置 GzipSink/GzipSource\n");
}

// ✅ Okio:读 GZip 文件
try (BufferedSource source = Okio.buffer(new GzipSource(Okio.source(gzFile)))) {
    System.out.println(source.readUtf8());
}

// ❌ Java IO 等效(更繁琐)
try (BufferedWriter bw = new BufferedWriter(
        new OutputStreamWriter(
            new GZIPOutputStream(
                new FileOutputStream("data.gz")), "UTF-8"))) {
    bw.write("这是要压缩的大文本内容...");
}

4.2 哈希计算(HashingSink / HashingSource)

这是 Okio 独有的能力,Java IO 没有对应的简洁实现:

java 复制代码
// 在写入数据的同时计算 MD5/SHA256,不需要读两遍数据!
HashingSink hashingSink = HashingSink.md5(Okio.blackhole()); 
// Okio.blackhole() 是一个丢弃所有数据的 Sink(类似 /dev/null)

try (BufferedSink sink = Okio.buffer(hashingSink)) {
    sink.writeUtf8("计算这段文字的 MD5");
}
ByteString md5 = hashingSink.hash();
System.out.println("MD5: " + md5.hex());

// 同样可以 wrap 文件 Sink,边写边算 hash
HashingSink fileSink = HashingSink.sha256(Okio.sink(new File("output.txt")));
try (BufferedSink sink = Okio.buffer(fileSink)) {
    sink.writeUtf8("文件内容");
}
System.out.println("SHA-256: " + fileSink.hash().hex());

// Java 原生等效(需要手动用 MessageDigest,且要读两遍或自己 wrap)
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] data = "计算这段文字的 MD5".getBytes("UTF-8");
byte[] digest = md.digest(data);
// 还需要手动转 hex...

4.3 ForwardingSink / ForwardingSource(拦截器模式)

这是实现监控、限速等功能的关键:

java 复制代码
// 自定义一个带进度回调的 Sink
public class ProgressSink extends ForwardingSink {
    private long bytesWritten = 0;
    private final long totalBytes;
    private final ProgressListener listener;

    public ProgressSink(Sink delegate, long totalBytes, ProgressListener listener) {
        super(delegate);
        this.totalBytes = totalBytes;
        this.listener = listener;
    }

    @Override
    public void write(Buffer source, long byteCount) throws IOException {
        super.write(source, byteCount);
        bytesWritten += byteCount;
        listener.onProgress(bytesWritten, totalBytes);
    }
}

// 使用:带进度的文件下载
long fileSize = 1024 * 1024; // 1MB
Sink fileSink = Okio.sink(new File("download.bin"));
ProgressSink progressSink = new ProgressSink(fileSink, fileSize, (done, total) -> {
    System.out.printf("进度: %.1f%%%n", done * 100.0 / total);
});

try (BufferedSink sink = Okio.buffer(progressSink)) {
    // ... 写入下载数据
}

第五章:ByteString --- 不可变字节序列

ByteString 是 Okio 的另一个利器,Java 原生没有对应物(只有 byte[],但它是可变的且没有工具方法):

java 复制代码
// 创建 ByteString
ByteString bs1 = ByteString.encodeUtf8("Hello Okio");
ByteString bs2 = ByteString.decodeHex("48656c6c6f"); // "Hello"
ByteString bs3 = ByteString.of(new byte[]{0x01, 0x02, 0x03});

// 丰富的工具方法
System.out.println(bs1.utf8());        // "Hello Okio"
System.out.println(bs1.hex());         // 十六进制字符串
System.out.println(bs1.base64());      // Base64 编码
System.out.println(bs1.md5().hex());   // MD5 哈希
System.out.println(bs1.sha256().hex());// SHA-256 哈希

// 截取子串(不拷贝数据,共享底层 byte[])
ByteString sub = bs1.substring(0, 5);
System.out.println(sub.utf8()); // "Hello"

// 不可变:适合当 HashMap key、缓存、常量
// ✅ 线程安全,可以在多线程间共享

// 对比 Java 原生 byte[](每次操作都需要手动转换)
byte[] javaBytes = "Hello Okio".getBytes("UTF-8");
String hex = javax.xml.bind.DatatypeConverter.printHexBinary(javaBytes); // 还需要引入额外包
String b64 = java.util.Base64.getEncoder().encodeToString(javaBytes);   // Java 8 才有

第六章:精通 --- Pipe、超时机制与网络应用

6.1 Pipe(进程内管道)

Pipe 是生产者-消费者模式的神器,Java NIO 的 Pipe 更复杂:

java 复制代码
// 创建一个缓冲 1MB 的管道
Pipe pipe = new Pipe(1024 * 1024);

// 生产者线程:向管道写入数据
Thread producer = new Thread(() -> {
    try (BufferedSink sink = Okio.buffer(pipe.sink())) {
        for (int i = 0; i < 100; i++) {
            sink.writeUtf8("数据块 " + i + "\n");
            sink.flush(); // 确保消费者能读到
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
});

// 消费者线程:从管道读取数据
Thread consumer = new Thread(() -> {
    try (BufferedSource source = Okio.buffer(pipe.source())) {
        String line;
        while ((line = source.readUtf8Line()) != null) {
            System.out.println("读到: " + line);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
});

producer.start();
consumer.start();

6.2 超时机制(Timeout)

这是 Okio 对 Java IO 的重要改进。Java IO 的 Socket.setSoTimeout 只能设读超时,Okio 提供更细粒度的控制:

java 复制代码
// 带超时的 Socket 通信
Socket socket = new Socket("example.com", 80);

// 创建带超时的 Source
Source rawSource = Okio.source(socket);
// AsyncTimeout 可以异步地在超时时关闭 socket
AsyncTimeout timeout = new AsyncTimeout() {
    @Override
    protected void timedOut() {
        try { socket.close(); } catch (IOException ignored) {}
    }
};
timeout.timeout(30, TimeUnit.SECONDS);
Source timeoutSource = timeout.source(rawSource);

try (BufferedSource source = Okio.buffer(timeoutSource)) {
    // 如果 30 秒内没读到数据,会自动关闭 socket 并抛出 IOException
    String response = source.readUtf8();
}

// Okio Timeout 还支持 deadline(截止时间)
Timeout t = new Timeout();
t.deadline(5, TimeUnit.SECONDS);  // 整个操作最多 5 秒
t.timeout(2, TimeUnit.SECONDS);   // 每次读写最多 2 秒

6.3 实战:高效的文件拷贝

java 复制代码
// Okio 最高效的文件拷贝
public static void copyFile(File src, File dst) throws IOException {
    try (Source source = Okio.source(src);
         Sink sink = Okio.sink(dst)) {
        // buffer 内部会以 Segment 为单位移动数据,不做额外拷贝
        Okio.buffer(sink).writeAll(source);
    }
}

// 对比 Java NIO(Files.copy 更简洁,但无法插入拦截逻辑)
java.nio.file.Files.copy(src.toPath(), dst.toPath());

// 对比 Java IO(经典 while 循环拷贝)
try (InputStream in = new FileInputStream(src);
     OutputStream out = new FileOutputStream(dst)) {
    byte[] buf = new byte[8192]; // 需要自己管理缓冲区大小
    int read;
    while ((read = in.read(buf)) != -1) {
        out.write(buf, 0, read);
    }
}

第七章:与 OkHttp 结合(Okio 的实战场景)

Okio 最广泛的使用场景之一就是 OkHttp,理解这个有助于你在网络层写出高质量代码:

java 复制代码
OkHttpClient client = new OkHttpClient();

// 上传文件(用 Okio 的 RequestBody)
RequestBody fileBody = new RequestBody() {
    @Override
    public MediaType contentType() {
        return MediaType.get("application/octet-stream");
    }

    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        // sink 就是 Okio 的 BufferedSink!直接用 Okio API 写
        try (Source source = Okio.source(new File("upload.bin"))) {
            sink.writeAll(source);
        }
    }
};

// 下载文件(用 Okio 的 ResponseBody)
Request request = new Request.Builder().url("https://example.com/file").build();
try (Response response = client.newCall(request).execute()) {
    ResponseBody body = response.body();
    // body.source() 就是 Okio 的 BufferedSource!
    try (BufferedSink fileSink = Okio.buffer(Okio.sink(new File("download.bin")))) {
        fileSink.writeAll(body.source());
    }
}

第八章:全面对比总结

来看一张完整的对比总结图:


学习路径总结

按照以下顺序练习,循序渐进:

  1. 入门 :用 Okio.buffer(Okio.source/sink) 替换你项目里的 BufferedReader/Writer,感受 API 的简洁。
  2. 理解 Buffer :在内存中直接 new Buffer() 做序列化/反序列化的练习,体会它和 ByteBuffer 的差异。
  3. 进阶装饰 :用 GzipSink + HashingSink 做压缩+校验的流水线,理解装饰器链。
  4. 自定义拦截 :继承 ForwardingSink 实现进度监控或限速 Sink,理解 Okio 的扩展点。
  5. 精通 :研究 AsyncTimeoutPipe,在网络编程中(配合 OkHttp)发挥 Okio 的全部威力。

如果你想深入某一个章节(比如 Kotlin 协程版 Okio、二进制协议解析,或者源码分析),随时告诉我!

Okio是否只能用于Android吗?

Okio 完全不限于 Android,它是一个纯 JVM 库(同时也支持 Kotlin Multiplatform),普通的 IntelliJ Kotlin/Java 项目完全可以用。

Okio 支持的平台:

  • JVM(普通 Java/Kotlin 项目、Spring Boot、命令行工具等)
  • Android(这只是 JVM 平台的一个子集)
  • Kotlin/Native(iOS、macOS、Linux)
  • Kotlin/JS

所以它只是"因为 OkHttp 和 Android 生态而出名",但本质上是一个跨平台的通用 IO 库。


在普通 IntelliJ Kotlin 项目里添加依赖:

如果用 Gradle(Kotlin DSL)

kotlin 复制代码
// build.gradle.kts
dependencies {
    implementation("com.squareup.okio:okio:3.9.0")
}

如果用 Gradle(Groovy DSL)

groovy 复制代码
// build.gradle
dependencies {
    implementation 'com.squareup.okio:okio:3.9.0'
}

如果用 Maven

xml 复制代码
<dependency>
    <groupId>com.squareup.okio</groupId>
    <artifactId>okio</artifactId>
    <version>3.9.0</version>
</dependency>

添加完同步一下 Gradle/Maven,就可以直接 import okio.* 使用了,和 Android 项目里的用法完全一致。