OKHttp源码分析---OKIO

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_36391075/article/details/82818603

简介

JavaNIO和阻塞I/O
  1. 阻塞I/O通信模式:调用InputStream.read()方法时是阻塞的,它会一直等到数据来时才返回。
  2. NIO通信模式:在JDK1.4开始,是一种非阻塞I/O,在Java NIO的服务器由一个专门的线程来处理所有I/O事件,并负责分发。线程之间通讯通过wait和notify等方式。
okio

okio是由square公司开发的,它补充了java.io和java.nio的不足,以便能够更加方便,快速的访问,存储和处理数据。OKHttp底层也是用该库作为支持,而且okio使用起来很简单,减少了很多io操作的基本代码,并且对内存和cpu使用做了优化,它的主要功能封装在ByteString和Buffer这两个类中。

okio的作者认为,java的JDK对字节流和字符流进行分开定义这一事情,并不是那么优雅,所以早okio并不进行这样的划分,具体的做法就是把比特数据交给Buffer管理,然后Buffer实现BufferedSource和BufferedSink这两个接口,最后通过调用buffer响应的方法对数据进行编码。

Okio的使用

假设有一个text.txt文件,我们用Okio将它读出来。
读取文件的步骤是首先要拿到一个输入流,okio封装了许多输入流,统一使用的方法重载source方法转换成一个source,然后使用buffer方法包装成BufferedSource,这个里面提供了流的各种操作,读String,读byte,short等,甚至是16进制数。这里直接读出文件的内容,十分简单,代码如下:

public static void main(String[] args){
        File file = new File("test.txt");
        try {
            readFile(new FileInputStream(file));
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    public static void readFile(InputStream inputStream)throws IOException{
        BufferedSource source = Okio.buffer(Okio.source(inputStream));
        String s = source.readUtf8();
        System.out.println(s);
        source.close();
    }

大体流程:

  1. 构建缓冲区,缓冲源对象
  2. 读写操作
  3. 关闭缓冲池

Sink和Source

在JDK里面有InputStream和OutputStream两个接口, Source和Sink类似于InputStream和OutoputStream,是io操作的顶级接口类,这两个接口均实现了Closeable接口。所以可以把Source简单的看成InputStream,Sink简单看成OutputStream。

在这里插入图片描述

其中Sink代表的是输出流,Source代表的是输入流:

Sink

还是先看官网介绍:

	Receives a stream of bytes. Use this interface to write data wherever it's
  needed: to the network, storage, or a buffer in memory. Sinks may be layered
  to transform received data, such as to compress, encrypt, throttle, or add
  protocol framing.

大概翻译:
接收字节流。使用此接口可以在任何需要写入数据的地方写入数据:网络,储存或内存中的缓冲区。Sink可能分层的去转换接收到的数据,像压缩,加密,限制或添加协议框架。

和OutputStream的比较:

		{@code OutputStream} requires multiple layers when emitted data is
  heterogeneous: a {@code DataOutputStream} for primitive values, a {@code
  BufferedOutputStream} for buffering, and {@code OutputStreamWriter} for
  charset encoding. This class uses {@code BufferedSink} for all of the above.
 
		Sink is also easier to layer: there is no {@linkplain
  java.io.OutputStream#write(int) single-byte write} method that is awkward to
  implement efficiently.

OutputStream在发出数据的是混合的时候,,需要多层:DataOutputStream作为原始的值,BufferedOutputStream用于缓冲,OutputStream用于编码。而Sink使用BufferedSink综合了上面所有。

Sink也更加容易分层,它没有OutputStream.write(int),这种很难有效实施的对单字节写入的方法。

public interface Sink extends Closeable, Flushable {
  /** Removes {@code byteCount} bytes from {@code source} and appends them to this. */
  //定义基础的write操作,该方法将字节写入Buffer
  void write(Buffer source, long byteCount) throws IOException;

  /** Pushes all buffered bytes to their final destination. */
  @Override void flush() throws IOException;

  /** Returns the timeout for this sink. */
  Timeout timeout();

  /**
   * Pushes all buffered bytes to their final destination and releases the
   * resources held by this sink. It is an error to write a closed sink. It is
   * safe to close a sink more than once.
   */
  @Override void close() throws IOException;
}

Source

还是先看官网介绍:


  		Supplies a stream of bytes. Use this interface to read data from wherever
  it's located: from the network, storage, or a buffer in memory. Sources may
  be layered to transform supplied data, such as to decompress, decrypt, or
  remove protocol framing.

提供字节流,使用此接口从任何地方读取数据:从网络,存储,或者内存中的缓冲区。Source可能分层以转换提供的数据,像解压缩,解密或移除协议框架。

和InputStream的比较:

{@code InputStream} requires multiple layers when consumed data is
  heterogeneous: a {@code DataInputStream} for primitive values, a {@code
  BufferedInputStream} for buffering, and {@code InputStreamReader} for
  strings. This class uses {@code BufferedSource} for all of the above.
 
  Source avoids the impossible-to-implement {@linkplain
  java.io.InputStream#available available()} method. Instead callers specify
  how many bytes they {@link BufferedSource#require require}.
 
 Source omits the unsafe-to-compose {@linkplain java.io.InputStream#mark
 mark and reset} state that's tracked by {@code InputStream}; instead, callers
  just buffer what they need.
 
 When implementing a source, you don't need to worry about the {@linkplain
   java.io.InputStream#read single-byte read} method that is awkward to implement efficiently
  and returns one of 257 possible values.
 
 And source has a stronger {@code skip} method: {@link BufferedSource#skip}
  won't return prematurely.
 

简单来说,Source对于混合数据,不用考虑它的分层,因为它综合了,Source不能使用InputStream中的available()方法,而是用require去指定需要多少字节;Sourse省略了不安全写入的InputStream的mark方法,相反,只是缓冲呼叫着需要的东西;Source不用担心读取一个字节的时候,InputStream可能会返回 256个值,Source使用的sikp方法,不会过早的返回。

public interface Source extends Closeable {
  /**
   * Removes at least 1, and up to {@code byteCount} bytes from this and appends
   * them to {@code sink}. Returns the number of bytes read, or -1 if this
   * source is exhausted.
   */
    // 定义基础的read操作,该方法将字节写入Buffer
  long read(Buffer sink, long byteCount) throws IOException;

  /** Returns the timeout for this source. */
  Timeout timeout();

  /**
   * Closes this source and releases the resources held by this source. It is an
   * error to read a closed source. It is safe to close a source more than once.
   */
  @Override void close() throws IOException;
}

Sink和Source只是定义了很少的方法,一般在使用的过程中,不会直接用它,而是用BufferedSink和BufferedSource这两个接口,它们有对Sink和Source进行了封装,它们的实现类是RealBufferedSink和RealBufferedSource

RealBufferedSink

final class RealBufferedSink implements BufferedSink {
  public final Buffer buffer = new Buffer();
  public final Sink sink;
  boolean closed;

  RealBufferedSink(Sink sink) {
    if (sink == null) throw new NullPointerException("sink == null");
    this.sink = sink;
  }

看源码可知ReadlBufferedSink类中有两个主要参数,一个是新建的Buffer对象,一个是Sink对象。虽然这个类叫做ReadlBufferedSink,但是实际上这个只是保存一个Buffer独享的代理而已,真正的实现都在Buffer中实现的。

@Override public void write(Buffer source, long byteCount)
      throws IOException {
    if (closed) throw new IllegalStateException("closed");
    buffer.write(source, byteCount);
    emitCompleteSegments();
  }

  @Override public BufferedSink write(ByteString byteString) throws IOException {
    if (closed) throw new IllegalStateException("closed");
    buffer.write(byteString);
    return emitCompleteSegments();
  }

这个Buffer类,等会分析。
刚才我们可以看到Sink接口中有一个Time类,这个就是Okoio实现超时机制的接口,用于包装IO操作的稳定性。

Segment和segmentPool

Segment

Segment字面的意思就是片段,Okio将数据也就是Buffer分割成一块块的片段,内部维护固定长度的byte[ ]数组,同时Segment拥有前面节点和后面节点,构成了一个双向循环链表:

在这里插入图片描述

这样采取分片使用链表连接,片中使用数组储存,兼具读的连续性和写的可插入性,对比单一使用链表或者数组,是一种这种的方案,读写更快,而且有一个好处,根据需求改动分片的大小来权衡读写的业务操作,另外,segment也有一些内置IDE优化操作:

还是先看官网描述:


  A segment of a buffer.
  一个buffer的片段
  
 Each segment in a buffer is a circularly-linked list node referencing the following and
  preceding segments in the buffer.
  每个片段是双向循环连接的一个节点
 
Each segment in the pool is a singly-linked list node referencing the rest of segments in the
pool.
 每个segment都是唯一的
 
 	The underlying byte arrays of segments may be shared between buffers and byte strings. When a
segment's byte array is shared the segment may not be recycled, nor may its byte data be changed.
The lone exception is that the owner segment is allowed to append to the segment, writing data at
{@code limit} and beyond. There is a single owning segment for each byte array. Positions,
 limits, prev, and next references are not shared.
 segments的byte数组可以在buffers和字节串之间共享。当一个segment的byte数组是共享的,
 那么这个segment是不可回收的,也不能改变byte数组的数据。除非byte数组的所有者是自己
// 每一个segment所含数据的大小,固定的
    static final int SIZE = 8192;
     // Segments 用分享的方式避免复制数组
    static final int SHARE_MINIMUM = 1024;
  
    final byte[] data;
     // data[]中第一个可读的位置
    int pos;
     // data[]中第一个可写的位 
     //所以一个Segment的可读数据数量为pos~limit-1=limit-pos;limit和pos的有效值为0~SIZE-1
    int limit;
    //当前存储的data数据是其它对象共享的则为真  
    boolean shared;
    //是否是自己是操作者 
    boolean owner;
    ///前一个Segment
    Segment pre;
    ///下一个Segment
    Segment next;

总结一下就是:
SIZE就是一个segment的最大字节数,其中还有一个SHARE_MINIMUM,这个涉及到segment优化的另一个技巧,共享内存,然后data就是保存的字节数组,pos,limit就是开始和结束点的index,shared和owner用来设置状态判断是否可读写,一个有共享内存的sement是不能写入的,pre,next就是前置后置节点。

构造方法:

Segment() {  
  this.data = new byte[SIZE];  
  this.owner = true;   
  this.shared = false; 
}  

Segment(byte[] data, int pos, int limit) {  
  this.data = data;  
  this.pos = pos;  
  this.limit = limit;  
  this.owner = false; 
  this.shared = true;  
}

  1. 无参构造,采用该构造器表明该数据data的所有者就是该Segment自己,所以owner为真,shared为假。
  2. 有参数的构造,表明数据是直接使用外面的,所以shared为真,owner为假
Segment的几个方法分析
  1. pop()方法:
    当前Segment从Segment链中移除,返回下一个Segment:
public Segment pop(){
        Segment result = next != this ? next : null;
        pre.next = next;
        next.pre = pre;
        next = null;
        pre = null;
        return result;
    }

  1. push方法:
    将一个Segment压入该Segment节点的后面,返回刚刚压入的Segment:
public Segment push(Segment segment){
        segment.pre = this;
        segment.next = next;
        next.pre = segment;
        next = segment;
        return segment;
    }

  1. writeTo方法:
/** Moves {@code byteCount} bytes from this segment to {@code sink}. */
//从该segment取出byteCount长的数据到sink
  public void writeTo(Segment sink, int byteCount) {
     //只能对自己操作
    if (!sink.owner) throw new IllegalArgumentException();
     //写的起始位置加上需要写的byteCount大于SIZE
    if (sink.limit + byteCount > SIZE) {
      // We can't fit byteCount bytes at the sink's current position. Shift sink first.
       //如果是共享内存,不能写,则抛异常
      if (sink.shared) throw new IllegalArgumentException();
      //如果不是共享内存,且写的起始位置加上byteCount减去头还大于SIZE则抛异常
      if (sink.limit + byteCount - sink.pos > SIZE) throw new IllegalArgumentException();
      //上述条件都不满足,我们需要先触动要写文件的地址,
      //把sink.data从sink.pos这个位置移动到 0这个位置,移动的长度是limit - sink.pos,
      //这样的好处就是sink初始位置是0
      System.arraycopy(sink.data, sink.pos, sink.data, 0, sink.limit - sink.pos);
      sink.limit -= sink.pos;
      sink.pos = 0;
    }
    //开始尾部写入 写完置limit地址
    System.arraycopy(data, pos, sink.data, sink.limit, byteCount);
    sink.limit += byteCount;
    pos += byteCount;//索引后移
  }

由于签名的pos索引可能因为read方法读出数据,所以pos后移,所以先执行移动操作,让pos移动到0的位置,更改pos和limit后,再在尾部写入byteCount长的数据,写完之后,实际上本segment读了byteCount长的数据,所以pos需要后移。

PS:这里我们来复习一下arraycopy方法:
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
src:源数组; srcPos:源数组要复制的起始位置;dest:目的数组; destPos:目的数组放置的起始位置;length:复制的长度。
实现过程是这样的,先生成一个长度为length的临时数组,将圆数组中srcPos 到scrPos+lengh-1之间的数据拷贝到临时数组中。然后将这个临时数组复制到dest中。

compact方法(压缩机制)

除了写入数据之外,segment还有一个优化的技巧,因为每个segment的片段的size是固定的,为了防止经过长时间的使用后,每个segment中的数据被分割的十分严重,可能一个很小的数据却占据了整个segment,所以有了一个压缩机制。

public void compact() {
    //上一个节点就是自己,意味着就一个节点,无法压缩,抛异常
    if (prev == this) throw new IllegalStateException();
     //如果上一个节点不是自己的,所以你是没有权利压缩的
    if (!prev.owner) return; // Cannot compact: prev isn't writable.
    //能进来说明,存在上一个节点,且上一个节点是自己的,可以压缩
    //记录当前Segment具有的数据,数据大小为limit-pos
    int byteCount = limit - pos;
    统计前结点是否被共享,如果共享则只记录Size-limit大小,如果没有被共享,则加上pre.pos之前的空位置;
    //本行代码主要是获取前一个segment的可用空间。先判断prev是否是共享的,如果是共享的,则只记录SIZE-limit,如果没有共享则记录SIZE-limit加上prev.pos之前的空位置
    int availableByteCount = SIZE - prev.limit + (prev.shared ? 0 : prev.pos);
   //判断prev的空余空间是否能够容纳Segment的全部数据,不能容纳则返回
    if (byteCount > availableByteCount) return; // Cannot compact: not enough writable space.
    //能容纳则将自己的这个部分数据写入上一个Segment
    writeTo(prev, byteCount);
    //讲当前Segment从Segment链表中移除
    pop();
    //回收该Segment
    SegmentPool.recycle(this);
  }

split方法(共享机制)

还有一种机制就是共享机制,为了减少数据复制带来的性能开销。
本方法用于将Segment一分为二,将pos+1— —pos+byteCount-1的内容给新的Segement,将pos+byteCount——limit-1的内容留给自己,然后将前面的新的Segment插入到自己的前面。这里需要注意的是,虽然这里变成了Segment,但是实际上byte[ ]数据并没有被拷贝,两个Segment都引用了该Segment。

public Segment split(int byteCount) {
    if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();
    Segment prefix;

    // We have two competing performance goals:
    //  - Avoid copying data. We accomplish this by sharing segments.
    //  - Avoid short shared segments. These are bad for performance because they are readonly and
    //    may lead to long chains of short segments.
    // To balance these goals we only share segments when the copy will be large.
    if (byteCount >= SHARE_MINIMUM) {
      prefix = new Segment(this);
    } else {
      prefix = SegmentPool.take();
      System.arraycopy(data, pos, prefix.data, 0, byteCount);
    }

    prefix.limit = prefix.pos + byteCount;
    pos += byteCount;
    prev.push(prefix);
    return prefix;
  }

SegmentPool

SegmentPool是一个Segment吃,由一个单链表构成。该池负责Segment的回收和闲置Segment管理,也就是说Buffer使用的Segment是从Segment单链表中取出,这样有效的避免了GC频率
先看一下它的属性:

//一个Segment记录的最大长度是8192,因此SegmentPool只能存储8个Segment
static final long MAX_SIZE = 64 * 1024;
//该SegmentPool存储了一个回收Segment的链表
static Segment next;
//该值记录了当前所有Segment的总大小,最大值是为MAX_SIZE
static long byteCount;

SegmentPool的方法十分简单,就两个,一个是取,一个是收。

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.
  }

这里的next表示整个对象吃的头,而不是下一个。如果next为null,则池子里面没有Segment。否则就从里面拿出一个next出来,并next的下一个节点赋值为next,然后设置一下byteCount。如果链表为空,就创建一个Segment对象返回。

static void recycle(Segment segment) {
    //如果这个要回收的Segment被前后引用,则无法回收
    if (segment.next != null || segment.prev != null) throw new IllegalArgumentException();
    //如果这个要回收的Segment的数据是分享的,则无法回收
    if (segment.shared) return; // This segment cannot be recycled.
    //加锁
    synchronized (SegmentPool.class) {
      //如果 这个空间已经不足以再放入一个空的Segment,则不回收
      if (byteCount + Segment.SIZE > MAX_SIZE) return; // Pool is full.
      //设置SegmentPool的池大小
      byteCount += Segment.SIZE;
      //segment的下一个指向头
      segment.next = next;
      //设置segment的可读写位置为0
      segment.pos = segment.limit = 0;
      //设置当前segment为头
      next = segment;
    }
  }

SegmentPool总结:
1.大家注意到没有SegmentPool的作用就是管理多余的Segment,不直接丢弃废弃的Segment,等客户需要Segment的时候直接从池中获取一个对象,避免了重复创建新兑现,提高资源利用率。
2.大家仔细读取源码会发现SegmentPool的所有操作都是基于对表头的操作。

Buffer

Buffer简介
  1. Buffer存储额是可变比特序列,需要注意的是Buffer内部对比特数据的存储不是直接使用一个byte数组那么简单,它是使用Segment进行存储,这样的话,Buffer的容量就可以动态扩展,从序列的尾部存入数据,从序列的头部读取数据。Buffer实现了BufferSource和BufferSink接口。
  2. Buffer其实是整个读写的核心。

还是先看官网描述:


  A collection of bytes in memory.
  内存中bytes的集合
 
 Moving data from one buffer to another is fast. Instead
 of copying bytes from one place in memory to another, this class just changes
 ownership of the underlying byte arrays.
  移动数据,通过改变它的所有者而替代copy array
 
 This buffer grows with your data. Just like ArrayList,
 each buffer starts small. It consumes only the memory it needs to.
 此buffer随data增长。每个buffer开始都很小,它只消耗它所需要的内存
 
 This buffer pools its byte arrays. When you allocate a
 byte array in Java, the runtime must zero-fill the requested array before
 returning it to you. Even if you're going to write over that space anyway.
 This class avoids zero-fill and GC churn by pooling byte arrays.
 在Java中,申请一个byte数组的时候,运行的时候需要先填充再给你。即使你打算写。
 这个类使用byte pool 来避免0填充和GC

Buffer的属性及实现接口:

public final class Buffer implements BufferedSource, BufferedSink, Cloneable {
  //明显是给16进制准备的
  private static final byte[] DIGITS =
      { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
  static final int REPLACEMENT_CHARACTER = '\ufffd';
  //Buffer存储了一个这样的head节点,这就是Buffer对数据的存储结构。
  //字节数组都是交给Segment进行管理
  Segment head;
  //当前存储的数据大小
  long size;

clone接口

Java中,拷贝分为深拷贝和浅拷贝,这里使用的是深拷贝:

 /** Returns a deep copy of this buffer. */
  @Override public Buffer clone() {
    Buffer result = new Buffer();
    //如果没有数据,就直接返回
    if (size == 0) return result;

    result.head = head.sharedCopy();
    result.head.next = result.head.prev = result.head;
    .//遍历整个Segment链
    for (Segment s = head.next; s != head; s = s.next) {
      result.head.prev.push(s.sharedCopy());
    }
    result.size = size;
    return result;
  }
  
Segment sharedCopy() {
    shared = true;
    return new Segment(data, pos, limit, true, false);
  }
BufferedSource,BufferedSink接口

只挑几个看:

@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;
  }

第一步:做了一次验证,因为一个int数据的字节数是4,所以必须保证当前Buffer的size大于4.
第二步:如果当前的Segment所包含的字节数小于4,那么还需要去另外一个Segment中获取一部分数据,因此通过调用readByte()方法一个字节一个字节的读取
第三步:如果当前的Segment的数据够用,因此直接从pos位置开始读取4个字节的数据,然后转换为int数据。
第四步:如果pos==limit,那么当前Segment中就没有数据了,因此就将该Segment从双向链表中移除,并回收该Segment。如果还有数据,就重新设置pos

 @Override public byte readByte() {
    if (size == 0) throw new IllegalStateException("size == 0");

    Segment segment = head;
    int pos = segment.pos;
    int limit = segment.limit;

    byte[] data = segment.data;
    byte b = data[pos++];
    size -= 1;

    if (pos == limit) {
      head = segment.pop();
      SegmentPool.recycle(segment);
    } else {
      segment.pos = pos;
    }

    return b;
  }

只读取一个字节的数据。

@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;
  }

我们可以看到,过程和readInt是差不多的。

然后我们来看看写的方法:

@Override
  public Buffer writeString(String string, int beginIndex, int endIndex, Charset charset) {
    if (string == null) throw new IllegalArgumentException("string == null");
    if (beginIndex < 0) throw new IllegalAccessError("beginIndex < 0: " + beginIndex);
    if (endIndex < beginIndex) {
      throw new IllegalArgumentException("endIndex < beginIndex: " + endIndex + " < " + beginIndex);
    }
    if (endIndex > string.length()) {
      throw new IllegalArgumentException(
          "endIndex > string.length: " + endIndex + " > " + string.length());
    }
    if (charset == null) throw new IllegalArgumentException("charset == null");
    if (charset.equals(Util.UTF_8)) return writeUtf8(string, beginIndex, endIndex);
    byte[] data = string.substring(beginIndex, endIndex).getBytes(charset);
    return write(data, 0, data.length);
  }

这个方法中,在做了一些判断后,如果字符编码集是UTF-8,那么就调用writeUtf8()方法,否则就将字符串截断,通过编码集得到字节数组,调用write()方法:

@Override public Buffer writeShort(int s) {
    //第一步
    Segment tail = writableSegment(2);
    //第二步
    byte[] data = tail.data;
    int limit = tail.limit;
    data[limit++] = (byte) ((s >>> 8) & 0xff);
    data[limit++] = (byte)  (s        & 0xff);
    tail.limit = limit;
    //第三步
    size += 2;
    //第四步
    return this;
  }

  /**
   * Returns a tail segment that we can write at least {@code minimumCapacity}
   * bytes to, creating it if necessary.
   */
  Segment writableSegment(int minimumCapacity) {
    if (minimumCapacity < 1 || minimumCapacity > Segment.SIZE) throw new IllegalArgumentException();
    //如果hea=null表明Buffer里面一个Segment都没有
    if (head == null) {
      //从SegmentPool取出一个Segment 
      head = SegmentPool.take(); // Acquire a first segment.
      return head.next = head.prev = head;
    }
    //如果head不为null.
    Segment tail = head.prev;
    //如果已经写入的数据+最小可以写入的空间超过限制,则在SegmentPool里面取一个
    if (tail.limit + minimumCapacity > Segment.SIZE || !tail.owner) {
      tail = tail.push(SegmentPool.take()); // Append a new empty segment to fill up.
    }
    return tail;
  }

writeShort用来给Buffer写入一个short数据
第一步,通过writeableSegment拿到一个能有2个字节的空间的segment
第二步,tail中的data就是字节数组,limit则是数据的尾部索引,写数据就是在尾部继续写,直接设置在data通过limit自增后的index,然后重置尾部索引.
第三步,size+2
第四步,返回tail

Okio中的超时机制

TimeOut

Okio的超时机制让I/O操作不会因为一场阻塞在某个未知的错误上,okio的基础超时机制是采取同步超时
以输出流Sink为例子,当我们使用下面的方法包装流的时候:

//在okio中
//实际上调用的两个参数的sink方法,第二个参数是new的TimeOut对象,即同步超时
public static Sink sink(OutputStream out) {
    return sink(out, new Timeout());
  }

  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 + ")";
      }
    };
  }

可以看到write方法里中实际上有一个while循环,在每个开始写的时候就调用了timeout.throwIfReached()方法,我们推断这个方法里面做了时间是否超时的判断,如果超时了,应该throw一个exception出来。这很明显是一个同步超时机制,按序执行。同理Source也是一样,那么咱们看下他里面到底是怎么执行的

 public void throwIfReached() throws IOException {
    if (Thread.interrupted()) {
      throw new InterruptedIOException("thread interrupted");
    }

    if (hasDeadline && deadlineNanoTime - System.nanoTime() <= 0) {
      throw new InterruptedIOException("deadline reached");
    }
  }

在okio中,还可以看到对socket的封装:

/**
   * Returns a sink that writes to {@code socket}. Prefer this over {@link
   * #sink(OutputStream)} because this method honors timeouts. When the socket
   * write times out, the socket is asynchronously closed by a watchdog thread.
   */
//返回一个sink去往socket中写数据。
//当socket写数据的时候超时了,socket会通过一个watch dog thread异步的关闭
 public static Sink sink(Socket socket) throws IOException {
        if (socket == null) {
            throw new IllegalArgumentException("socket == null");
        } else if (socket.getOutputStream() == null) {
            throw new IOException("socket's output stream == null");
        } else {
            AsyncTimeout timeout = timeout(socket);
            Sink sink = sink((OutputStream)socket.getOutputStream(), (Timeout)timeout);
            return timeout.sink(sink);
        }
    }

我们可以看到AsyncTimeout ,它又是什么呢?

AsyncTimeout

这个实际上继承于TimeOut所实现的一个异步超时类,这个异步类比同步要复杂的多,它使用了人一个WatchDog线程在后台进行监听超时。
这里面用到了Watchdog,Watchdog是AsyncTimeout的静态内部类

public class AsyncTimeout extends Timeout

那么watch dog又是什么?


  private static final class Watchdog extends Thread {
    Watchdog() {
      super("Okio Watchdog");
      setDaemon(true);
    }

    public void run() {
      while (true) {
        try {
          AsyncTimeout timedOut;
          synchronized (AsyncTimeout.class) {
            timedOut = awaitTimeout();

            // Didn't find a node to interrupt. Try again.
            if (timedOut == null) continue;

            // The queue is completely empty. Let this thread exit and let another watchdog thread
            // get created on the next call to scheduleTimeout().
            if (timedOut == head) {
              head = null;
              return;
            }
          }

          // Close the timed out node.
          timedOut.timedOut();
        } catch (InterruptedException ignored) {
        }
      }
    }
  }

里面的run方法执行的就是超时的判断,之所以在socket写时采取异步超时,这完全是由socket自身的性质决定的,socket经常会阻塞自己,导致下面的事情执行不了。AsyncTimeout继承Timeout类,可以复写里面的timeout方法,这个方法会在watchdao的线程中调用,所以不能执行长时间的操作,否则就会引起其他的超时。

下面分析AsuyncTimeout

//不要一次写超过64k的数据否则可能会在慢连接中导致超时
    private static final int TIMEOUT_WRITE_SIZE = 64 * 1024;
    private static AsyncTimeout head;
    private boolean inQueue;
    private AsyncTimeout next;
    private long timeoutAt;

首先就是一个最大的写值,定义为64K,刚好和一个Buffer大小一样。官方的解释是如果连续读写超过这个数字的字节,那么及其容易导致超时,所以为了限制这个操作,直接给出了一个能写的最大数。
下面两个参数head和next,很明显表明这是一个单链表,timeoutAt则是超时时间。使用者在操作之前首先要调用enter()方法,这样相当于注册了这个超时监听,然后配对的实现exit()方法。这样exit()有一个返回值会表明超时是否出发,注意:这个timeout是异步的,可能会在exit()后才调用

public final void enter() {
    if (inQueue) throw new IllegalStateException("Unbalanced enter/exit");
    long timeoutNanos = timeoutNanos();
    boolean hasDeadline = hasDeadline();
    if (timeoutNanos == 0 && !hasDeadline) {
      return; // No timeout and no deadline? Don't bother with the queue.
    }
    inQueue = true;
    scheduleTimeout(this, timeoutNanos, hasDeadline);
  }

这里只是判断了inQueue的状态,然后设置inQueue的状态,真正的调用是在scheduleTimeout()里面,那我们进去看下

private static synchronized void scheduleTimeout(
      AsyncTimeout node, long timeoutNanos, boolean hasDeadline) {
    // Start the watchdog thread and create the head node when the first timeout is scheduled.
    //head==null,表明之前没有,本次是第一次操作,开启Watchdog守护线程
    if (head == null) {
      head = new AsyncTimeout();
      new Watchdog().start();
    }

    long now = System.nanoTime();
    //如果有最长限制(hasDeadline我翻译为最长限制),并且超时时长不为0
    if (timeoutNanos != 0 && hasDeadline) {
      // Compute the earliest event; either timeout or deadline. Because nanoTime can wrap around,
      // Math.min() is undefined for absolute values, but meaningful for relative ones.
      //对比最长限制和超时时长,选择最小的那个值
      node.timeoutAt = now + Math.min(timeoutNanos, node.deadlineNanoTime() - now);
    } else if (timeoutNanos != 0) {
      //如果没有最长限制,但是超时时长不为0,则使用超时时长
      node.timeoutAt = now + timeoutNanos;
    } else if (hasDeadline) {
      //如果有最长限制,但是超时时长为0,则使用最长限制
      node.timeoutAt = node.deadlineNanoTime();
    } else {
     //如果既没有最长限制,和超时时长,则抛异常
      throw new AssertionError();
    }

    // Insert the node in sorted order.
    long remainingNanos = node.remainingNanos(now);
    for (AsyncTimeout prev = head; true; prev = prev.next) {
      //如果下一个为null或者剩余时间比下一个短 就插入node
      if (prev.next == null || remainingNanos < prev.next.remainingNanos(now)) {
        node.next = prev.next;
        prev.next = node;
        if (prev == head) {
          AsyncTimeout.class.notify(); // Wake up the watchdog when inserting at the front.
        }
        break;
      }
    }
  }

上面可以看出这个链表实际上是按照剩余的超时时间来进行排序的,快到超时的节点排在表头,一次往后递增。我们以一个read的代码来看整个超时的绑定过程。

@Override
      public long read(Buffer sink, long byteCount) throws IOException {
            boolean throwOnTimeout = false;
            enter();
             try {
                 long result = source.read(sink,byteCount);
                 throwOnTimeout = true;
                 return result;
             }catch (IOException e){
                 throw exit(e);
             }finally {
                 exit(throwOnTimeout);
             }
      }

首先调用enter方法,然后去做读的操作,这里可以看到不仅在catch上而且是在finally中也做了操作,这样一场和正常的情况都考虑到了,在exit中调用了真正的exit方法,exit中会判断这个异步超时对象是否在链表中:

final void exit(boolean throwOnTimeout) throws IOException {
        boolean timeOut =  exit();
        if (timeOut && throwOnTimeout)
            throw newTimeoutException(null);
    }

    public final boolean exit(){
        if (!inQueue)
            return false;
        inQueue = false;
        return cancelScheduledTimeout(this);
    }

回到前面说的的WatchDog,内部的run方法是一个while(true)的一个死循环,由于在while(true)里面锁住了内部的awaitTimeout的操作,这个await正是判断是否超时的真正地方。

static AsyncTimeout awaitTimeout() throws InterruptedException {
        //拿到下一个节点
        AsyncTimeout node = head.next;
        //如果queue为空,等待直到有node进队,或者触发IDLE_TIMEOUT_MILLS
        if (node == null) {
            long startNanos = System.nanoTime();
            AsyncTimeout.class.wait(IDLE_TIMEOUT_MILLS);//等待60s
            return head.next == null && (System.nanoTime() - startNanos) >= IDLE_TIMEOUT_NANOS ? head : null;
        }
        long waitNanos = node.remainingNanos(System.nanoTime());
        //这个head依然还没有超时,继续等待
        if (waitNanos > 0) {
            long waitMills = waitNanos / 1000000L;
            waitNanos -= (waitMills * 1000000L);
            AsyncTimeout.class.wait(waitMills, (int) waitNanos);
            return null;
        }
        //返回超时的head
        head.next = node.next;
        node.next = null;
        return node;
    }

这里就比较清晰了,主要就是通过这个remainNanos来判断预定的超时时间减去当前时间是否大于0,如果比0大就说明还没超时,于是wait剩余的时间,然后表示没有超时,如果小于0,就会把这个从链表中移除,根据前面的exit方法中的判断就能触发整个超时的方法。

所以Buffer的写操作,实际上就是不断增加Segment的一个过程,读操作,就是不断消耗Segment中的数据,如果数据读取完,则使用SegmentPool进行回收。Buffer更多的逻辑主要是跨Segment读取数据,需要把前一个Segment的前端拼接在一起,因此看起来代码相对很多,但是其实开销非常低

okio的高效方便之处

  1. 它对数据进行了分块处理,这样在大数据IO的时候可以以块为单位进行IO,这样可以提高IO的吞吐率。
  2. 它对这些数据块使用链表来进行管理,这可以仅通过移动指针就进行数据的管理,而不用真正的处理数据,而且对扩容来说十分方便.
  3. 闲置的块进行管理,通过一个块池(SegmentPool)的管理,避免系统GC和申请byte时的zero-fill。其他的还有一些小细节上的优化,比如如果你把一个UTF-8的String转化为ByteString,ByteString会保留一份对原来String的引用,这样当你下次需要decode这个String时,程序通过保留的引用直接返回对应的String,从而避免了转码过程。
  4. 他为所有的Source、Sink提供了超时操作,这是在Java原生IO操作是没有的。
  5. okio它对数据的读写都进行了封装,调用者可以十分方便的进行各种值(Stringg,short,int,hex,utf-8,base64等)的转化。

本文来源:https://www.jianshu.com/p/f5941bcf3a2d

猜你喜欢

转载自blog.csdn.net/qq_36391075/article/details/82818603