okhttp(七)底层通库okio-关键类剖析

801 阅读9分钟

上节介绍了okio中几个重要的对象,但还没有对他们进行深入地分析。本结将其深入源码对他们进行详细的分析。首先回顾下几个重要的对象

  1. sink和source:定义了两个接口,此接口是对原有java io中outputStream和inputStream的替代(实际上是做了二次封装)

  2. Buffer:缓冲,内部是一个由segment组成的双向循环链表

  3. BufferedSink和BufferedSource:带缓冲功能的sink和source接口

  4. RealBufferedSink和RealBufferedSource:BufferedSink和BufferedSource的实现类,内部依赖Buffer和sink(source)对象

下面先重点分析底层几个关键的类

Segement


segment是Buffer内部的数据存储对象,它的结构如下图所示:

其有几个重要的参数:

 //存储数据的数组
 final byte[] data;
 //数据可读的位置  
 int pos;
 //数据可写的位置   
 int limit;
 //表示是否是可分享的。如果data引用其他segment的data数组或此segment的data数组被其他segment的data数组引用,shared为true
 boolean shared;
//表示独占data数组,即data数组不被其他segment引用或此segment也不引用其他segment的data数组
 boolean owner;
//指向下一个segment的指针
Segment next;
//指向上一个segment的指针
Segment prev;

对于参数shared如果为true则不能再写数据了,只能读取。提供segment共享功能的目的在与数据的复用,节省开销。segment有三个构造函数,第一种创建的segment是独占的

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

后面的方式创建的segment是共享的,数据与其他segment共享

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

segment对外提供的方法并不多,下面对几个方法做下介绍。

pop:此方法就是将当前segment元素移出链表,并返回下一个setment元素,方法比较简单,就是链表的一些常规操作,这就不细说了。

  public Segment pop() {
    Segment result = next != this ? next : null;
    prev.next = next;
    next.prev = prev;
    next = null;
    prev = null;
    return result;
  }

push:此方法将segment加入当前segment的后面

  public Segment push(Segment segment) {
    segment.prev = this;
    segment.next = next;
    next.prev = segment;
    next = segment;
    return segment;
  }

split:此方法主要是将头结点segment分裂为两个segment,第一个segment的数据为原节点数据[pos--pos+byteCount]范围内的数据,第二个节点为原节点数据[pos+byteCount--limit]范围内的数据。

  public Segment split(int byteCount) {
    if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();
    Segment prefix;
    //如果要分割出字节数大于了最小分享门限,则通过分享的方式,创建一个segment,与原segment共享底层的data数组
    if (byteCount >= SHARE_MINIMUM) {
      prefix = new Segment(this);
    } else {
    //小于最小分享门限,则通过复制的方式,将原数据复制byteCount个字节到新的segment
      prefix = SegmentPool.take();
      System.arraycopy(data, pos, prefix.data, 0, byteCount);
    }
   //修改分割出去的segment数据
    prefix.limit = prefix.pos + byteCount;
    pos += byteCount;
    //将分割出来的segment加到当前segment之前
    prev.push(prefix);
    return prefix;
  }

此过程对于初次接触的同学来说可能有点难理解,画了一张图来描述此过程:

方法中对要分割出去的字节数做了判断,如果大于了最小分享的门限,则共用底层data数组,通过pos和limit来表示不同的数据区间。而小于最小门限的话就直接通过复制的方式创建了一个新的底层data数组。这样做的原因还是从性能的角度来考虑的。如果太多小的segment存在会导致链表过长,降低性能,而大segment的复制也会消耗比较大的性能。因此才有了目前的分割方案,即大数据共享数据,小数据复制。

