okio 源碼學習筆記

最近在學習okhttp的過程中,很多地方遇到了okio的功能,okio是square公司封裝的IO框架,okhttp的網絡IO都是通過okio來完成的。我們都知道Java原生的IO框架使用了裝飾者模式,雖然體系結構很明確,但是有一個致命的缺點就是類太多,對數據進行IO操作時往往需要對IO流套多層。而okio則是對Java原生IO系統的一次封裝,使得進行IO數據操作時更加方便靈活。
對于okio的介紹也有很多,這里推薦兩篇個人認為挺好的文章,本文也是參考了其中的介紹,整理為學習筆記。

OKio - 重新定義了“短小精悍”的IO框架
大概是最完全的Okio源碼解析文章

首先我們來看一下應用okio的一個簡單的小例子,該方法的功能就是完成文件的拷貝

   public static void copyFile(String fromName, String toName) throws IOException{
        File from = new File(fromName);
        File to = new File(toName);

        BufferedSource source = Okio.buffer(Okio.source(from));
        BufferedSink sink = Okio.buffer(Okio.sink(to));

        String copyContent = "";
        while (!source.exhausted()){
            copyContent = source.readString(Charset.defaultCharset());
            sink.write(copyContent.getBytes());
        }

    }

首先我們需要明確在okio中,source代表輸入流,sink代表輸出流,我們可以看到構建輸入輸出流很簡單,Okio這個類提供一系列的工具方法供我們使用,而在使用流的時候沒有那么多類,包括BufferedXXX, DataXXX, FileXXX,以及XXXStream, Reader, Writer等區分, 在okio中source和sink代表基本的輸入輸出流的接口,對其封裝的只有BufferedXXX一種類型,它提供了幾乎所有 的我們常用的輸入輸出操作,不過在BufferedSource和BufferedSink中都包含了一個Buffer對象,這個類是我們需要重點學習的對象,okio都是通過這個類與InputeStream和OutputStream通信完成的IO操作,下面我們開始進入學習okio的代碼結構。

1. 整體結構

在學習代碼之前,我們首先看一下okio的整體結構圖,該圖是源自于推薦的第一篇文章中的圖片,這里特別說明。


okio整體結構

從圖中我們可以看到結構很清晰,上面是輸出流,下面是輸入流,每個都只有BufferedXXX一個裝飾器對象,并且兩個裝飾器對象都是依賴Buffered對象。這里需要說明的是Source, Sink, BufferedSource, BufferedSink都是接口,他們定義了需要的基本功能,而他們都有對應的實現類,RealBufferedSource和RealBufferedSink, 但是他們兩個的功能實現幾乎都是通過封裝的Buffer對象來完成的,Buffer類同時實現了BufferedSource, BufferedSink接口,再次強調這個我們重點學習的對象。
在推薦的第一篇文章中是以Sink為例講述的,這里就記錄一下我學習Source的過程,避免重復,其實原理是完全一致的,了解其中之一,另外一個也就明白了。我們首先來看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.
 **/
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.
   */
  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;
}

這里保留的Source源碼中的第一段注釋,其他部分可以自行查看,注釋說明的很清楚,它作為一個輸入流,可以為內存輸入(提供)一個字節流,這個流可以是封裝的任何的底層文件結構,比如網絡通信的中的socket, 硬盤中保存的普通文件,或者內存中的緩存數據等等。代碼中我們需要注意的時候read方法,它規定了應該將該流的內容讀到內存中的Buffer對象中去。第二個方法為timeout返回一個timeout對象,關于IO的超時對象,我們留到本文的最后一部分介紹。下面再來看BufferedSource的代碼:

/**
 * A source that keeps a buffer internally so that callers can do small reads without a performance
 * penalty. It also allows clients to read ahead, buffering as much as necessary before consuming
 * input.
 */
public interface BufferedSource extends Source {
  Buffer buffer();

  boolean exhausted() throws IOException;

  void require(long byteCount) throws IOException;

  boolean request(long byteCount) throws IOException;

  byte readXXX() throws IOException;

  void read(XXXX) throws IOException;

  void skip(long byteCount) throws IOException;

  int select(Options options) throws IOException;

  long indexOf(ByteString bytes, long fromIndex) throws IOException;
 
  boolean rangeEquals(long offset, ByteString bytes) throws IOException;
  /** Returns an input stream that reads from this source. */
  InputStream inputStream();
}

這里對代碼做了最大的簡化,首先來看它定義了一個buffer()方法用來獲取該流對象中封裝的Buffer對象,其次就是exhaust開始的三個方法,exhaust提供流是否結束的檢查,另外兩個則是用于檢查流中是否還有若干字節,二者不同之處在于require是在不足時拋異常,而request則以false作為返回值。接下來就是最為重要的一系列的read方法,他們可以簡單地分為兩類,一個是返回值的形式,主要是返回一些類型的數據,有點類似于DataInputStream中定義的方法,還有一類是將流的內容讀到傳入的數組參數中,通常是字節數組。最后定義了skip方法用于跳過字節流中的一部分內容,select方法則用于匹配一定的選項規則,具體可以看源碼中所舉的例子,這里不再介紹,另外還有一系列的indexOf,rangeEqual等方法,通過名稱也可以明白其中的意思,inputStream則可以將Source對象轉為InputStream對象輸出。

