值得一用的IO神器Okio
本文主题:
-
传统Java IO的使用
-
Java IO内部的设计实现为什么是这样?
-
IO神器Okio
传统的java io不太好用啊
记得在我第一次使用java的io去操作一个文件时,从网上copy类似这样一段代码:
InputStream in = null;
InputStream binStream = null;
try {
in = new FileInputStream("./test.txt");
binStream = new BufferedInputStream(in);
byte[] data = new byte[128];
while (binStream.read(data) != -1) {
//...
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if (in != null){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (binStream != null ){
try {
binStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
当时就觉得很麻烦,不理解为啥要这么写。由于没有弄懂后面的原理,之后每次写类似的代码都会去网上copy一份。
现在再写这段代码,第一反应是想去搞清楚为啥自己不能徒手撸出这段代码。学技术要弄懂背后的本质呀。于是,就有了一段探索之旅:
探索过程中最困惑的一点是,
用FileInputStream
不就能直接把文件内容读取出来,BufferedInputStream
出现的意义是什么?如果BufferedInputStream
如它的名字一样,有缓存功能,那直接用它不行吗?还要把FileInputStream
对象注入到它里面,这么做真的有必要吗?
要解决这个困惑,就要通过源码看看下背后的实现机制:
class FileInputStream extends InputStream{
}
class BufferedInputStream extends FilterInputStream {
}
class FilterInputStream extends InputStream {
protected volatile InputStream in;
protected FilterInputStream(InputStream in) {
this.in = in;
}
public int read() throws IOException {
return in.read();
}
public void close() throws IOException {
in.close();
}
//...
}
可以看出来的是,文件输入流FileInputStream
很正常的继承了InputStream
抽象类,但奇怪的是BufferedInputStream
,它继承的却是FilterInputStream
,进入这个类的实现只是简单的重写父类的几个方法,具体实现也只是调用父类的方法,其他没有做任何额外操作。
这里就迷惑了,这么设计意图何在?只是简单封装一层,没看出有什么意义。
如果你对装饰者设计模式很熟悉,很轻松就可看出这里的设计就是装饰者设计模式的运用。如果不了解的话,那可能就和我一样困惑了。
装饰器设计模式应用场景有个很重要的特点:装饰器类会附加跟原始类相关的增强功能
BufferedInputStream就是这个装饰器类,它提供的增强功能:增加缓存功能。通过提供一块缓冲区,输入流可以先放到这个缓冲区里面,然后再输出到目的地(内存或网络)。它的好处就减少和内存的读取交互次数,毕竟频繁的读取交互是比较耗费性能的。
举个例子解释下缓冲区是如何提升性能的:
假设有一个8K大小的文件,如果仅使用
InputStream
来读取,每次读取1K,则需要读取8次,也就需要和文件交互8次。但如果使用缓冲流BufferedInputStream
来读取,在第一次读取文件的时候,就会从文件中一次性读取8K(BufferedInputStream
中默认缓冲区大小)数据到缓冲区中,虽然最后还是得会从缓冲区每次读取1K,共读取8次,但是这8次是从缓冲区读,远比直接与文件交互的性能高。另外可见的是,当文件数据越大,通过缓冲区的方式效率提升越明显。
至于为啥这里要用装饰器模式呢?下面简单探讨一下。
我们假设不用装饰器模式,功能增强就只是通过继承的方式实现,会出现什么问题呢?
比如需要文件缓存功能,我们就要加一个 BufferedFileInputStream
,一个类而已完全可以接受。
如果我们还需要对功能进行其他方面的增强,比如支持按照基本数据类型(int、boolean、long 等)来读取数据(命名为DataInputStream
)。这种情况下,如果我们继续按照继承的方式来实现的话,就需要再继续派生出类似像 DataFileInputStream
、DataPipedInputStream
这样的类。如果我们还需要既支持缓存、还要按照基本类型读取数据的类,那就要再继续派生出 BufferedDataFileInputStream
、BufferedDataPipedInputStream
等。
这才添加了两个增强功能,假设有m个增强功能,n个基础类。那通过继承就会有m*n个类。同时类继承结构会变得复杂,代码也不好扩展,不好维护。
但通过装饰者设计模式,开发者需要哪些功能自己去组合。这样就可以通过组合的方式解决这种继承爆炸的问题,只需要m+n个类。
下图就是JDK通过装饰者模式实现后的io读取字节流相关的类,可以看出来还是有相当多的类:
现在明白了背后设计的核心思想,最开始的那段代码也就很轻松的理解了。
但依然感觉不是很好用,我只是想读写一个文件,这样写还是有点麻烦。有没有更简易的方式,比如对这些操作封装后的API,或者新的IO框架?
我相信我的问题肯定有很多人也有,Google一番,发现了一个IO神器,有趣的是,这个神器的名字曾在Okhttp
框架源码里见过 - Okio。
IO神器Okio
官方是这么介绍Okio的:
Okio is a library that complements
java.io
andjava.nio
to make it much easier to access, store, and process your data. It started as a component of OkHttp, the capable HTTP client included in Android. It’s well-exercised and ready to solve new problems.
用Google翻译成“人话“:
Okio是对java.io和java.nio的补充,它使访问,存储和处理数据变得更加容易。它作为OkHttp(Android包含的功能强大的HTTP客户端)的组件开始的。它已被很好地锻炼,并准备解决新问题。
重点是这一句它使访问,存储和处理数据变得更加容易,既然Okio是对java.io的补充,那是否比传统IO好用呢?
看下Okio这么使用的,用它读写一个文件试试:
// OKio写文件
private static void writeFileByOKio() {
try (Sink sink = Okio.sink(new File(path));
BufferedSink bufferedSink = Okio.buffer(sink)) {
bufferedSink.writeUtf8("write" + "\n" + "success!");
} catch (IOException e) {
e.printStackTrace();
}
}
//OKio读文件
private static void readFileByOKio() {
try (Source source = Okio.source(new File(path));
BufferedSource bufferedSource = Okio.buffer(source)) {
for (String line; (line = bufferedSource.readUtf8Line()) != null; ) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
使用try后面跟随()括号是Java7新特性: try括号内的资源会在try语句结束后自动释放,前提是这些可关闭的资源必须实现 java.lang.AutoCloseable 接口。
从代码中可以看出,读写文件关键一步要创建出 BufferedSource
或 BufferedSink
对象。有了这两个对象,就可以直接读写文件了。
不过也没比传统IO使用简洁到哪里去。其实是因为这个例子比较简单,前面提到传统IO使用了装饰者设计模式来可以提供增强能力。
把场景稍微变复杂点,那假设需要读取一个整数或浮点数,就需要用DataInputStream
来增强,同时为了效率还需要缓存功能,就还要装饰一层BufferInputStream
。类似下面这样(核心代码):
fileStream = new FileInputStream(path);
binStream = new BufferedInputStream(fileStream);
dataInputStream = new DataInputStream(binStream);
dataInputStream.readInt();
但Okio为我们提供的BufferedSink
和BufferedSource
就具有以上基本所有的功能,不需要再串上一系列的装饰类。类似下面这样(核心代码):
Source source = Okio.source(new File(path));
BufferedSource bufferedSource = Okio.buffer(source)) {
bufferedSource.readInt()
这里可以看出,Okio把传统IO的复杂场景使用简单化了,也确实让我们访问数据更容易了。到这里,我最开始的需求已经解决。
现在开始好奇Okio是怎么设计成这么好用的?看一下它的类设计:
在Okio读写使用中,比较关键的类有Source、Sink、BufferedSource、BufferedSink。
Source和Sink
Source
和Sink
是接口,类似传统IO的InputStream
和OutputStream
,具有输入、输出流功能。
Sourece
接口主要用来读取数据,而数据的来源可以是磁盘,网络,内存等
public interface Source extends Closeable {
long read(Buffer sink, long byteCount) throws IOException;
Timeout timeout();
@Override void close() throws IOException;
}
Sink
接口主要用来写入数据
public interface Sink extends Closeable, Flushable {
void write(Buffer source, long byteCount) throws IOException;
@Override void flush() throws IOException;
Timeout timeout();
@Override void close() throws IOException;
}
BufferedSource和BufferedSink
BufferedSource
和BufferedSink
是对Source
和Sink
接口的扩展处理。Okio将常用方法封装在BufferedSource
/BufferedSink
接口中,把底层字节流直接加工成需要的数据类型,摒弃Java IO中各种输入流和输出流的嵌套,并提供了很多方便的api,比如readInt()
、readString
public interface BufferedSource extends Source, ReadableByteChannel {
Buffer getBuffer();
int readInt() throws IOException;
String readString(long byteCount, Charset charset) throws IOException;
}
public interface BufferedSink extends Sink, WritableByteChannel {
Buffer buffer();
BufferedSink writeInt(int i) throws IOException;
BufferedSink writeString(String string, int beginIndex, int endIndex, Charset charset)
throws IOException;
}
RealBufferedSink和RealBufferedSource
上面的BufferedSource
和BufferedSink
都还是接口,它们对应的实现类就是RealBufferedSink
和RealBufferedSource
了。
final class RealBufferedSource implements BufferedSource {
public final Buffer buffer = new Buffer();
@Override public String readString(Charset charset) throws IOException {
if (charset == null) throw new IllegalArgumentException("charset == null");
buffer.writeAll(source);
return buffer.readString(charset);
}
//...
}
final class RealBufferedSink implements BufferedSink {
public final Buffer buffer = new Buffer();
@Override public BufferedSink writeString(String string, int beginIndex, int endIndex,
Charset charset) throws IOException {
if (closed) throw new IllegalStateException("closed");
buffer.writeString(string, beginIndex, endIndex, charset);
return emitCompleteSegments();
}
//...
}
RealBufferedSource
和RealBufferedSink
内部都维护一个Buffer
对象。里面的实现方法,最终实现都转到Buffer对象处理。所以这个Buffer
类可以说是Okio
的灵魂所在。下面会详细介绍。
Buffer
Buffer
的好处是以数据块Segment
从InputStream
读取数据的,相比单个字节读取来说,效率提高了,是一种空间换时间的策略。
public final class Buffer implements BufferedSource, BufferedSink, Cloneable, ByteChannel {
Segment head;
@Override public Buffer getBuffer() {
return this;
}
@Override public String readString(long byteCount, Charset charset) throws EOFException {
checkOffsetAndCount(size, 0, byteCount);
if (charset == null) throw new IllegalArgumentException("charset == null");
if (byteCount > Integer.MAX_VALUE) {
throw new IllegalArgumentException("byteCount > Integer.MAX_VALUE: " + byteCount);
}
if (byteCount == 0) return "";
Segment s = head;
if (s.pos + byteCount > s.limit) {
// If the string spans multiple segments, delegate to readBytes().
return new String(readByteArray(byteCount), charset);
}
String result = new String(s.data, s.pos, (int) byteCount, charset);
s.pos += byteCount;
size -= byteCount;
if (s.pos == s.limit) {
head = s.pop();
SegmentPool.recycle(s);
}
return result;
}
//...
}
从代码中可以看出,这个Buffer
是个集大成者,实现了BufferedSink
和BufferedSource
的接口,也就是意味着它同时具有读和写的功能。
它的内部维护了一个数据块Segment
,它又是什么呢?
final class Segment {
//大小是8kb
static final int SIZE = 8192;
//读取数据的起始位置
int pos;
//写数据的起始位置
int limit;
//后继
Segment next;
//前继
Segment prev;
//将当前的Segment对象从双向链表中移除,并返回链表中的下一个结点作为头结点
public final @Nullable Segment pop() {
Segment result = next != this ? next : null;
prev.next = next;
next.prev = prev;
next = null;
prev = null;
return result;
}
//向链表中当前结点的后面插入一个新的Segment结点对象,并移动next指向新插入的结点
public final Segment push(Segment segment) {
segment.prev = this;
segment.next = next;
next.prev = segment;
next = segment;
return segment;
}
//单个Segment空间不足以存储写入的数据时,就会尝试拆分为两个Segment
public final Segment split(int byteCount) {
//...
}
//合并一些邻近的Segment
public final void compact() {
}
}
从pop
和push
方法可以看出Segment
是一个双向链表
的数据结构。一个Segment大小是8kb。正是由于Segment使IO读写操作能如此高效。
和Segment紧密相关的还有一个SegmentPoll
。
final class SegmentPool {
static final long MAX_SIZE = 64 * 1024;
static @Nullable Segment next;
//当池子里面有空闲的 Segment 就直接复用,否则就创建一个新的 Segment
static Segment take() {
synchronized (SegmentPool.class) {
if (next != null) {
Segment result = next;
next = result.next;
result.next = null;
byteCount -= Segment.SIZE;
return result;
}
}
return new Segment(); // Pool is empty. Don't zero-fill while holding a lock.
}
//回收 segment 进行复用,提高效率
static void recycle(Segment segment) {
if (segment.next != null || segment.prev != null) throw new IllegalArgumentException();
if (segment.shared) return; // This segment cannot be recycled.
synchronized (SegmentPool.class) {
if (byteCount + Segment.SIZE > MAX_SIZE) return; // Pool is full.
byteCount += Segment.SIZE;
segment.next = next;
segment.pos = segment.limit = 0;
next = segment;
}
}
}
SegmentPool
是一个缓存Segment
的池,它有64kb
大小也就是8
个Segment
的长度。既然作为一个池,就和线程池的作用类似,为了复用前面被回收的Segment
。recycle()
方法的作用则是回收一个Segment
对象。被回收的Segment对象将会被插入到SegmentPool
中的单链表的头部,以便后面继续复用。
SegmentPool
的作用防止已申请的资源被回收,增加资源的重复利用,减少GC,过于频繁的GC是会降低性能的
可以看到Okio在内存优化上下了很大的功夫,提升了资源的利用率,从而提升了性能。
另外需要注意的是,对于OKio
来说,它的Buffer
是个外部工具而已。什么意思呢,OKio
要把数据写入到Buffer
,是需要通过source
的read
方法,而不是xxx.write
方法:
try (Source source = Okio.source(new File(path))) {
Buffer buffer = new Buffer();
//把source的数据写入到buffer里面去
source.read(buffer, 1024);
System.out.println("okio buffer read:" + buffer.readUtf8Line());
} catch (IOException e) {
e.printStackTrace();
}
总结
不仅如此,Okio还提供其他很有用的功能:
比如提供了一系列的方便工具
- GZip的透明处理
- 对数据计算md5、sha1等都提供了支持,对数据校验非常方便
再比如提供了超时机制的处理,内部的设计也很有意思,感兴趣可参考
写本篇的目的很明确就是为了安利大家把Okio
这个IO神器用起来,基本上IO操作它都可以比传统IO更加高效,简单的使用,不过有一个不足就是不能像传统IO那样灵活搭配自己想要的增强功能。
本文笔者自觉写的不是很好,如果大家觉得没看懂还可以参考下面的文章
该篇对源码讲解对比较细:
该篇对内容讲解的比较细,提到了Socket通信
其他相关零碎知识:
转载自:https://juejin.cn/post/6923902848908394510