浅析okio的架构和源码实现

Okhttp的浅层架构分析
Okhttp的责任链模式和拦截器分析
Okhttp之RetryAndFollowUpInterceptor拦截器分析
Okhttp之BridgeInterceptor拦截器分析
Okhttp之CacheInterceptor拦截器分析
Okhttp之ConnectInterceptor拦截器分析
Okhttp之网络连接相关三大类RealConnection、ConnectionPool、StreamAllocation

最近研究了Okhttp的源码,看到最后发现了okio的身影,之前一直对io流概念模糊,这次决定好好啃一啃这一块的知识点,弄清楚io的整体脉络。
看了okio的评价,很多人都说它比原生的io更高效,代码更简洁,使用起来更方便,缓存设计更合理,带着这些疑惑问题来研究研究它是怎么做到的。

先来看看一般的文件copy操作,原生的io一般用到缓存BufferedInputStream类来实现:

       //读取文件(缓存字节流)
        val inPutStream = BufferedInputStream(FileInputStream("1.txt"))
        //写入相应的文件
        val outPutStream = BufferedOutputStream(FileOutputStream("2.txt"))
        //读取数据
        //一次性取多少字节
        val bytes = ByteArray(2048)
        //接受读取的内容(n就代表的相关数据,只不过是数字的形式)
        var n = -1
        //循环取出数据
        while (inPutStream.read(bytes, 0, bytes.size).also { n = it } != -1) {
            //写入相关文件
            outPutStream.write(bytes, 0, n)
        }
        //flush缓存
        outPutStream.flush()
        //关闭流
        inPutStream.close()
        outPutStream.close()

再来看看BufferedInputStream和BufferedOutputStream的源码,他们是怎么实现缓存策略的?


public class BufferedInputStream extends FilterInputStream {  
 
    // 默认的缓冲大小是8192字节 
    // BufferedInputStream 会根据“缓冲区大小”来逐次的填充缓冲区; 
    // 即,BufferedInputStream填充缓冲区,用户读取缓冲区,读完之后,BufferedInputStream会再次填充缓冲区。如此循环,直到读完数据... 
    private static int defaultBufferSize = 8192;  
 
    // 缓冲数组 
    protected volatile byte buf[];  
 
    // 缓存数组的原子更新器。 
    // 该成员变量与buf数组的volatile关键字共同组成了buf数组的原子更新功能实现, 
    // 即,在多线程中操作BufferedInputStream对象时,buf和bufUpdater都具有原子性(不同的线程访问到的数据都是相同的) 
    private static final 
        AtomicReferenceFieldUpdater bufUpdater =  
        AtomicReferenceFieldUpdater.newUpdater  
        (BufferedInputStream.class,  byte[].class, "buf");  
 
    // 当前缓冲区的有效字节数。 
    // 注意,这里是指缓冲区的有效字节数,而不是输入流中的有效字节数。 
    protected int count;  
 
    // 当前缓冲区的位置索引 
    // 注意,这里是指缓冲区的位置索引,而不是输入流中的位置索引。 
    protected int pos;  
 
    // 当前缓冲区的标记位置 
    // markpos和reset()配合使用才有意义。操作步骤: 
    // (01) 通过mark() 函数,保存pos的值到markpos中。 
    // (02) 通过reset() 函数,会将pos的值重置为markpos。接着通过read()读取数据时,就会从mark()保存的位置开始读取。 
    protected int markpos = -1;  
 
    // marklimit是标记的最大值。 
    // 关于marklimit的原理,我们在后面的fill()函数分析中会详细说明。这对理解BufferedInputStream相当重要。 
    protected int marklimit;  
 
    // 获取输入流 
    private InputStream getInIfOpen() throws IOException {  
        InputStream input = in;  
        if (input == null)  
            throw new IOException("Stream closed");  
        return input;  
    }  
 
    // 获取缓冲 
    private byte[] getBufIfOpen() throws IOException {  
        byte[] buffer = buf;  
        if (buffer == null)  
            throw new IOException("Stream closed");  
        return buffer;  
    }  
 
    // 构造函数:新建一个缓冲区大小为8192的BufferedInputStream 
    public BufferedInputStream(InputStream in) {  
        this(in, defaultBufferSize);  
    }  
 
