OKio解析

关于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;
  }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容