以上是BufferedSource中定義的接口方法,而該接口在okio中該接口有兩個實現類,一個是RealBufferedSource,另外一個就是Buffer,這里我們先簡單了解一下前者:

final class RealBufferedSource implements BufferedSource {
  public final Buffer buffer = new Buffer();
  public final Source source;
  boolean closed;

  RealBufferedSource(Source source) {
    if (source == null) throw new NullPointerException("source == null");
    this.source = source;
  }
...

從代碼中可以看到它封裝了一個Buffer對象以及一個Source對象, 其中Buffer對象提供緩存以及字節流與其他數據類型的轉換的功能, 而source對象則是封裝的底層輸入字節流,包括文件,Socket等,這個在前面已經做了介紹,可見如此設計甚好。我們就可以預感到RealBufferedSource將多數在BufferedSource中定義的接口功能都代理給了Buffer。下面我們來看部分代碼:

@Override public long read(Buffer sink, long byteCount) throws IOException {
    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;
    }

    long toRead = Math.min(byteCount, buffer.size);
    return buffer.read(sink, toRead);
  }

@Override public byte readByte() throws IOException {
    require(1);
    return buffer.readByte();
  }

這里只是截取了其中具有代表性的兩個方法,第一個是Source中定義的方法,我們可以看到該方法中,首先將source對象中的字節流內容讀取到buffer對象中,然后通過Buffer對象提供的讀功能完成將內容讀取到指定緩存中的任務。
第二個方法是讀取一個字節,同樣也是首先將source對象中的內容讀取到Buffer對象中,(這里的讀取到Buffer對象的工作是在request方法中完成的, require方法是調用的request方法)然后將該方法的功能全權代理給Buffer對象來完成。有興趣的同學可以查看其他方法的實現,起基本原理都是一致的,代理給Buffer對象。

以上內容我們就整體介紹了一下okio中的輸入流的類的結構以及其中的關系,同樣地,在輸出流中,其結構與之類似,有興趣的同學可以自行查看源碼。在使用okio的功能時,不可或缺的是Okio這個工具類,該工具類中有兩類靜態方法, 一類是buffer()方法,它可以將任何source或sink對象轉換為BufferedSource或BufferedSink對象,第二類方法就是sink()或source()方法,這一類方法可以將任何流對象,包括InputStream或OutputStream,以及流的底層結構文件,字符串,Socket等轉換為source或者sink對象,這樣以來就可以很方便地使用okio完成輸入輸出操作,而且也可以很容易與原生的JavaIO完成轉換。

okio中將所有的功能方法都定義在了BufferedXXX接口中,而兩個接口分別有兩個實現類,即RealBufferedXXX 和Buffer, Buffer對象實現了上述兩個接口,而RealBufferedXXX中的功能都是代理給了Buffer對象,因此在下一部分我們需要重點學習Buffer對象。

2 Buffer對象

關于Buffer對象,其注釋明確說明了,它是內存中字節數據的集合。在前面我們知道在okio中幾乎所有的IO操作都是經由Buffer中轉實現的,而Buffer中的內部數據結構就是一組字節數組,通過在內存中的中轉緩存實現一系列的IO操作。所以在了解Buffer如何實現IO操作功能之前,我們先來了解一下Buffer的底層數據結構,這就涉及Segment的概念。

首先我們需要明確,在Buffer的底層是有Segment的鏈表實現的,而且是一個環形的雙向鏈表,即普通的雙向鏈表收尾連接起來。這里放一張本文開頭中推薦的第二篇文章中的一張示意圖以示意,這里特此說明。


Segment鏈表結構

所以這里我們首先來學習一下Segment的結構,其代碼為:

/**
 * A segment of a buffer.
 */
final class Segment {
    ...

  final byte[] data;

  /** The next byte of application data byte to read in this segment. */
  int pos;

  /** The first byte of available data ready to be written to. */
  int limit;

  /** True if other segments or byte strings use the same byte array. */
  boolean shared;

  /** True if this segment owns the byte array and can append to it, extending {@code limit}. */
  boolean owner;

  /** Next segment in a linked or circularly-linked list. */
  Segment next;

  /** Previous segment in a circularly-linked list. */
  Segment prev;