    // 构造函数:新建指定缓冲区大小的BufferedInputStream 
    public BufferedInputStream(InputStream in, int size) {  
        super(in);  
        if (size <= 0) {  
            throw new IllegalArgumentException("Buffer size <= 0");  
        }  
        buf = new byte[size];  
    }  
 
    // 从“输入流”中读取数据,并填充到缓冲区中,正是这个方法使得数据得到提前缓存
    // 至于它的具体代码实现,这里不深究,先把他理解成一个快速获取缓存数据的填充方法
    private void fill() throws IOException {  
        byte[] buffer = getBufIfOpen();  
        if (markpos < 0)  
            pos = 0;            /* no mark: throw away the buffer */ 
        else if (pos >= buffer.length)  /* no room left in buffer */ 
            if (markpos > 0) {  /* can throw away early part of the buffer */ 
                int sz = pos - markpos;  
                System.arraycopy(buffer, markpos, buffer, 0, sz);  
                pos = sz;  
                markpos = 0;  
            } else if (buffer.length >= marklimit) {  
                markpos = -1;   /* buffer got too big, invalidate mark */ 
                pos = 0;        /* drop buffer contents */ 
            } else {            /* grow buffer */ 
                int nsz = pos * 2;  
                if (nsz > marklimit)  
                    nsz = marklimit;  
                byte nbuf[] = new byte[nsz];  
                System.arraycopy(buffer, 0, nbuf, 0, pos);  
                if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {  
                    throw new IOException("Stream closed");  
                }  
                buffer = nbuf;  
            }  
        count = pos;  
        int n = getInIfOpen().read(buffer, pos, buffer.length - pos);  
        if (n > 0)  
            count = n + pos;  
    }  
 
    // 读取下一个字节 
    public synchronized int read() throws IOException {  
        // 若已经读完缓冲区中的数据,则调用fill()从输入流读取下一部分数据来填充缓冲区 
        if (pos >= count) {  
            fill();  
            if (pos >= count)  
                return -1;  
        }  
        // 从缓冲区中读取指定的字节 
        return getBufIfOpen()[pos++] & 0xff;  
    }  
 
    // 将缓冲区中的数据写入到字节数组b中。off是字节数组b的起始位置,len是写入长度 
    private int read1(byte[] b, int off, int len) throws IOException {  
        int avail = count - pos;  
        if (avail <= 0) {  
            // 加速机制。 
            // 如果读取的长度大于缓冲区的长度 并且没有markpos, 
            // 则直接从原始输入流中进行读取,从而避免无谓的COPY(从原始输入流至缓冲区,读取缓冲区全部数据,清空缓冲区,  
            //  重新填入原始输入流数据) 
            if (len >= getBufIfOpen().length && markpos < 0) {  
                return getInIfOpen().read(b, off, len);  
            }  
            // 若已经读完缓冲区中的数据,则调用fill()从输入流读取下一部分数据来填充缓冲区 
            fill();  
            avail = count - pos;  
            if (avail <= 0) return -1;  
        }  
        int cnt = (avail < len) ? avail : len;  
        //走到这里,则只是单纯的从缓存里复制数据
        System.arraycopy(getBufIfOpen(), pos, b, off, cnt);  
        pos += cnt;  
        return cnt;  
    }  
 
    // 将缓冲区中的数据写入到字节数组b中。off是字节数组b的起始位置,len是写入长度 
    public synchronized int read(byte b[], int off, int len)  
        throws IOException  
    {  
        getBufIfOpen(); // Check for closed stream 
        if ((off | len | (off + len) | (b.length - (off + len))) < 0) {  
            throw new IndexOutOfBoundsException();  
        } else if (len == 0) {  
            return 0;  
        }  
 
        // 读取到指定长度的数据才返回 
        int n = 0;  
        for (;;) {  
            int nread = read1(b, off + n, len - n);  
            if (nread <= 0)  
                return (n == 0) ? nread : n;  
            n += nread;  
            if (n >= len)  
                return n;  
            // if not closed but no bytes available, return 
            InputStream input = in;  
            if (input != null && input.available() <= 0)  
                return n;  
        }  
    }  
 
  
 
    // 下一个字节是否存可读 
    public synchronized int available() throws IOException {  
        int n = count - pos;  
        int avail = getInIfOpen().available();  
        return n > (Integer.MAX_VALUE - avail)  
                    ? Integer.MAX_VALUE  
                    : n + avail;  
    }  
 
