文章目录
1 okio 概念
以下三小节翻译自 okio 文档。
1.1 okio
okio 是对传统 io 和 nio 的扩展库,使数据访问、数据存储及相应处理更加简单。
1.2 ByteStrings 和 Buffers
Okio 是围绕 ByteString
和 Buffer
两种类型来构建的,ByteString
和 Buffer
提供了简明的 API,并包含了丰富的功能。
- ByteString 是一个不可变(immutable)byte 序列。
String
类型是字符数据的基础(String 是不可变的,比如不能在原变量中修改某个字符),而ByteString
有点像String
,但它是用来简化处理二进制数据的。ByteString 是个‘人性化’的类:它知道如何将自身编码/解码成 hex,base64 和 UTF-8 类型的数据。 - Buffer 是一个可变(mutable) byte 序列。就像
ArrayList
,使用者不用提前设置Buffer
的大小。读写Buffer
时,可以把它想像成一个 Queue:把写入的数据放到了队尾,读取时从队头获取数据。使用者不用关心怎么去维护 positions, limits 或 capacities(译注:这些都是相对底层使用到的标记信息,Buffer
通过Segment
对象对这些信息进行了封装)。
ByteString
和 Buffer
内部做了一些聪明的事件来节省 CUP 和 内存资源。比如,使用者把一个 UTF-8 类型的 String 编码为一个 ByteString
,ByteString
会缓存这个 String 的引用,之后对 ByteString 解码时就不需要执行解码操作了。
Buffer
内部有一个 Segment
的链表。当需要将数据从一个 buffer 转移到另一个 buffer 时,Buffer
会改变 Segment
的所属对象,这样就避免了来回拷贝数据。这种处理方式在多线程环境下特别好使:一个用于网络传输的线程可以将数据直接传送给工作线程而且不需要任何复制或仪式性(ceremony)的操作。
1.3 Sources 和 Sinks
java.io
设计优雅的部分是将流(streams)进行分层,一层一层对数据流进行传输处理,比如加密和压缩。Okio 包含了另外的一套流类型叫做 Source
和 Sink
(译注:这种命名来自 guava),它们工作机制与 InputStream
和 OutputStream
类似,但有如下主要区别:
- 超时。 okio 流对基础 I/O 操作提供了访问超时机制。不像
java.io
socket 流,okio 中read()
和write()
都支持超时功能。 - 容易实现(implement)。
Source
声明了三个方法:read()
,close()
和timeout()
。没有需要靠人品方法如available()
或单字节读取操作,单字节读取会导致程序在正确性和性能方面时有‘惊喜’。 - 容易使用。 尽管
Source
和Sink
只有三个方法去实现(implements),调用者通过BufferedSource
和BufferedSink
提供了丰富的 API,这里有你需要的一切。 - 形式上不区分字节流和字符流。 它们全部是 data。读取和写入时会把 data 当作 byte, UTF-8 字符串,大端 32-bit 整型,小端 short 型;无论做什么,都不会再有
InputStreamReader
! - 容易测试。
Buffer
类实现了BufferedSource
和BufferedSink
接口,so 你的测试代码会简单清晰。
Source 和 Sink 可以与 InputStream
和 OutputStream
相互转换(译注:通过 okio 提供的工具方法和实现相关接口来转换)。可以将任何 Source
视作 InputStream
,并且可以把任何 InputStream
当作 Source
。类似的,Sink
与 OutputStream
也有这样的关系。
2 底层实现
本节会从原理及源码角度去分析 okio 的诸多优点,看看别人家的代码是如何写的…
2.1 简洁明了 API 背后的逻辑
还记得被 java.io 各种字节流、字符流处理支配的恐惧吗?还记得为了记住 InputStreamReader
与 OutputStreamWriter
是干什么的而精神分裂吗?是的,这些东西在 okio 里统统没有。okio 里不分字节流、字符流,甚至没有‘流’的概念。它在处理数据的方面更偏向与 nio 中‘块’的概念,在 okio 中叫 Segment
。Segment
是以队列(双向链表)形式存在的,并且 Segment
的创建与销毁共享同一个缓存池 – SegmentPool
。后面会分析 Segment
与 SegmentPool
的工作原理。现在先体会一下它简洁的 API,ok, 帖代码,看看 okio 是如何完成文件复制的
@Test
public void readAndSaveFileByOkio() throws Exception {
final int size = 1024;
// createTemp(size) 是自定义方法,表示创建一个长度等于 size 字节的文件
File srcFile = createTemp(size);
assertTrue(srcFile.exists());
File desFile = temporaryFolder.newFile();
assertEquals(desFile.length(), 0L);
BufferedSource source = Okio.buffer(Okio.source(srcFile)); // 1
BufferedSink sink = Okio.buffer(Okio.sink(desFile)); // 2
source.readAll(sink); // 3
source.close();
sink.close();
assertEquals(desFile.length(), size);
}
从整个过程来看,只用三行代码完成了数据的转移。这里主要涉及了三个概念 Source, Sink 和 Buffer,Source 代表了数据来源,Sink 代表数据要传输到何方,而 Buffer 就是数据的中转站。它们关系如下:
细分 1、2、3 行代码,可以感觉到第 1、2 行属于构建行为,描绘出数据流动趋势,真正动作的发起者是 source.readAll(sink)
。现在以 source.readAll(sink)
方法为主线,看一下 okio 是如何运行的。
首先看一下 source
生成过程,通过工具方法 Okio.buffer(Source)
生成一个 RealBufferedSource
对象。
public static BufferedSource buffer(Source source) {
return new RealBufferedSource(source);
}
看名字就能猜出对 Source 所有操作会在 RealBufferedSource
对象内进行,暂时不需要知道 source 是什么,等到查看 Source 某个方法需要调用到派生类中的方法时再看。同样 Okio.buffer(Sink)
会返回一个 RealBufferedSink
对象。
进入 source.readAll(sink)
方法,即 RealBufferedSource.readAll(Sink)
方法
// RealBufferedSource.readAll
@Override
public long readAll(Sink sink) throws IOException {
...
long totalBytesWritten = 0;
// 从 source 读取 SIZE 份数据,放入 buffer 中,直到读取值为 -1
while (source.read(buffer, Segment.SIZE) != -1) {
long emitByteCount = buffer.completeSegmentByteCount();
if (emitByteCount > 0) {
totalBytesWritten += emitByteCount;
// 向 sink 中写入 emitByteCount 份数据
sink.write(buffer, emitByteCount);
}
}
...
return totalBytesWritten;
}
里面代码也挺直白的,因为 source 是个接口,所以要定位 source.read(buffer, Segment.SIZE)
的执行者,现在可以看 Okio.source(srcFile)
方法了
// Okio.source
public static Source source(File file) throws FileNotFoundException {
...
return source(new FileInputStream(file));
}
// Okio.source
public static Source source(InputStream in) {
return source(in, new Timeout());
}
// Okio.source
private static Source source(final InputStream in,