关于OKio,之前在OKHttp中有提及到,事实上除去OKHttp以外,OKio也是一个非常好的库,那么作为OKHttp底层IO操作库究竟有什么特殊的本领呢?我们通过这篇文章做一个详尽的了解。
OKio使用
Okio.sink(new File("***"));
Okio.sink(new FileOutputStream(new File("***")));
Okio.sink(new Socket("****",888));
Okio.source(new File("***"));
Okio.source(new FileInputStream(new File("***")));
Okio.source(new Socket("****",888));
BufferedSink bufferedSink = Okio.buffer(Okio.sink(new File("***")));
bufferedSink.writeInt(1);
BufferedSource bufferedSource = Okio.buffer(Okio.source(new File("***")));
bufferedSource.readInt();
//网络请求中可能会用到Gzip功能
GzipSink gzipSink = new GzipSink(Okio.sink(new File("***")));
BufferedSink bufferedSink1 = Okio.buffer(gzipSink);
bufferedSink1.writeInt(1);
使用就是如上了
写入分析
BufferedSink bufferedSink = Okio.buffer(Okio.sink(new File("***")));
bufferedSink.writeInt(1);
1.从这里分析首先是生成Sink对象
即
Okio.sink(new File("***"))
通过查看代码发现最后调用了方法如下
private static Sink sink(final OutputStream out, final Timeout timeout) {
if (out == null) throw new IllegalArgumentException("out == null");
if (timeout == null) throw new IllegalArgumentException("timeout == null");
return new Sink() {
@Override public void write(Buffer source, long byteCount) throws IOException {
checkOffsetAndCount(source.size, 0, byteCount);
while (byteCount > 0) {
timeout.throwIfReached();
Segment head = source.head;
int toCopy = (int) Math.min(byteCount, head.limit - head.pos);
out.write(head.data, head.pos, toCopy);
head.pos += toCopy;
byteCount -= toCopy;
source.size -= toCopy;
if (head.pos == head.limit) {
source.head = head.pop();
SegmentPool.recycle(head);
}
}
}
@Override public void flush() throws IOException {
out.flush();
}
@Override public void close() throws IOException {
out.close();
}
@Override public Timeout timeout() {
return timeout;
}
@Override public String toString() {
return "sink(" + out + ")";
}
};
}
可以看到这里返回了一个Sink类,当然这里实现了Sink接口中的几个方法,这些方法在后面都是有作用的
2.接下来是生成RealBufferedSink,并进行写入操作
即
BufferedSink bufferedSink = Okio.buffer(Okio.sink(new File("***")));
这里调用了Okio的buffer方法如下
public static BufferedSink buffer(Sink sink) {
return new RealBufferedSink(sink);
}
这里生成了RealBufferedSink对象,并将前面生成的sink对象传进去了
RealBufferedSink类中有一个Buffer变量,而这个Buffer类则是整个写入操作的实际操作部分
接下来我们看一下写入操作,如下
bufferedSink.writeInt(1);
RealBufferedSink中writeInt实现方法如下
@Override public BufferedSink writeInt(int i) throws IOException {
if (closed) throw new IllegalStateException("closed");
buffer.writeInt(i);
return emitCompleteSegments();
}
可以看到这里调用了Buffer的writeInt方法,如下
@Override public Buffer writeInt(int i) {
Segment tail = writableSegment(4);
byte[] data = tail.data;
int limit = tail.limit;
data[limit++] = (byte) ((i >>> 24) & 0xff);
data[limit++] = (byte) ((i >>> 16) & 0xff);
data[limit++] = (byte) ((i >>> 8) & 0xff);
data[limit++] = (byte) (i & 0xff);
tail.limit = limit;
size += 4;
return this;
}
这里实际是将数据写入了Segment中,至于Segment是什么,我们后续在进行说明
我们看下调用的writableSegment方法,如下
Segment writableSegment(int minimumCapacity) {
if (minimumCapacity < 1 || minimumCapacity > Segment.SIZE) throw new IllegalArgumentException();
if (head == null) {
head = SegmentPool.take(); // Acquire a first segment.
return head.next = head.prev = head;
}
Segment tail = head.prev;
if (tail.limit + minimumCapacity > Segment.SIZE || !tail.owner) {
tail = tail.push(SegmentPool.take()); // Append a new empty segment to fill up.
}
return tail;
}
可以看到这里的Segment是链表的结构,当新写入数据,就通过链表加入数据
所以到这里,我们写入的数据都是存储在Segment中的
接下来看一下emitCompleteSegments方法,如下
@Override public BufferedSink emitCompleteSegments() throws IOException {
if (closed) throw new IllegalStateException("closed");
long byteCount = buffer.completeSegmentByteCount();
if (byteCount > 0) sink.write(buffer, byteCount);
return this;
}
可以看到这里我们写入数据成功后,会调用sink的write方法,而这个write方法,就是前面Sink类中的方法,如下
@Override public void write(Buffer source, long byteCount) throws IOException {
checkOffsetAndCount(source.size, 0, byteCount);
while (byteCount > 0) {
timeout.throwIfReached();
Segment head = source.head;
int toCopy = (int) Math.min(byteCount, head.limit - head.pos);
out.write(head.data, head.pos, toCopy);
head.pos += toCopy;
byteCount -= toCopy;
source.size -= toCopy;
if (head.pos == head.limit) {
source.head = head.pop();
SegmentPool.recycle(head);
}
}
}
可以看到这里获取之前存储在Segment中的数据,然后用OutputStream的write方法,将数据写入到需要写入的地方,然后调用Segment的pop方法,释放之前存储的消息
其他的写入方法与上述的方法类型,不概述了
读取分析
同样的流程就不分析了,就从读取方法开始分析,如下
bufferedSource.readInt();
具体实现在RealBufferedSource中的readInt方法
@Override public int readInt() throws IOException {
require(4);
return buffer.readInt();
}
这里调用了require方法,而require方法调用request方法,这个方法调用了source.read(buffer, Segment.SIZE)
接下来看下这个read方法,如下
@Override public long read(Buffer sink, long byteCount) throws IOException {
if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
if (byteCount == 0) return 0;
try {
timeout.throwIfReached();
Segment tail = sink.writableSegment(1);
int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);
int bytesRead = in.read(tail.data, tail.limit, maxToCopy);
if (bytesRead == -1) return -1;
tail.limit += bytesRead;
sink.size += bytesRead;
return bytesRead;
} catch (AssertionError e) {
if (isAndroidGetsocknameError(e)) throw new IOException(e);
throw e;
}
}
可以看到这里通过InputStream的read方法,将数据读取到Segment数据中。
接下来看一下Buffer的readInt方法,如下
@Override public int readInt() {
if (size < 4) throw new IllegalStateException("size < 4: " + size);
Segment segment = head;
int pos = segment.pos;
int limit = segment.limit;
// If the int is split across multiple segments, delegate to readByte().
if (limit - pos < 4) {
return (readByte() & 0xff) << 24
| (readByte() & 0xff) << 16
| (readByte() & 0xff) << 8
| (readByte() & 0xff);
}
byte[] data = segment.data;
int i = (data[pos++] & 0xff) << 24
| (data[pos++] & 0xff) << 16
| (data[pos++] & 0xff) << 8
| (data[pos++] & 0xff);
size -= 4;
if (pos == limit) {
head = segment.pop();
SegmentPool.recycle(segment);
} else {
segment.pos = pos;
}
return i;
}
可以看到这里从之前存储在Segment中获取数据并返回。
其他的读取方法与上述的方法类型,不概述了
Segment
Segment字面翻译就是片段,Okio将数据也就是Buffer分割成一块块的片段,同时segment拥有前置节点和后置节点,构成一个双向循环链表,就像下面这个图的方式。
![](/Users/admin/Documents/Life/LibReadProgejct/2799272-acef05808a0c1e58.png =500x200)
这样采取分片使用链表连接,片中使用数组存储,兼具读的连续性,以及写的可插入性,对比单一使用链表或者数组,是一种折中的方案,读写更快,而且有个好处根据需求改动分片的大小来权衡读写的业务操作,另外,segment也有一些内置的优化操作,综合这些Okio才能大放异彩
Segment中成员变量如下
static final int SIZE = 8192;
static final int SHARE_MINIMUM = 1024;
final byte[] data;
int pos;
int limit;
boolean shared;
boolean owner;
Segment pre;
Segment next;
SIZE就是一个segment的最大字节数,其中还有一个SHARE_MINIMUM,这个涉及到segment优化中的另一个技巧,共享内存,然后data就是保存的字节数组,pos,limit就是开始和结束点的index,shared和owner用来设置状态判断是否可写,一个有共享内存的segment是不能写入的,pre,next就是前置后置节点。
Segment方法分析
既然是双向循环链表,其中也会有一些操作的方法,比如
public Segment pop(){
Segment result = next != this ? next : null;
pre.next = next;
next.pre = pre;
next = null;
pre = null;
return result;
}
pop方法移除了自己,首先将自己的前后两个节点连接起来,然后将自己的前后引用置空,这样就脱离了整个双向链表,然后返回next
public Segment push(Segment segment){
segment.pre = this;
segment.next = next;
next.pre = segment;
next = segment;
return segment;
}
push方法就是在当前和next引用中间插入一个segment进来,并且返回插入的segment,这两个都是寻常的双向链表的操作,我们再来看看如何写入数据
public void writeTo(Segment sink , int byteCount){
if (!sink.owner)
throw new IllegalArgumentException();
if (sink.limit + byteCount > SIZE){ //limit和需要写的字节总和大于SIZE
if (sink.shared) //共享无法写
throw new IllegalArgumentException();
if (sink.limit + byteCount - sink.pos > SIZE){ //如果减去头依然比SIZE大 那么就无法写抛异常
throw new IllegalArgumentException();
}
//否则我们需要先移动要写的文件地址 然后置limit pos的地址
System.arraycopy(sink.data,sink.pos,sink.data,0,sink.limit - sink.pos);
sink.limit = sink.limit - sink.pos;
sink.pos = 0;
}
//开始尾部写入 写完置limit地址
System.arraycopy(data,pos,sink.data,sink.limit,byteCount);
sink.limit = sink.limit + byteCount;
pos = pos + byteCount; //当前索引后移
}
owner和Shared这两个状态目前看来是完全相反的,赋值都是同步赋值的,这里有点不明白存在两个参数的意义,现在的功能主要是用来判断如果是共享就无法写,以免污染数据,会抛出异常。当然,如果要写的字节大小加上原来的字节数大于单个segment的最大值也是会抛出异常,也存在一种情况就是虽然尾节点索引和写入字节大小加起来超过,但是由于前面的pos索引可能因为read方法取出数据,pos索引后移这样导致可以容纳数据,这时就先执行移动操作,使用系统的 System.arraycopy 方法来移动到pos为0的状态,更改pos和limit索引后再在尾部写入byteCount数的数据,写完之后实际上原segment读了byteCount的数据,所以pos需要后移这么多。过程十分的清晰,比较好理解。
除了写入数据之外,segment还有一个优化的技巧,因为每个segment的片段size是固定的,为了防止经过长时间的使用后,每个segment中的数据千疮百孔,可能十分短的数据却占据了一整个segment,所以有了一个压缩机制。
public void compact(){
if (pre == this)
throw new IllegalStateException();
if (!pre.owner) // pre不可写
return;
int byteCount = limit - pos;
int availableByteCount = SIZE - pre.limit + (pre.shared ? 0 : pre.pos); //前一个的剩余大小
if (byteCount > availableByteCount)
return;
writeTo(pre,byteCount); //将数据写入到前一个的片段中
pop(); // 从双向链表中移除当前
SegmentPool.recycle(this); //加入到对象池中
}
照例如果前面是共享的那么不可写,也就不能压缩了,然后判断前一个的剩余大小是否比当前的大,有足够的空间来容纳数据,调用前面的 writeTo 方法来写数据,写完后移除当前segment,然后通过 SegmentPool 来回收。
另一个技巧就是共享机制,为了减少数据复制带来的性能开销,segment存在一个共享机制。
public Segment split(int byteCount){
if (byteCount <= 0 || byteCount > limit - pos )
throw new IllegalArgumentException();
Segment prefix;
if (byteCount >= SHARE_MINIMUM){ //如果byteCount大于最小的共享要求大小
prefix = new Segment(this); //this这个构造函数会
}else {
prefix = SegmentPool.take();
System.arraycopy(data,pos,prefix,0,byteCount);
}
prefix.limit = prefix.pos + byteCount;
pos = pos + byteCount;
pre.push(prefix);
return prefix;
}
再回头看刚才的 compact 中出现的 SegmentPool ,这个实际上是一个segment的对象池
static final long MAX_SIZE = 64 * 1024;
static Segment next;
static long byteCount;
同样的有一个池子的上限,也就是64k,相当于8个segment,next这个节点可以看出这个 SegmentPool 是按照单链表的方式进行存储的,byteCount则是目前已有的大小。
SegmentPool方法分析
SegmentPool的方法十分的少,一个取,一个回收,十分简洁。
/**
* take方法用来取数据
* 如果池子为空就创建一个空对象 owner true | share false
* next是链表的头 就是一个简单的取表头的操作
* @return
*/
static Segment take(){
synchronized (SegmentPool.class){
if (next != null){
Segment result = next;
next = result.next;
result.next = null;
byteCount = byteCount - Segment.SIZE;
return result;
}
}
return new Segment();
}
为了防止多线程同时操作造成数据的错乱,这里加了锁,这里的next命名虽然是next,但是实际上是整个对象池的头,但是next为空,表示池子为空,直接返回一个空对象,否则从里面拿出next,并将next的下一个节点赋为next,置一下状态,这个方法就结束了
/**
* 如果当前要回收的segment有前后引用或者是共享的 那么就回收失败
* 如果加入后的大小超过了最大大小 也会失败
* 然后将新回收的next指向表头 也就是加到的链表的头 并且将回收的segment置为next也就是head
* @param segment
*/
static void recycle(Segment segment){
if (segment.next != null || segment.pre != null)
throw new IllegalArgumentException();
if (segment.shared)
return;
synchronized (SegmentPool.class){
if (byteCount + Segment.SIZE > MAX_SIZE){
return;
}
byteCount += Segment.SIZE;
segment.next = next;
segment.pos = segment.limit = 0;
next = segment;
}
}
如果要回收的segment有前后引用或者是共享的,就不能被回收,所以要回收前先将引用置空,同样这里也加了锁,以免那个同时回收超过池子最大的大小,然后就是将回收的插到表头的操作。
所以SegmentPool无论是回收和取对象都是在表头操作。
装饰流
1.Forwarding流
public abstract class ForwardingSink implements Sink {
private final Sink delegate;
public ForwardingSink(Sink delegate) {
if (delegate == null) throw new IllegalArgumentException("delegate == null");
this.delegate = delegate;
}
/** {@link Sink} to which this instance is delegating. */
public final Sink delegate() {
return delegate;
}
@Override public void write(Buffer source, long byteCount) throws IOException {
delegate.write(source, byteCount);
}
@Override public void flush() throws IOException {
delegate.flush();
}
@Override public Timeout timeout() {
return delegate.timeout();
}
@Override public void close() throws IOException {
delegate.close();
}
@Override public String toString() {
return getClass().getSimpleName() + "(" + delegate.toString() + ")";
}
}
Forwarding流乍看之下似乎没有什么实际用处,但是在实际作用中,常常用来作为匿名的代理对象,由于传入的sink本身我们无法继承和复写,这样我们可以直接采用装饰Forwarding流的方式来监控和拦截一些操作。
2.DeflaterSink和InflaterSource流
这对流主要对应于zip压缩流,类似ZipInputStream和ZipOutputStream,而实际上再ZipInputStream中实际上也用到了相同的API。
//code ZipInputStream.java
public class ZipInputStream {
public void read(...) {
try {
read = inf.inflate(buffer, byteOffset, byteCount);//引用了一个protected Inflater inf;对象
} catch (DataFormatException e) {
throw new ZipException(e.getMessage());
}
}
}
我们看到在ZipInputStream的流读取处理的时候,用到了一个Inflater类型的对象,这个对象是专门用来处理Zip格式编码的工具类。而在InflaterSource这个源去读取数据的时候,也一样用到了这个类:
while (true) {
boolean sourceExhausted = refill();
// Decompress the inflater's compressed data into the sink.
try {
Segment tail = sink.writableSegment(1);
int bytesInflated = inflater.inflate(tail.data, tail.limit, Segment.SIZE - tail.limit);
if (bytesInflated > 0) {
tail.limit += bytesInflated;
sink.size += bytesInflated;
return bytesInflated;
}
if (inflater.finished() || inflater.needsDictionary()) {
releaseInflatedBytes();
if (tail.pos == tail.limit) {
// We allocated a tail segment, but didn't end up needing it. Recycle!
sink.head = tail.pop();
SegmentPool.recycle(tail);
}
return -1;
}
if (sourceExhausted) throw new EOFException("source exhausted prematurely");
} catch (DataFormatException e) {
throw new IOException(e);
}
}
3.GzipSource和GzipSink流
Gzip和zip的主要区别在于平台通用性和压缩率,一般情况下,Gzip的压缩率更高点,Gzip是基于zip算法上的再改造。因此,在Gzip流中必须包装原始流为一个InflaterSource流:
public GzipSink(Sink sink) {
if (sink == null) throw new IllegalArgumentException("sink == null");
this.deflater = new Deflater(DEFAULT_COMPRESSION, true /* No wrap */);
this.sink = Okio.buffer(sink);
this.deflaterSink = new DeflaterSink(this.sink, deflater);
writeHeader();
}
4.Pipe流
Okio中的pipe流类似生产者消费者的模式,例如在管道流PipeSource读取的时候,发现buffer.size,也就是缓冲池数据长度为0的时候,管道PipeSource流陷入等待。一直等到PipeSink流往Buffer中再输出的时候,阻塞消失。并且,管道的流一定是成对出现的。
final class PipeSource implements Source {
final Timeout timeout = new Timeout();
@Override public long read(Buffer sink, long byteCount) throws IOException {
synchronized (buffer) {
if (sourceClosed) throw new IllegalStateException("closed");
while (buffer.size() == 0) {// buffer为空
if (sinkClosed) return -1L;
timeout.waitUntilNotified(buffer); // Wait until the sink fills the buffer.//陷入等待
}
long result = buffer.read(sink, byteCount);
buffer.notifyAll(); // Notify the sink that it can resume writing.
return result;
}
}
@Override public void close() throws IOException {
synchronized (buffer) {
sourceClosed = true;
buffer.notifyAll(); // Notify the sink that no more bytes are desired.
}
}
@Override public Timeout timeout() {
return timeout;
}
}