    // 标记“缓冲区”中当前位置。 
    // readlimit是marklimit,关于marklimit的作用,参考后面的说明。 
    public synchronized void mark(int readlimit) {  
        marklimit = readlimit;  
        markpos = pos;  
    }  
 
    // 将“缓冲区”中当前位置重置到mark()所标记的位置 
    public synchronized void reset() throws IOException {  
        getBufIfOpen(); // Cause exception if closed 
        if (markpos < 0)  
            throw new IOException("Resetting to invalid mark");  
        pos = markpos;  
    }  
 
    public boolean markSupported() {  
        return true;  
    }  
 
    // 关闭输入流 
    public void close() throws IOException {  
        byte[] buffer;  
        while ( (buffer = buf) != null) {  
            if (bufUpdater.compareAndSet(this, buffer, null)) {  
                InputStream input = in;  
                in = null;  
                if (input != null)  
                    input.close();  
                return;  
            }  
            // Else retry in case a new buf was CASed in fill() 
        }  
    }  
} 

BufferedInputStream 总结:
继承自FilterInputStream ,用到装饰模式,内部维护了一个InputStream ,实际的读取操作还是这个InputStream ,BufferedInputStream 的主要作用是给他提供缓存机制,维护了一个byte[]数组buf,每次读数据的时候先看看要读的数据的大小,大于缓存的大小则没必要从缓存读,直接从输入流读取,小于缓存大小则看看缓存里有没有数据,有的话直接从缓存里复制,没有的话,先把数据从输入流里填充到缓存,然后再从缓存里复制。
 
public class BufferedOutputStream extends FilterOutputStream {
    //存储数据的内部缓冲区,存储"缓冲输入流"数据的字节数组
    protected byte buf[];
 
    //缓冲区中的有效字节数,即缓冲区数据的个数
    protected int count;
 
    //构造器:创建了一个新的缓存区为默认字节为8192大小的“缓冲输出流”
    public BufferedOutputStream(OutputStream out) {
        this(out, 8192);
    }
 
    //创建一个新的缓冲输出流,大小为size
    public BufferedOutputStream(OutputStream out, int size) {
        super(out);
        if (size <= 0) {  //size<=0会报非法参数异常
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }
 
    //冲刷缓存区
    private void flushBuffer() throws IOException {
        if (count > 0) {
            out.write(buf, 0, count);
            count = 0;
        }
    }
 
    //将指定的字节写到这个缓冲的输出流
    public synchronized void write(int b) throws IOException {
        // 若缓冲已满,则先将缓冲数据写入到输出流中。
        if (count >= buf.length) {
            flushBuffer();
        }
         // 将“数据b”写入到缓冲中
        buf[count++] = (byte)b;
    }
 
    //
    public synchronized void write(byte b[], int off, int len) throws IOException {
         // 若"写入长度len"大于"缓冲区大小",会先进行冲刷,将缓冲中的数据写入到输出流,
         //   然后直接将数组b写入到输出流中
         if (len >= buf.length) {
            flushBuffer();
            out.write(b, off, len);
            return;
        }
        //若"写入的长度len"大于"缓冲区剩余的空间",会先将缓冲区进行冲刷,然后将b[]写入缓冲区
        if (len > buf.length - count) {
            flushBuffer();
        }
        System.arraycopy(b, off, buf, count, len);
        count += len;
    }
 
    //冲刷,调用flushBuffer()方法,将“缓冲数据”写入到输出流中,
    //flush()方法可以强迫输出流(或缓冲的流)发送数据,即使此时缓冲区还没有填满,以此来打破这种死
    //锁的状态。
    public synchronized void flush() throws IOException {
        flushBuffer();
        out.flush();
    }
}
BufferedOutputStream 总结:
继承自FilterOutputStream ,也是用到装饰模式,内部维护了一个OutputStream ,实际的写操作还是这个OutputStream ,BufferedOutputStream 的主要作用是给他提供缓存机制,仍然是维护了一个byte[]数组buf,每次写数据的时候先判断数据大小,超过了缓存最大值就会先把缓存区的冲刷一下(先把缓存写到输出流,因为可能有上次写的时候写在了缓存),然后直接写到输出流里,没有超过则会先复制到缓存里,所以写操作最后一定要记得调用flush()一下,否则数据可能没有真实的写到输出流而只是复制到缓存中去了。

现在来分析之前的那个复制的操作:

