为何要了解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);
}
}
}
复制代码