  ...

我們首先來看一下Segment的結構,這里由于不擅長畫圖,又沒有在網上找到合適的圖片,所以暫且以文字描述,如果是熟悉Java的nio框架的同學可以借鑒ByteBuffer的結構,Segment與之類似。data是底層的字節數組,pos表示當前讀到的位置,lim表示當前寫到的位置,讀操作的結束點為lim, 寫操作的結束位置為data的末尾,也就是SIZE-1,這兩個屬性與ByteBuffer的結構完全一致。而在okio中為了避免過多的數據拷貝,它使用了共享的方式,我們在后面還會介紹到。這里share屬性表示該Segment對象的數據即data數組是與其他Segment對象共享的, 而owner屬性表示該Segment對data數組是否擁有所有權。舉個例子來說,當我們需要拷貝一份數據,剛好處于一個Segment中,為了避免拷貝,我們可以新建一個Segment對象,但是新的Segment對象與之前的Segment對象共享data數組,此時兩個Segment對象的share屬性都置為true, 而原有的Segment的ower屬性為true,新建的Segment對象ower屬性則為false, 此時原Segment對于數組中lim到數組結尾的空間具有寫權限,而新建的Segment則沒有。對于最后兩個屬性pre, next則應該很容易明白他們是用于構建鏈表的。下面我們來看它的構造器, 注意其中的ower和shareed屬性:

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

  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對象,并且新建了data數組,而后兩個構造器則是用于新建Segment對象,并且與別的Segment共享data數組,需要注意理解其中的shareed和ower屬性的設置,后面我們還會提到如何利用這兩個屬性。下面我們來看Segment的方法:

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

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

這里是兩個最為常用但是也很簡單的方法,就是簡單的pop和push方法,pop方法用于將自己移除鏈表,并返回下一個節點的引用,即下一個Segment對象,如果這是最后一個節點,則返回null; push方法則是將一個新的Segment對象添加到當前節點的后面。

 /**
   * Splits this head of a circularly-linked list into two segments. The first
   * segment contains the data in {@code [pos..pos+byteCount)}. The second
   * segment contains the data in {@code [pos+byteCount..limit)}. This can be
   * useful when moving partial segments from one buffer to another.
   *
   * <p>Returns the new head of the circularly-linked list.
   */
  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;
  }

這是一個將Segment分割的方法,這里保留了源碼中的注釋,可以仔細閱讀一下,明確說明了是 為了避免過多的數據拷貝,使用數組共享的方式,不過為了避免將Segment分割的過小造成鏈表太長,這里設置了共享的最小的大小。該方法常用與在數據拷貝之前,首先將需要拷貝的字節數分割為一個新的Segment對象,便于拷貝。
這里的SegmentPool顧名思義就是一個Segment的共享池,避免創建對象和回收引起的內存抖動,該對象提供兩個靜態方法就是take()和recycle()很容易猜到什么含義,這里不再介紹。
這里需要注意的是,創建新的Segment對象,不管是不是共享,新建的Segment對象都會添加當前Segment之前的位置,并且返回新建的Segment對象,而原Segment對象的pos會向后移動byteCount字節,lim不變,并且具有寫權限,而新建的Segment如果是與原Segment共享data數組,則新Segment對象不能對lim之后的空間進行寫操作,這一點在前面也做過介紹。既然有分割那么也應該有合并的方法,那么下面來看compact()方法:

  /**
   * Call this when the tail and its predecessor may both be less than half
   * full. This will copy data so that segments can be recycled.
   */
  public void compact() {
    if (prev == this) throw new IllegalStateException();
    if (!prev.owner) return; // Cannot compact: prev isn't 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();
    SegmentPool.recycle(this);
  }

  /** Moves {@code byteCount} bytes from this segment to {@code sink}. */
  public void writeTo(Segment sink, int byteCount) {
    if (!sink.owner) throw new IllegalArgumentException();
    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();
      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;
    }

    System.arraycopy(data, pos, sink.data, sink.limit, byteCount);
    sink.limit += byteCount;
    pos += byteCount;
  }

compact()方法用于將當前Segment節點與之前的節點合并,將當前節點的數據寫入到之前的節點中,代碼邏輯很清晰,如pre.owner為false則不能寫入,然后判斷之前的節點是否具有足夠的空間,這里注意availableByteCount的計算方法,如果之前的節點不是被共享的,那么pos到lim之間的數據可以向前移動,移動到0~lim-pos的位置,所以availableByteCount就多出了pos大小的空間,而如果之前的節點是被共享的,那么數據是不能被移動的。最后如果可用空間足夠大就執行數據移動,并刪除當前Segment對象,并且回收到SegmentPool中。
下面來看數據移動的方法, 數據移動的方法邏輯也比較清晰,與compact()類似,如果數據空間不足,而數據可移動(即不是共享狀態)移動后充足則移動數據,否則會拋出異常。然后執行數據拷貝并設置sink的lim屬性以及原Segment的pos屬性即可完成任務。

至此我們就是介紹完了Segment的方法,它的代碼很少,功能也很簡單,包括pop, push, slit, compact和writeTo五個方法,而Buffer的底層結構就是關于Segment的環形雙向鏈表,那么Buffer的IO操作都是通過Segment的操作來完成的。下面我們來簡單學習一下Buffer的部分功能。

/**
 * A collection of bytes in memory.
 */
public final class Buffer implements BufferedSource, BufferedSink, Cloneable {
  
    ...
  Segment head;
  long size;

  public Buffer() {
  }
...

可以看到Buffer的屬性域很簡單,就是一個關于Segment的雙向鏈表,head屬性指向該鏈表的頭部,size表示該Buffer的大小,下面我們來看Buffer的部分功能方法:


  @Override public void write(Buffer source, long byteCount) {
    // Move bytes from the head of the source buffer to the tail of this buffer
    // while balancing two conflicting goals: don't waste CPU and don't waste
    // memory.
    //
    //
    // Don't waste CPU (ie. don't copy data around).
    //
    // Copying large amounts of data is expensive. Instead, we prefer to
    // reassign entire segments from one buffer to the other.
    //
    //
    // Don't waste memory.
    //
    // As an invariant, adjacent pairs of segments in a buffer should be at
    // least 50% full, except for the head segment and the tail segment.
    //
    // The head segment cannot maintain the invariant because the application is
    // consuming bytes from this segment, decreasing its level.
    //
    // The tail segment cannot maintain the invariant because the application is
    // producing bytes, which may require new nearly-empty tail segments to be
    // appended.
    //
    //
    // Moving segments between buffers
    //
    // When writing one buffer to another, we prefer to reassign entire segments
    // over copying bytes into their most compact form. Suppose we have a buffer
    // with these segment levels [91%, 61%]. If we append a buffer with a
    // single [72%] segment, that yields [91%, 61%, 72%]. No bytes are copied.
    //
    // Or suppose we have a buffer with these segment levels: [100%, 2%], and we
    // want to append it to a buffer with these segment levels [99%, 3%]. This
    // operation will yield the following segments: [100%, 2%, 99%, 3%]. That
    // is, we do not spend time copying bytes around to achieve more efficient
    // memory use like [100%, 100%, 4%].
    //
    // When combining buffers, we will compact adjacent buffers when their
    // combined level doesn't exceed 100%. For example, when we start with
    // [100%, 40%] and append [30%, 80%], the result is [100%, 70%, 80%].
    //
    //
    // Splitting segments
    //
    // Occasionally we write only part of a source buffer to a sink buffer. For
    // example, given a sink [51%, 91%], we may want to write the first 30% of
    // a source [92%, 82%] to it. To simplify, we first transform the source to
    // an equivalent buffer [30%, 62%, 82%] and then move the head segment,
    // yielding sink [51%, 91%, 30%] and source [62%, 82%].

    if (source == null) throw new IllegalArgumentException("source == null");
    if (source == this) throw new IllegalArgumentException("source == this");
    checkOffsetAndCount(source.size, 0, byteCount);

    while (byteCount > 0) {
      // Is a prefix of the source's head segment all that we need to move?
      if (byteCount < (source.head.limit - source.head.pos)) {
        Segment tail = head != null ? head.prev : null;
        if (tail != null && tail.owner
            && (byteCount + tail.limit - (tail.shared ? 0 : tail.pos) <= Segment.SIZE)) {
          // Our existing segments are sufficient. Move bytes from source's head to our tail.
          source.head.writeTo(tail, (int) byteCount);
          source.size -= byteCount;
          size += byteCount;
          return;
        } else {
          // We're going to need another segment. Split the source's head
          // segment in two, then move the first of those two to this buffer.
          source.head = source.head.split((int) byteCount);
        }
      }

      // Remove the source's head segment and append it to our tail.
      Segment segmentToMove = source.head;
      long movedByteCount = segmentToMove.limit - segmentToMove.pos;
      source.head = segmentToMove.pop();
      if (head == null) {
        head = segmentToMove;
        head.next = head.prev = head;
      } else {
        Segment tail = head.prev;
        tail = tail.push(segmentToMove);
        tail.compact();
      }
      source.size -= movedByteCount;
      size += movedByteCount;
      byteCount -= movedByteCount;
    }
  }

  @Override public long read(Buffer sink, long byteCount) {
    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;
    sink.write(this, byteCount);
    return byteCount;
  }

首先我們來看兩個比較重要的方法,即讀和寫方法,其中最主要的邏輯就在write()方法中,這里保留了所有的注釋,個人認為還是要去讀一下注釋的,這里不再對注釋解釋,可以自行查看,通過理解其中的例子,就可以理解write()方法的執行邏輯了。這里只是解釋其中的代碼邏輯。首先是在一個循環中執行寫操作,直到byteCount為零。然后看需要寫的字節數量是否超出了source的第一個Segment的范圍,如果沒有超出,則看sink的最后一個Segment是否可以寫入并且具有充足的空間寫入,如果可以就直接拷貝過去,這里調用了Segment.writeTo()方法,前面也做過介紹,其實就是System.arrayCopy()將數組拷貝過去。否則的話就將source的head節點拆分,保證source的head節點是需要全部寫入到sink中去的。后面的邏輯就比較簡單了,就是將source的head節點掛到sink的尾部,然后執行一次compact()操作,并更新各個屬性就完成了一個Segment的寫入,這里不會更新Segment節點時不需要數組拷貝,所以節約了CPU,而在compact()時執行少量的數據拷貝,提高內存的利用率。如此循環,完成數據的寫入操作。

下面來看read()方法就比較簡單了,它其實就是調用了sink.write()方法,其實也就是借助上面的write方法,實現數據讀取,反方向寫入就是數據讀取,將數據讀取到sink中。
下面我們再來看Buffer中三個比較典型的方法作為實例:

  /** Copy {@code byteCount} bytes from this, starting at {@code offset}, to {@code out}. */
  public Buffer copyTo(Buffer out, long offset, long byteCount) {
    if (out == null) throw new IllegalArgumentException("out == null");
    checkOffsetAndCount(size, offset, byteCount);
    if (byteCount == 0) return this;

    out.size += byteCount;

    // Skip segments that we aren't copying from.
    Segment s = head;
    for (; offset >= (s.limit - s.pos); s = s.next) {
      offset -= (s.limit - s.pos);
    }

    // Copy one segment at a time.
    for (; byteCount > 0; s = s.next) {
      Segment copy = new Segment(s);
      copy.pos += offset;
      copy.limit = Math.min(copy.pos + (int) byteCount, copy.limit);
      if (out.head == null) {
        out.head = copy.next = copy.prev = copy;
      } else {
        out.head.prev.push(copy);
      }
      byteCount -= copy.limit - copy.pos;
      offset = 0;
    }

    return this;
  }

第一個是copyTo()方法,這個方法有兩個重載形式,一個是CopyTo outputStream, 一個是copyTo Buffer, 這里只是介紹第二個作為例子,其實二者形式很相近。拷貝的步驟包括,跳過一定的字節數,然后逐個Segment進行拷貝,這里Segment地方data數組會進行共享。在創建完新的Segment對象以后添加到雙向列表中,就可以完成了數據拷貝任務。

  /** Write {@code byteCount} bytes from this to {@code out}. */
  public Buffer writeTo(OutputStream out, long byteCount) throws IOException {
    if (out == null) throw new IllegalArgumentException("out == null");
    checkOffsetAndCount(size, 0, byteCount);

    Segment s = head;
    while (byteCount > 0) {
      int toCopy = (int) Math.min(byteCount, s.limit - s.pos);
      out.write(s.data, s.pos, toCopy);

      s.pos += toCopy;
      size -= toCopy;
      byteCount -= toCopy;

      if (s.pos == s.limit) {
        Segment toRecycle = s;
        head = s = toRecycle.pop();
        SegmentPool.recycle(toRecycle);
      }
    }

    return this;
  }

要介紹的第二個方法是寫入方法,基本的邏輯就是逐個Segment進行數據寫入,首先計算需要寫入的字節數量,然后寫入到輸出流中,最后更新每個段的pos, lim 屬性,并且回收可以回收的Segment對象。

  private void readFrom(InputStream in, long byteCount, boolean forever) throws IOException {
    if (in == null) throw new IllegalArgumentException("in == null");
    while (byteCount > 0 || forever) {
      Segment tail = writableSegment(1);
      int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);
      int bytesRead = in.read(tail.data, tail.limit, maxToCopy);
      if (bytesRead == -1) {
        if (forever) return;
        throw new EOFException();
      }
      tail.limit += bytesRead;
      size += bytesRead;
      byteCount -= bytesRead;
    }
  }

介紹的第三個方法是讀取方法,首先是獲取一個可以寫入的Segment對象,然后計算讀取的字節數量,然后執行數據讀取,將數據讀取到Segment的data中,最后是更新lim屬性,完成了讀取任務。
Buffer對象同時實現了BufferedSource和BufferedSink兩個接口,在這兩個接口定義的方法中,Buffer都是通過類似的邏輯通過操作雙向鏈表中的Segment對象完成數據的IO任務,有興趣的同學可以自行查看Buffer的源碼。

至此就是完成了對Buffer的分析,當我們了解了Buffer的功能實現,也就明白BufferedSource和BufferedSink的實現方式,這里雖然在使用過程中不會涉及到Buffer的直接操作,更不會涉及Segment的操作,不過這里閱讀其中的源碼可以借鑒的東西還是很多,而且這里只要理解了Segment的操作,就可以較為容易的看懂Buffer的代碼。

除此之外,這里簡單提一下okio中另外一個重要的類, ByteString,從這個類的名字中可以看出它是一個字節串,此外它的實例是不可變的對象,這一點類似于String對象,它底層有一個data[]數組對象,維護數據,延遲初始化uft-8的字符串,兩份數據不會干擾,用空間換取了效率,同時由于它是不可變的對象,在多線程中就具備了安全和效率兩方面的優勢,此外它提供了一系列的api可以完成它與流之間的數據交換,與Buffer之間的數據交換,以及與string等類型之間的轉換,有興趣的同學可以閱讀其源碼,較為簡單可以通讀其代碼,這里不再介紹。

3. Okio的超時機制

okio中使用timeout對象控制I/O的操作的超時。該超時機制使用了時間段(Timeout)和絕對時間點(Deadline)兩種計算超時的方式,可以選擇使用其中一種。下面我們看其源碼,首先看它的屬性:

  private boolean hasDeadline;
  private long deadlineNanoTime;
  private long timeoutNanos;

可以看到Timeout中,使用deadlineNanoTime記錄過期的絕對時間點,使用timeoutNanos記錄過期的一段時間,在Timeout類中的前半部分都是針對這三個屬性的設置和返回方法,可以理解過簡單的getter和setter方法,只不過setter方法返回Timeout對象本身,代碼比較簡單,讀者可以自行查看。
下面是針對超時的處理,第一種是超出deadline時,拋異常:

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

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