        //读取文件(缓存字节流)
        val inPutStream = BufferedInputStream(FileInputStream("1.txt"))
        //写入相应的文件
        val outPutStream = BufferedOutputStream(FileOutputStream("2.txt"))
        //读取数据
        //一次性取多少字节
        val bytes = ByteArray(2048)
        //接受读取的内容(n就代表的相关数据,只不过是数字的形式)
        var n = -1
        //循环取出数据
        while (inPutStream.read(bytes, 0, bytes.size).also { n = it } != -1) {
            //写入相关文件
            outPutStream.write(bytes, 0, n)
        }
        //flush缓存
        outPutStream.flush()
        //关闭流
        inPutStream.close()
        outPutStream.close()

来看看它的大概过程,首先从BufferedInputStream读数据,里面有一个缓存的操作,读出来后BufferedOutputStream进行写的操作,里面也设计到了一个缓存操作,里面设计到了两轮的复制和两个缓存的维护。
而这个过程对okio来说就显得繁琐了,现在看看okio是怎么做文件复制的:

/**
         * 构造带缓冲的输入流
         */
        var source = File("1.txt").source()
        var bufferedSource = source.buffer()

        /**
         * 构造带缓冲的输出流
         */
        var sink = File("2.txt").sink()
        val bufferedSink = sink.buffer()

        val bufferSize = 2 * 1024 // 2kb


        // 复制文件
        while (!bufferedSource.exhausted()) {
            // 从输入流读取数据到输出流缓冲,其实这里只是把缓存做了一个复制
            bufferedSource.read(bufferedSink.buffer,bufferSize.toLong())
            // 到这里,才是真正的输出流缓冲写出
            bufferedSink.emit()
        }

        source.close()
        sink.close()

代码是不是简洁很多?再来分析分析okio的结构:

Source和Sink

对应IO中的输入流和输出流,Source的实现类实现需read(Buffer sink, long byteCount) throws IOException; Sink的实现类实现write(Buffer source, long byteCount)。不难猜测,在Okio中以Buffer作为操作媒介,可以发挥它的最大优势。

BufferedSource 和 BufferedSink

对应IO中输入流缓冲和输出流缓冲,提供对外的API进行读写操作。

Okio

入口类,工厂类,提供source方法可以得到一个Source输入流,提供sink方法得到一个Sink输出流。两种方法可接受的入参都可为 File、Socket、InputStream / OutputStream。对每个对应的方法进行查看,Okio并没有改变各种Java 输入输出流的对应装饰对象的构造,在构造上,对于涉及到的上面说到的入参,构造起来比较方便。也能看出,Okio并没有打算改变底层的IO方式,旨在弥补原声IO框架上的不足。

Segment

这一部分开篇已现对Segment进行了介绍。除了介绍都的内容外,Segment可以以单链、双链的方式存储。提供了pop()将自己从链中删除,返回下一节点;push()将一个Segment加在自己后面,这两个对于链表的操作不做深入。既然提供了split()方法进行分割,自然也提供了compact()方法Segment进行合并,前提是用来做合并的Segment的剩余容量装得下,也不做深入。

SegmentPoll

复用Segment,可以理解为一个Segment池,知道作用就行这里不深入了解。

RealBufferedSource,RealBufferdSink

为BufferedSource 和 BufferedSink的实现类,但其实只是个代理类,里面有个成员相应的输入输出流成员变量,它们才是真正做事的,而且都维护了一个buffer,给他们提供缓存操作。

Buffer

Okio使用了Segment作为数据存储的方式,自然要提供对应的缓冲方式来操作Segment,Segment在Buffer中以双向链表形式存在。Buffer则负责此项事务。Buffer也实现了BufferedSource和BufferedSink,这是因在使用Okio提供的输入/输出缓冲时,都需要进行缓冲处理,均由Buffer来处理,这样使API对应。

TimeOut

提供超时功能,希望IO能在一定时间内进行完毕,否则视为异常。分两种情况,同步超时和异步超时。这里不做重点讲解。

同步超时:在每次读写中判断超时条件,因为处于同步方法,因此当IO发生阻塞时,不能及时响应。
异步超时:用单独的线程监控超时条件,如果IO发生阻塞,并且检测到超时,抛出IO异常,阻塞终止。
现在来分析分析RealBufferedSource的read源码,也就是上面的那个复制过程中的最重要的那行:

bufferedSource.read(bufferedSink.buffer,bufferSize.toLong()) //RealBufferedSource的方法

