为何要了解io? io是我们每天都要使用,但是很少有人关注细节的一个模块,尤其对于客户端的开发来讲,几乎很少直接面向io编程,android rom中提供的基础方法已经足够好,基本满足各种需求。至于NIO这种看上去高大上可能只有服务端的同学才有了解了。但是 随着你app用户的增多不免遇到各种各样奇怪的问题,这个时候你如果熟悉io会对你找bug 修bug 有很大的好处。何况关于io 的代码你如果能研读一遍对你自身也是有很大好处的。 本篇不探讨NIO的部分,NIO的部分我们下篇再说。 java 提供的io操作方法 简单看一下 java 原生提供的io包 大概有哪些东西,类的关系如何
实际上看的出来 输入输出流的 结果都差不多。 简要分析一下InputStream输入流的结构
首先这是一个抽象类,有一个重要的read 抽象方法 等待他的子类去实现 结合前面的图 我们可以得知,inputstream有很多子类,这里我们重点看一下 一个特殊的子类,FilterInputStream, 因为很容易看出来,InputStream中其他子类就到底了,没有子类的子类了,但是这个FilterInputStream 仍然有不少子类 public class FilterInputStream extends InputStream { /** * The input stream to be filtered. */ protected volatile InputStream in;
protected FilterInputStream(InputStream in) {
this.in = in;
}
public int read() throws IOException {
return in.read();
}
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
return in.read(b, off, len);
}
public long skip(long n) throws IOException {
return in.skip(n);
}
public int available() throws IOException {
return in.available();
}
public void close() throws IOException {
in.close();
}
public synchronized void mark(int readlimit) {
in.mark(readlimit);
}
public synchronized void reset() throws IOException {
in.reset();
}
public boolean markSupported() {
return in.markSupported();
}
}
复制代码FilterInputStream 这个源码可以看出来,内部就只有一个inputstream 对象,然后所有操作都交给这个inputstream的对象来完成 相当于这个FilterInputStream 就是一个壳吗 然后我们再来看看这个FilterInputStream 的子类都干嘛了,我们选BufferedInputStream 来看看。 public class BufferedInputStream extends FilterInputStream { public synchronized int read() throws IOException { if (pos >= count) { fill(); if (pos >= count) return -1; } return getBufIfOpen()[pos++] & 0xff; } 复制代码这里不深究细节,只需要理解,你看我们之前的FilterInputStream read方法里什么都没做,你传什么is对象进来就用这个对象的read方法,但是BufferedInputStream 不同,BufferedInputStream重写了read方法,使得这个bis对象具有了缓存读入的功能。 相当于bis 把 传进来的is对象给装饰了一下。同理,对于其它的FilterInputStream的子类,其作用也是一样的,那就是装饰一个InputStream,为它添加它原本不具有的功能。OutputStream以及家属对于装饰器模式的体现,也以此类推。 写个demo仔细体会java io中装饰者模式的用法和设计思路 //随便打开一个本地文件 final String filePath = "D:\error.log";
//InputStream相当于被装饰的接口或者抽象类,FileInputStream相当于原始的待装饰的对象,FileInputStream无法装饰InputStream
//另外FileInputStream是以只读方式打开了一个文件,并打开了一个文件的句柄存放在FileDescriptor对象的handle属性
//所以下面有关回退和重新标记等操作,都是在堆中建立缓冲区所造成的假象,并不是真正的文件流在回退或者重新标记
InputStream inputStream = new FileInputStream(filePath);
final int len = inputStream.available();//记录一下流的长度
System.out.println("FileInputStream not support mark and reset:" + inputStream.markSupported());
System.out.println("---------------------------------------------------------------------------------");
//首先装饰成BufferedInputStream,它提供我们mark,reset的功能
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);//装饰成 BufferedInputStream
System.out.println("BufferedInputStream support mark and reset:" + bufferedInputStream.markSupported());
bufferedInputStream.mark(0);//标记一下
char c = (char) bufferedInputStream.read();
System.out.println("file first char is :" + c);
bufferedInputStream.reset();//重置
c = (char) bufferedInputStream.read();//再读
System.out.println("reset and first char is :" + c);
bufferedInputStream.reset();
System.out.println("---------------------------------------------------------------------------------");
复制代码IO学习到这就足够了吗? 可以体会一下上面的java io 代码,其实java io 为什么要使用 装饰者模式,我也没想明白,粗看起来,这样的代码,分层明确, 但是细细体会一下 这样的设计模式带来的后果就是类太多了。其实对于使用者而言,并不是特别友好。我只想要一杯水,你给我一个大海干啥?尤其对于客户端来说,有没有更加优雅的一种io解决方案呢?有,OKIO OKIO是什么 Okio库是一个由square公司开发的,它补充了java.io和java.nio的不足,以便能够更加方便,快速的访问、存储和处理你的数据。而OkHttp的底层也使用该库作为支持。见过大多的客户端开发用OKHttp用的飞起,然后在碰到io类的开发时,又回去使用并不太好用的java io,而忽略了OKio这个神器,那这篇文章后半段就带你介绍介绍OKio。(学习OKIO源码对理解java IO 也是有很大好处的) OKIO的结构
可以看出来,这个okio的整体结构还是简洁明了非常简单的,不像java io 我截图都截不下。 (OKio的用法很简单,就不过多做介绍了) 这里写一段通过okio输出数据的代码 String fileName="C:\Users\16040657\Desktop\iotest.txt"; File file= new File(fileName); BufferedSink bufferedSink=Okio.buffer(Okio.sink(file)); bufferedSink.writeString("12345", Charset.defaultCharset()); bufferedSink.writeString("678910", Charset.defaultCharset()); bufferedSink.close(); 复制代码可以看一下调用链: 1.生成一个file对象 2.通过OKio.sink的构造方法 生成一个sink 对象,我们把这个对象称之为对象A。
private Okio() { }
public static BufferedSource buffer(Source source) {
return new RealBufferedSource(source);
}
public static BufferedSink buffer(Sink sink) {
return new RealBufferedSink(sink);
}
复制代码然后把这个对象A 传到OKio的buffer方法里 就返回一个RealBufferedSink 对象B。 最后再对这个B对象进行实际操作。 Sink的简要分析 sink其实就等于java io 体系中的输入流。我们简要分析一下。 public interface Sink extends Closeable, Flushable { void write(Buffer var1, long var2) throws IOException;
void flush() throws IOException;
Timeout timeout();
void close() throws IOException;
} 复制代码可以看出来sink就4个方法,write方法有个参数叫buffer,还有个方法叫flush,很容易才想到这个东西跟缓存是相关的。 接着看她的子类BufferedSink
可以看到这仍然是一个接口,只不过提供了更多的抽象方法而已。 最后我们看看真正的实现类。RealBufferedSink
我们选取其中的一部分代码看看 public final Buffer buffer = new Buffer(); public final Sink sink; boolean closed;
RealBufferedSink(Sink sink) {
if(sink == null) {
throw new NullPointerException("sink == null");
} else {
this.sink = sink;
}
}
public Buffer buffer() {
return this.buffer;
}
public void write(Buffer source, long byteCount) throws IOException {
if(this.closed) {
throw new IllegalStateException("closed");
} else {
this.buffer.write(source, byteCount);
this.emitCompleteSegments();
}
}
public BufferedSink write(ByteString byteString) throws IOException {
if(this.closed) {
throw new IllegalStateException("closed");
} else {
this.buffer.write(byteString);
return this.emitCompleteSegments();
}
}
复制代码可以看出来,这些所谓write的方法真正的执行者 其实就是这个buffer对象而已。看上去很像java io的装饰者模式对吧。 对外暴露了一个类C,但其实类c的这些方法里真正操作的却是另外一个对象B 搞清楚Buffer到底在干嘛 结合上述的分析,我们可以看到,最后的操作都是通过buffer来完成的,我们就来看看这个buffer是怎么完成输出流 这件事的。 public Buffer writeString(String string, int beginIndex, int endIndex, Charset charset) { if(string == null) { throw new IllegalArgumentException("string == null"); } else if(beginIndex < 0) { throw new IllegalAccessError("beginIndex < 0: " + beginIndex); } else if(endIndex < beginIndex) { throw new IllegalArgumentException("endIndex < beginIndex: " + endIndex + " < " + beginIndex); } else if(endIndex > string.length()) { throw new IllegalArgumentException("endIndex > string.length: " + endIndex + " > " + string.length()); } else if(charset == null) { throw new IllegalArgumentException("charset == null"); } else if(charset.equals(Util.UTF_8)) { return this.writeUtf8(string, beginIndex, endIndex); } else { byte[] data = string.substring(beginIndex, endIndex).getBytes(charset); return this.write(data, 0, data.length); } } 复制代码可以看到utf-8的数据写入是比较简单的,其他的数据就是直接写成byte 字节流了。
最核心的写入数据方法就在这里了。首先我们来看看这个Segment到底是啥 // // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) //
package okio;
import javax.annotation.Nullable; import okio.SegmentPool;
final class Segment { //最大长度是8192个byte static final int SIZE = 8192; //可共享的data数组长度 static final int SHARE_MINIMUM = 1024; //真正存放数据的地方 final byte[] data; //开始和结束的limit int pos; int limit; //是否可以共享,可以避免数据拷贝。增强效率 boolean shared; //是否对自己的data数组有写权限,比如segment b是用segment a来构造出来的,那么a就拥有写入权限,但是b没有。 boolean owner; //看到next 和prev 应该很容易联想到双向链表 Segment next; Segment prev;
Segment() {
this.data = new byte[8192];
this.owner = true;
this.shared = false;
}
Segment(Segment shareFrom) {
this(shareFrom.data, shareFrom.pos, shareFrom.limit);
shareFrom.shared = true;
}
Segment(byte[] data, int pos, int limit) {
this.data = data;
this.pos = pos;
this.limit = limit;
this.owner = false;
this.shared = true;
}
@Nullable
public Segment pop() {
Segment result = this.next != this?this.next:null;
this.prev.next = this.next;
this.next.prev = this.prev;
this.next = null;
this.prev = null;
return result;
}
public Segment push(Segment segment) {
segment.prev = this;
segment.next = this.next;
this.next.prev = segment;
this.next = segment;
return segment;
}
//数据共享以后就无法写入了,所以要避免出现存在大片的共享小片段,所以一定要大于1024个byte 才会使用这个共享data
//数组的提高效率的操作
public Segment split(int byteCount) {
if(byteCount > 0 && byteCount <= this.limit - this.pos) {
Segment prefix;
if(byteCount >= 1024) {
prefix = new Segment(this);
} else {
//对于pool这样的关键字,我们要有敏感性就是为了防止频繁创造销毁对象造成的cpu抖动,所以可以认为是对象池
prefix = SegmentPool.take();
System.arraycopy(this.data, this.pos, prefix.data, 0, byteCount);
}
prefix.limit = prefix.pos + byteCount;
this.pos += byteCount;
this.prev.push(prefix);
return prefix;
} else {
throw new IllegalArgumentException();
}
}
//时间长了以后一个segment中间可能只有一小段是可以用的,所以这里做压缩,用于将当前的data 放到前面的
//数据中 然后将自己移出,放入到segmentpool中
public void compact() {
if(this.prev == this) {
throw new IllegalStateException();
} else if(this.prev.owner) {
int byteCount = this.limit - this.pos;
int availableByteCount = 8192 - this.prev.limit + (this.prev.shared?0:this.prev.pos);
if(byteCount <= availableByteCount) {
this.writeTo(this.prev, byteCount);
this.pop();
SegmentPool.recycle(this);
}
}
}
public void writeTo(Segment sink, int byteCount) {
if(!sink.owner) {
throw new IllegalArgumentException();
} else {
if(sink.limit + byteCount > 8192) {
//如果是共享的就不让写
if(sink.shared) {
throw new IllegalArgumentException();
}
//如果将要写入的数据和目前存在的数据加起来大于8192 也不给写
if(sink.limit + byteCount - sink.pos > 8192) {
throw new IllegalArgumentException();
}
System.arraycopy(sink.data, sink.pos, sink.data, 0, sink.limit - sink.pos);
sink.limit -= sink.pos;
sink.pos = 0;
}
System.arraycopy(this.data, this.pos, sink.data, sink.limit, byteCount);
sink.limit += byteCount;
this.pos += byteCount;
}
}
}
复制代码搞清楚segment大概是什么东西以后,我们就可以来看看buffer的write方法到底做了啥了。 //循环写入数据 public Buffer write(byte[] source, int offset, int byteCount) { if(source == null) { throw new IllegalArgumentException("source == null"); } else { Util.checkOffsetAndCount((long)source.length, (long)offset, (long)byteCount);
Segment tail;
int toCopy;
for(int limit = offset + byteCount; offset < limit; tail.limit += toCopy) {
//拿出一个可用的segment容器来,他的内部就是byte数组也就是data,拿出的原则就是看segment是否有足够的空//间写入
tail = this.writableSegment(1);
//计算剩余空间长度
toCopy = Math.min(limit - offset, 8192 - tail.limit);
//把byte数组 复制到segmnt的容器中 并且计算索引
System.arraycopy(source, offset, tail.data, tail.limit, toCopy);
offset += toCopy;
}
//更新buffer的大小
this.size += (long)byteCount;
return this;
}
}
复制代码到目前我们就可以稍微捋一捋okio写入数据的流程: 其实就是把bytes数组 往buffer里面写,buffer 里面 是一个双向的segment链表。写入的数据实际上就存放在segment的data数组中 这个data数组当然是bytes数组。 但是目前我们发现写入的数据还是在内存里,在缓存里啊,在哪里真正的输出到我们的硬盘当中的呢? public void close() throws IOException { if(!this.closed) { Throwable thrown = null;
try {
if(this.buffer.size > 0L) {
//这个sink其实就是一开始我们传进去的文件输出流啊。之前所有的操作都在缓存里。
//只有在这才是真正的输出到文件里,到硬盘。
this.sink.write(this.buffer, this.buffer.size);
}
} catch (Throwable var3) {
thrown = var3;
}
try {
this.sink.close();
} catch (Throwable var4) {
if(thrown == null) {
thrown = var4;
}
}
this.closed = true;
if(thrown != null) {
Util.sneakyRethrow(thrown);
}
}
}