代碼邏輯很簡單,到達截止日期時就拋出異常,該方法用在一次I/O操作之后調用,通過調用一次該方法檢查是否超時。該方法只考慮deadline一種時間參考。
第二種方式是使用wait()方式等待一段時間,常用與輸入和輸出同步,比如輸出操作等待輸入一定的時間等,其方法代碼如下:

public final void waitUntilNotified(Object monitor) throws InterruptedIOException {
    try {
      boolean hasDeadline = hasDeadline();
      long timeoutNanos = timeoutNanos();

      //1. 無限期等待
      if (!hasDeadline && timeoutNanos == 0L) {
        monitor.wait(); // There is no timeout: wait forever.
        return;
      }

      //2. Compute how long we'll wait.(計算等待時長)
      long waitNanos;
      long start = System.nanoTime();
      if (hasDeadline && timeoutNanos != 0) {
        long deadlineNanos = deadlineNanoTime() - start;
        waitNanos = Math.min(timeoutNanos, deadlineNanos);
      } else if (hasDeadline) {
        waitNanos = deadlineNanoTime() - start;
      } else {
        waitNanos = timeoutNanos;
      }

      //3. 等待
      // Attempt to wait that long. This will break out early if the monitor is notified.
      long elapsedNanos = 0L;
      if (waitNanos > 0L) {
        long waitMillis = waitNanos / 1000000L;
        monitor.wait(waitMillis, (int) (waitNanos - waitMillis * 1000000L));
        elapsedNanos = System.nanoTime() - start;
      }

      //4. 滿足條件時拋異常
      // Throw if the timeout elapsed before the monitor was notified.
      if (elapsedNanos >= waitNanos) {
        throw new InterruptedIOException("timeout");
      }
    } catch (InterruptedException e) {
      throw new InterruptedIOException("interrupted");
    }
  }

該方法的邏輯流程已經在注釋中說明。首先是處理沒有等待時長的特殊情況,即無限期等待,直到有人喚醒。如果設置了等待時長,則計算時長以后進入等待狀態,并等待一定時間。這里注意,由于該方法常用于輸入和輸出的同步問題,因此這里就會出現兩種可能,一是等待一方被另外一方喚醒,程序繼續執行,此時不超時則不拋異常,正常退出。另外一種可能就是等待超時而不是被另一方喚醒,此時檢查發現超時直接拋出異常。
okio中Timeout的機制較為簡單,主要是throwIfReached()和waitUntilNotified()方法,前者用于在每次執行I/O操作之后調用檢查是否超時,后者則是用于輸入和輸出的同步,需要數據的在某個對象上等待一定時間,數據準備好以后通知,如果超時則會拋出異常。
由于okio庫主要是服務于okhttp用于解決網絡請求的問題,因此對于okio的超時機制,Timeout還有一個子類需要學習,即AsyncTimeout,該類有兩個方法用于包裝輸入和輸出,即source和sink,返回一個包裝了自動檢查超時的輸入輸出對象。下面來看AsyncTimeout的代碼。
AsyncTimeout主要作用是用于包裝輸入輸出流,因此首先從包裝方法source()和sink(),下面來看source()方法:

public final Source source(final Source source) {
    return new Source() {
      @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);
        }
      }
      ...
    }
  }

這里我們只看內部類的read()方法,flush()和close()方法可以自行查看。在read()方法中將可能會超時的操作包含在enter()和exit()之間用于處理超時,下面再來看sink()方法:

public final Sink sink(final Sink sink) {
    return new Sink() {
      @Override public void write(Buffer source, long byteCount) throws IOException {
        checkOffsetAndCount(source.size, 0, byteCount);

        while (byteCount > 0L) {
          //1. 計算寫入的字節長度
          // Count how many bytes to write. This loop guarantees we split on a segment boundary.
          long toWrite = 0L;
          for (Segment s = source.head; toWrite < TIMEOUT_WRITE_SIZE; s = s.next) {
            int segmentSize = source.head.limit - source.head.pos;
            toWrite += segmentSize;
            if (toWrite >= byteCount) {
              toWrite = byteCount;
              break;
            }
          }

          //2.執行一次寫入
          // Emit one write. Only this section is subject to the timeout.
          boolean throwOnTimeout = false;
          enter();
          try {
            sink.write(source, toWrite);
            byteCount -= toWrite;
            throwOnTimeout = true;
          } catch (IOException e) {
            throw exit(e);
          } finally {
            exit(throwOnTimeout);
          }
        }
      }
      ...
    };
  }

從這段代碼來看,首先計算需要寫入的字節長度,然后執行寫入的邏輯,同樣,執行寫入這個可能超時的邏輯也是添加在enter()和exit()方法之間的,同理,flush()和close()方法與之類似,因此對于AsyncTimeout的分析主要是去看enter()和exit()中主要做什么工作,來檢測中間過程的可能發生的超時。
首先來看其域屬性:

  static AsyncTimeout head;

  /** True if this node is currently in the queue. */
  private boolean inQueue;

  /** The next node in the linked list. */
  private AsyncTimeout next;

  /** If scheduled, this is the time that the watchdog should time this out. */
  private long timeoutAt;