 @Override 
public long read(Buffer sink, long byteCount) throws IOException {
    // 用来接收数据的Buffer 不能为空
    if (sink == null) throw new IllegalArgumentException("sink == null");
    // 读取数据不能为负数
    if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
    if (closed) throw new IllegalStateException("closed");

    // 缓冲区没有数据了
    if (buffer.size == 0) {
      // 从输入流中读取数据,重点关注这个方法
      long read = source.read(buffer, Segment.SIZE);
      if (read == -1) return -1;
    }
    
    // 比较 byteCount 与 缓冲中的数据容量,得到到实际要读取的数据量
    long toRead = Math.min(byteCount, buffer.size);
    // 从Buffer 中读取数据到另一个buffer里的缓存
    return buffer.read(sink, toRead);
  }

与Java原生的缓冲方式类似,都先考虑缓冲区中的数据情况,如果缓冲区中没有数据,则先向流读取数据填充缓冲区,再根据所需读取容量与实际缓冲区中存有的数据容量进行读取。这里有一点和Java原生的不同,如果byteCount的数据超出Segment的容量的话,不会直接向流读取。可以看出Okio非常希望以Segment为单位来对流数据进行操作,看接收byte[]为参数的read()的重载方法也接受这个规则。
再看source.read(buffer, Segment.SIZE)。 source是通过之前File("1.txt").source()得来的,它其实就是一个实现了Source的子类:

 // 匿名内部类Source
    return new Source() {
      @Override public long read(Buffer sink, long byteCount) throws IOException {
        // 获取的byteCount不能为负数
        if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
        if (byteCount == 0) return 0;
        try {
          // 检查是否超时
          timeout.throwIfReached();
          // 获取尾节点的Segment,尾节点不满足填充新数据条件则拿到新的Segment
          // 也是位于尾节点
          Segment tail = sink.writableSegment(1);
          // 实际从in读取的数据,能看出最大不能超过 Segment.SIZE
          int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);
          // 从in中读取数据到Segment.data,这个in就是原生的Inputstream,就是File的输入流
          int bytesRead = in.read(tail.data, tail.limit, maxToCopy);
          // 这里说明没有读到有效数据
          if (bytesRead == -1) return -1;
          // 更新索引位置
          tail.limit += bytesRead;
          // 更新buffer容量
          sink.size += bytesRead;
          return bytesRead;
        } catch (AssertionError e) {
          if (isAndroidGetsocknameError(e)) throw new IOException(e);
          throw e;
        }
      }
      ......
    };

从输入流中读取数据,数据存于Buffer中,位于尾节点的Segment,与前面说的一样,单次向流读出操作,大小不能超过Segment.SIZE。