writeTo:此方法将当前segment中bytecCount数量的数据移动到另一个segement sink中去。

  public void writeTo(Segment sink, int byteCount) {
    if (!sink.owner) throw new IllegalArgumentException();
    if (sink.limit + byteCount > SIZE) {
    if (sink.shared) throw new IllegalArgumentException();
      //如果sink中存不下byteCount数量的数据,则先将sink中的数据移动到pos位置为0处
      if (sink.limit + byteCount - sink.pos > SIZE) throw new IllegalArgumentException();
      System.arraycopy(sink.data, sink.pos, sink.data, 0, sink.limit - sink.pos);
      sink.limit -= sink.pos;
      sink.pos = 0;
    }
    //将当前segment中数据复制到到sink中
    System.arraycopy(data, pos, sink.data, sink.limit, byteCount);
    sink.limit += byteCount;
    pos += byteCount;
  }
}

compact:当链表尾部的元素和它的上一个元素中的数据都没有达到一半时,调用此方法将会把尾元素中的内容复制到他的上一个元素中去,并移除掉尾元素。

  public void compact() {
    if (prev == this) throw new IllegalStateException();
    if (!prev.owner) return; // Cannot compact: prev is not writable.
    int byteCount = limit - pos;
    //可移动空间
    int availableByteCount = SIZE - prev.limit + (prev.shared ? 0 : prev.pos);
    //如果要移动数据大于了可用空间,则返回
    if (byteCount > availableByteCount) return; // Cannot compact: not enough writable space.
    //将当前元素的内容复制到上一个元素中去
    writeTo(prev, byteCount);
    //将自己弹出链表
    pop();
    //回收当前segment
    SegmentPool.recycle(this);
  }

上面就是segment提供的几个方法,这些方法都比较简单,不必深入代码细节,知道每个方法的作用即可。后续很多上层的操作都会用到这些方法。

SegmentPool


从名字上就不难理解这个类是用来干啥的了,提供Segment对象的创建和回收,避免垃圾回收产生性能损耗,其内部是有Segment元素组成的单链表结构,如下图所示:

对象内部有一个参数表示最大能存储的字节数(64kib):

 static final long MAX_SIZE = 64 * 1024; // 64 KiB.

除此之外,还提供了两个方法takerecyle分别表示获取和回收segment元素,take方法源代码如下,方法很简单,就是一个单链表的操作,描述了获取链表头部元素的操作:

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

recycle方法的源代码,将回收的segment放置在链表头部:

  static void recycle(Segment segment) {
    if (segment.next != null || segment.prev != null) throw new IllegalArgumentException();
    if (segment.shared) return; // This segment cannot be recycled.
    synchronized (SegmentPool.class) {
      if (byteCount + Segment.SIZE > MAX_SIZE) return; // Pool is full.
      byteCount += Segment.SIZE;
      segment.next = next;
      segment.pos = segment.limit = 0;
      next = segment;
    }
  }

Buffer


前面已经说过RealBufferedSink和RealBufferedSouce内部的接口实现,实际上是依赖于Buffer和具体的Sink和Source。Buffer提供缓冲功能,sink和souce实现对象完成具体的数据传输。Buffer类中有个重要的方法,write(Buffer source, long byteCount),此方法是一个比较底层的方法,完成将source缓冲中的数据从头结点开始,移动byteCount数量到当前Buffer的尾部中去。此方法的实现有它独特的地方,源码及注释如下:

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);

    while (byteCount > 0) {
      //如果移动的字节数小于要移动的元素的数据大小
      if (byteCount < (source.head.limit - source.head.pos)) {
        Segment tail = head != null ? head.prev : null;
        //如果当前buffer的尾节点有足够的空间
        if (tail != null && tail.owner
            && (byteCount + tail.limit - (tail.shared ? 0 : tail.pos) <= Segment.SIZE)) {
          //将source头结点中的元素移动byteCount到尾节点中
          source.head.writeTo(tail, (int) byteCount);
          source.size -= byteCount;
          size += byteCount;
          return;
        } else {
        //如果尾节点不能存储元素,则先将source头节点分割成两个segment,其中prefix节点大小为byteCount
          source.head = source.head.split((int) byteCount);
        }
      }
      //获取到要移动的segment
      Segment segmentToMove = source.head;
      //获取要移动segment的数据大小
      long movedByteCount = segmentToMove.limit - segmentToMove.pos;
      //将要移动的segment从source中移除,并将source的head指针指向下一个元素
      source.head = segmentToMove.pop();
      //如果当前buffer为空,则将当前buffer的head指针指向要移动的segment
      if (head == null) {
        head = segmentToMove;
        head.next = head.prev = head;
      } else {
        Segment tail = head.prev;
        //将移动的segment加入到链表尾部
        tail = tail.push(segmentToMove);
        //对尾部元素进行压缩
        tail.compact();
      }
      //修改相关数据参数
      source.size -= movedByteCount;
      size += movedByteCount;
      byteCount -= movedByteCount;
    }
  }