其中第一個head是一個屬于類的靜態域,從head和next的名稱上來看,AsyncTimeout是組建一個鏈表或者隊列的節點,而head是一個靜態域,那么說明這是一個全局唯一的隊列或者鏈表,而inQueue標識該節點是否處于該隊列,timeoutAt則記錄該節點超時的時間點。下面我們從enter()方法開始分析:

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

邏輯很簡單,檢查條件,設置狀態屬性,然后調用scheduleTimeout()方法,可以想到該方法是將節點加入隊列的方法,其代碼為:

  private static synchronized void scheduleTimeout(
      AsyncTimeout node, long timeoutNanos, boolean hasDeadline) {
        //1. 控隊列,第一次加入檢測超時對象,初始化head,并開啟看門狗,其實就是一個檢測的線程,后面分析其邏輯
    // Start the watchdog thread and create the head node when the first timeout is scheduled.
    if (head == null) {
      head = new AsyncTimeout();
      new Watchdog().start();
    }
    //2. 計算節點的超時時間點
    long now = System.nanoTime();
    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) {
      node.timeoutAt = now + timeoutNanos;
    } else if (hasDeadline) {
      node.timeoutAt = node.deadlineNanoTime();
    } else {
      throw new AssertionError();
    }
    //3. 將節點加入隊列,按照超時的時間先后順序入隊
    // Insert the node in sorted order.
    long remainingNanos = node.remainingNanos(now);
    for (AsyncTimeout prev = head; true; prev = prev.next) {
      if (prev.next == null || remainingNanos < prev.next.remainingNanos(now)) {
        node.next = prev.next;
        prev.next = node;
        //4. 如果加入的節點位于隊列的第一個,即head之后的節點,則需要喚醒等待的線程(在介紹watchdog部分統一介紹)
        if (prev == head) {
          AsyncTimeout.class.notify(); // Wake up the watchdog when inserting at the front.
        }
        break;
      }
    }
  }

該方法邏輯很清晰,已經用注釋表明,這里先不需要明白喚醒的邏輯,我們只需要明白該方法是將一個檢測可能會超時邏輯操作的AsyncTimeout對象加入隊列中。
下面繼續來看exit()方法,該方法有三種重載形式,這里我們只關心boolean參數和無參數的重載形式,其中前者最終也是調用無參形式,根據返回的結果是否超時,以及參數中是否需要拋異常決定是否拋異常,其代碼如下:

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

所以重點是去看exit()方法是如何判斷操作超時的,其代碼為:

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

設置inQueue屬性后,調用cancelScheduledTimeout(),在該方法中判斷是否超時,并將節點移除隊列:

  private static synchronized boolean cancelScheduledTimeout(AsyncTimeout node) {
    // Remove the node from the linked list.
    for (AsyncTimeout prev = head; prev != null; prev = prev.next) {
      if (prev.next == node) {
        prev.next = node.next;
        node.next = null;
        return false;
      }
    }

    // The node wasn't found in the linked list: it must have timed out!
    return true;
  }

移除節點的邏輯很清晰,只不過這里需要注意判斷超時的條件是看該AsyncTimeout節點是否在隊列中,如果在其中則沒有超時,如果不在其中則已經超時。從這里我們可以猜測watchdog的作用了,它的作用是在一個新的線程中檢測這個隊列的所有節點,當然只需要檢測第一個,即最早結束的即可,如果超時則將該節點移除,所以exit()時就可以判斷一個I/O操作是否超時了。
在明白WatchDog的作用以后,我們可以比較容易地去閱讀它的代碼了

  private static final class Watchdog extends Thread {
    public Watchdog() {
      super("Okio Watchdog");
      //講線程設置為后臺線程,其特點就是開啟它的線程結束以后它會自動結束
      setDaemon(true);
    }

    public void run() {
      while (true) {
        try {
          AsyncTimeout timedOut;
          synchronized (AsyncTimeout.class) {
            //1. 等待
            timedOut = awaitTimeout();
            //2. 等待結束以后,有節點超時
            // Didn't find a node to interrupt. Try again.
            if (timedOut == null) continue;

            //a. 控隊列的特殊情況
            // 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;
            }
          }

          //b. 某個節點已經超時,這里timeOut()方法在AsyncTimeout中是空方法,可以通過覆寫該方法定義超時以后需要處理的邏輯
          // Close the timed out node.
          timedOut.timedOut();
        } catch (InterruptedException ignored) {
        }
      }
    }
  }