回到buffer.read(sink, toRead);,在确认缓冲区有数据之后,从缓冲区中读取数据到sink,即从Buffer中读取数据到另一Buffer.

  @Override public long read(Buffer sink, long byteCount) {
    // 用来接收数据的sink不能为空
    if (sink == null) throw new IllegalArgumentException("sink == null");
    // 接收的数据大小不能为负数
    if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
    if (size == 0) return -1L;
    // 读取的数据不超过当前缓冲区的容量
    if (byteCount > size) byteCount = size;
    // 从当前缓冲区,将数据写入到另一缓冲区,即从 this,写到sink
    sink.write(this, byteCount);
    return byteCount;
  }

  @Override public void write(Buffer source, long byteCount) {
        if (source == null) throw new IllegalArgumentException("source == null");
    if (source == this) throw new IllegalArgumentException("source == this");
    checkOffsetAndCount(source.size, 0, byteCount);
    
    /**
    缓冲区数据从 缓冲区source 移动到 缓冲区this, 
    在当前的案例中,缓冲区source代表输入流缓冲数据,缓冲区this代表输出流缓冲数据
    此函数源码内部有一大段注释,可以细细品味,我就不贴了
    */

    while (byteCount > 0) {

      if (byteCount < (source.head.limit - source.head.pos)) {
        // 进到这里说明,说明source的头节点有足够的数据
        
        // 获取当前缓冲区尾节点
        Segment tail = head != null ? head.prev : null;
        if (tail != null && tail.owner
            && (byteCount + tail.limit - (tail.shared ? 0 : tail.pos) <= Segment.SIZE)) {
          // 尾节点不为空,并且尾可解是owner状态
          // 并且尾节点能够装下byteCount数量的数据
          
          // 将数据从source的头节点 copy 到 当前缓冲区的尾节点
          source.head.writeTo(tail, (int) byteCount);
          source.size -= byteCount;
          size += byteCount;
          return;
        } else {
          // 说明数据当前缓冲区尾节点不能存下byteCount大小的数据
          // 将source头节点的Segment分割,byteCount过阈值则共享,否则拷贝
          // 共享过程则不用copy
          source.head = source.head.split((int) byteCount);
        }
      }

      /**
       将source缓冲区的头节点pop,加入到当前缓冲区
      */
      Segment segmentToMove = source.head;
      long movedByteCount = segmentToMove.limit - segmentToMove.pos;
      source.head = segmentToMove.pop();
      if (head == null) {
        // 进到这里说明当前缓冲区没有数据,将segmentToMove作为头节点
        head = segmentToMove;
        head.next = head.prev = head;
      } else {
        // 进到这里则是将segmentToMove放到链表尾部
        Segment tail = head.prev;
        tail = tail.push(segmentToMove);
        tail.compact();
      }
      /**
      更新缓冲区大小,以及还需的byteCount数量
      */
      source.size -= movedByteCount;
      size += movedByteCount;
      byteCount -= movedByteCount;
    }
  }

Okio最亮眼的操作,就是设计出了Segment存储数据,通过Buffer进行缓冲管理,并在Buffer.write()则里,通过移动引用而不是真实数据,是减少数据copy进而交换数据的关键。

上面分析了RealBufferedSource,而RealBufferedSink也是同样的道理,只是方向相反,缓冲数据存储依然离不开Segment和Buffer,RealBufferedSink先把数据copy到buffer里的Segment,在通过成员sink的write()操作把buffer里的缓存写出,可以通过Okio.sink()拿到的匿名内部类Sink()查看:

 /** Returns a sink that writes to {@code out}. */
  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);
          //原生的io操作
          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 + ")";
      }
    };
  }

想较于Java原生IO的缓冲方案,双流操中,或者说以Buffer来代替 写入/写出 的 byte[],减少了copy的过程,通过Segment的移动达到目的。

此外,Okio的写入/写出操作,也可以像原生那样,接受byte[]参数,或者直接获取下一个数据,这种情况时,则于原生相似,需要时一样依赖copy,不再有减少copy的优势。并且,Okio接口也更友好,如之前说原生实现向文件写入自定义数据时,需要Data的流类型进行转译,自身就封装了这样的操作。

总结

Okio核心竞争力为,增强了流于流之间的互动,使得当数据从一个缓冲区移动到另一个缓冲区时,可以不经过copy能达到,而只是通过引用来直接分享:

以Segment作为存储结构,真实数据以类型为byte[]的成员变量data存在,并用其它变量标记数据状态,在需要时,如果可以,移动Segment引用,而非copy data数据
Segment在Segment线程池中以单链表存在以便复用,在Buffer中以双向链表存在存储数据,head指向头部,是最老的数据,通过head能找到所有的segement。
Segment能通过slipt()进行分割,可实现数据共享,能通过compact()进行合并。由Buffer来进行数据调度,基本遵守 “大块数据移动引用,小块数据进行copy” 的思想
Source 对应输入流,Sink 对应输出流
TimeOut 以达到在期望时间内完成IO操作的目的,同步超时在每次IO操作中检查耗时,异步超时开启另一线程间隔时间检查耗时
Okio并没有打算优化底层IO方式以及替代原生IO方式,Okio优化了缓冲策略以减轻内存压力和性能消耗,并且对于部分IO场景,提供了更友好的API,而更多的IO场景,该记的还得记。

你可能感兴趣的:(浅析okio的架构和源码实现)