从上面的方法可以看出,在Buffer中传输数据时,实际上并没有发生数据的复制和转移,仅仅只是segment的指针发生了变化而已,通过引用指针的变化,就完成了数据在不同Buffer间的传输,这是其高性能的重要原因之一,也是值得我们平时在写代码的过程借鉴的。 有了上面这些重要的介绍,我们来详细完整的分析一个数据在底层的传输过程。上节我们以请求头的传输为例简单大概地介绍了一下数据的传输,跟踪到RealBufferedSink类中的writeUtf8(String string)方法后就点到为止了,下面将深入代码细节,看看究竟是怎么完成数据传输的。

public BufferedSink writeUtf8(String string) throws IOException {
    if (closed) throw new IllegalStateException("closed");
    //将传输的内容写入到segment链表中,此方法涉及到很多utf8编码的细节,就不展开说了,最终都会将内容转换成对应的字节存储
    buffer.writeUtf8(string);
    //将segment中的数据传输出去
    return emitCompleteSegments();
  }

此方法中出现了一个emitCompleteSegments()方法,你会看到此方法机会在类中的每个方法中都会出现,它负债完成数据从segment缓存到目的端的具体传输过程。

public BufferedSink emitCompleteSegments() throws IOException {
    if (closed) throw new IllegalStateException("closed");
    //返回当前buffer中写入的字节数(即可传输的字节数)
    long byteCount = buffer.completeSegmentByteCount();
    //通过流对象,将buffer中的数据传输到目的端
    if (byteCount > 0) sink.write(buffer, byteCount);
    return this;
  }

上述代码中的最后的sink.write(buffer, byteCount)中的sink,是在创建RealBufferedSink对象是传入的,即sink = Okio.buffer(Okio.sink(rawSocket)),Okio.sink的代码如下

  public static Sink sink(Socket socket) throws IOException {
    if (socket == null) throw new IllegalArgumentException("socket == null");
    AsyncTimeout timeout = timeout(socket);
    //创建了一个匿名对象
    Sink sink = sink(socket.getOutputStream(), timeout);
    return timeout.sink(sink);
  }

来看看创建sink对象的代码

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() {
      //将buffer中的数据写入到目的端
      @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是socket对象的outputStream,调用其write方法来完成最后的数据传输
          out.write(head.data, head.pos, toCopy);
        //修改相关参数
          head.pos += toCopy;
          byteCount -= toCopy;
          source.size -= toCopy;
        //segment中的数据完成传输后,回收segment
          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方法中我们可以看出,数据的最后传输还是依赖于socket对象outputStream,框架做了那么多工作其实都是在对out.write(head.data, head.pos, toCopy)这段代码的封装。整个库的核心在于其缓存的实现,通过引入缓存来减少io发生的次数,从而提高效率。 实际上这里的sink,除了完成网络的处理,还可以完成文件的处理,这里就不具体分析了。实际上流程都是一样的。

public static Sink sink(File file) throws FileNotFoundException {
    if (file == null) throw new IllegalArgumentException("file == null");
    return sink(new FileOutputStream(file));
  }

总结


本结分析了okio底层几个重要的类。okio在原有的面向流(sink和source)的基础之上提供了面向了缓冲的能力,其缓冲内部依赖于一个双向循环的Segment链表,通过改变数据的指针,而不是操作数据,提高了数据的数据效率。同时避免频繁的创建Segment,提供了SegmentPoll来缓存对象,这些实现细节都是值得我们平时在编码过程中学习的。