這里我們看到它是在一個后臺線程中檢測這個超時隊列,循環檢測,第一步就是執行等待方法,為了便于分析,我們需要首先看這個awaitTimeout()方法:

 static AsyncTimeout awaitTimeout() throws InterruptedException {
    // Get the next eligible node.
    AsyncTimeout node = head.next;

    //在前面看到開啟檢測線程以前一定時初始化head對象的,但是head之后,即隊列的第一個節點可能為空,此時就是空隊列情形
    // The queue is empty. Wait until either something is enqueued or the idle timeout elapses.
    if (node == null) {
      //在控隊列的情況下,等待一個空閑時間,此間沒有入隊的對象將線程喚醒,空閑時間過后如果依然時空隊列,則返回head,則對應上面的a. 特殊情況
      //執行return,結束循環并結束檢測線程
      //如果等待空閑時間內,有節點入隊,此時檢測線程被喚醒,這里返回null, 則上面的while循環會執行下一次循環,去檢測第一個節點
      long startNanos = System.nanoTime();
      AsyncTimeout.class.wait(IDLE_TIMEOUT_MILLIS);
      return head.next == null && (System.nanoTime() - startNanos) >= IDLE_TIMEOUT_NANOS
          ? head  // The idle timeout elapsed.
          : null; // The situation has changed.
    }
    //1. 計算第一個節點的超時剩余時間
    long waitNanos = node.remainingNanos(System.nanoTime());

    // The head of the queue hasn't timed out yet. Await that.
    if (waitNanos > 0) {
      // Waiting is made complicated by the fact that we work in nanoseconds,
      // but the API wants (millis, nanos) in two arguments.
      long waitMillis = waitNanos / 1000000L;
      waitNanos -= (waitMillis * 1000000L);
      //2. 等待一個超時時間
      AsyncTimeout.class.wait(waitMillis, (int) waitNanos);
      //3. 執行到這里有兩種可能,一是等待超時,二是對入隊的節點,并且該入隊的節點在隊列的第一個時,也就是比當前節點還要早結束
      return null;
    }

    //如果隊列的第一個節點已經超時了,則返回該節點,此時會走到上面的b情形,去執行該節點的timeout()方法
    // The head of the queue has timed out. Remove it.
    head.next = node.next;
    node.next = null;
    return node;
  }

關于WatchDog的邏輯已經在代碼中標識,我們需要明確的一點時,檢測線程也就是看門狗線程始終檢測第一個節點(如果不為空的話),而且這一段代碼需要和上面的調用該方法的地方結合來看。這里我們首先看空隊列的特殊情況,此時會等待一段時間,期間如果有入隊的,那么一定加在第一的位置,那么一定會調用notify()方法,前面的enter()方法中提到過,就會喚醒檢測線程,此時條件不成立,則返回null,回到線程的run()方法,發現返回null時則會執行下一次循環,下次循環則會去檢測剛加入隊列的第一個節點的超時情況,如果等待一段時間沒有入隊的節點,超時以后wait()方法退出,此時條件滿足,返回head,同樣在run()方法中我們看到此時清空了head, 并結束了線程,也就是沒有檢測的任務了。
下面繼續分析非空的清空,此時獲取到第一個節點,計算超時的時間,等待一段時間,這里依然有兩種可能,一是有入隊的節點,并且該節點的結束時間還要早,加在了當前節點的前面,那么此時會調用notify()方法,喚醒檢測線程,wait()方法提前結束,此時返回null,回到run()方法依然是執行下一次循環,檢測剛剛加入的新的節點的超時,如果新加入的節點結束時間晚于當前節點,它會加入到隊列的后面,而不會調用notify()方法,這種情況與沒有入隊的情況相同,都是等到當前節點超時,wait()方法退出,依然是返回null,執行下一次循環,而下一次循環時取到了剛剛結束的節點,此時就會返回該節點,返回到run方法中則執行該節點的timeout()方法了。
好了,啰嗦一大堆,所有的情況基本都覆蓋到了,可能畫圖可以更好說明,只是作圖技術欠佳,希望通過文字可以看懂。分析發現在執行I/O操作時,使用了AsyncTimeout,超時以后有可能會立即調用timeout方法(該節點位于第一個),也有可能不會立即調用(該節點位于靠后的位置),只有當前面的節點都移除以后才會輪到該節點。因為一個節點結束的時間點排序,因此后入隊的節點其結束時間通常也會靠后,所以通常不存在一個節點始終存在于隊列中的情況。

4. 總結

因為在學習okhttp的過程中遇到了很多的使用okio執行的I/O操作,因此學習了okio的源碼,該庫十分簡練,是對Java IO的一次成功的封裝。對于okio首先需要明白source和sink接口的定義,明白它們如何將一個數據源包裝成數據流;其次是最為重要的,即buffer類,在構造BufferedSink和BufferedSource時,輸入輸出操作均轉嫁給了buffer,而且buffer在一定意義上也是sink和source所包裝的數據目的地;然后,如果okio為了提升拷貝的效率,使用了Segment的鏈表,通過共享數據,避免了拷貝帶來的消耗,這一部分對使用okio沒有影響,但是很有學習的價值;最后是okio的超時機制,邏輯很簡單,主要是用于檢測輸入輸出的操作超時,不過AsyncTimeout的代碼對于學習非阻塞I/O,線程的同步具有很高的學習價值。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,283評論 6 530
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,947評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,094評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,485評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,268評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,817評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,906評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,039評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,551評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,502評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,662評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,188評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,907評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,304評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,563評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,255評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,637評論 2 370

推薦閱讀更多精彩內容