1. 程式人生 > >深入理解okio的優化思想

深入理解okio的優化思想

隨著越來越多的應用使用OKHttp來進行網路訪問,我們有必要去深入研究OKHTTP的基石,一套更加輕巧方便高效的IO庫okio.

OKIO的優點

有同學或會問,目前Java的IO已經非常成熟了,為什麼還要使用新的IO庫呢?筆者認為,答案有以下幾點:

  1. 低的CPU和記憶體消耗。後面我們會分析到,okio採用了segment的機制進行記憶體共享和複用,儘可能少的去申請記憶體,同時也就降低了GC的頻率。我們知道,過於頻繁的GC會給應用程式帶來效能問題。
  2. 使用方便。在OKIO中,提供了ByteString來處理不變的byte序列,在記憶體上做了優化,不管是從byte[]到String或是從String到byte[],操作都非常輕快,同時還提供瞭如hex字元,base64等工具。而Buffer是處理可變byte序列的利器,它可以根據使用情況自動增長,在使用過程中也不用去關心position等位置的處理。
  3. N合一。Java的原生IO,InputStream/OutputStream, 如果你需要讀取資料,如讀取一個整數,一個布林,或是一個浮點,你需要用DataInputStream來包裝,如果你是作為快取來使用,則為了高效,你需要使用BufferedOutputStream。在OKIO中BufferedSink/BufferedSource就具有以上基本所有的功能,不需要再串上一系列的裝飾類。
  4. 提供了一系列的方便工具,如GZip的透明處理,對資料計算md5、sha1等都提供了支援,對資料校驗非常方便。

OKIO的框架設計

這裡寫圖片描述

OKIO之所以輕量,他的程式碼非常清晰。最重要的兩個介面分別是Source和Sink。

Source

這個介面主要用來讀取資料,而資料的來源可以是磁碟,網路,記憶體等,同時還可以對介面進行擴充套件處理,比如解壓,解密,去掉不需要的網路幀等。

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的子類,我們需要重點關注BufferedSource。它同樣是個介面,不過它提供了更多的操作方法。

這裡寫圖片描述

而RealBufferedSource是它的直接實現類。實現了其所有介面。它們的關係如下。

這裡寫圖片描述

而實際上,RealBufferedSource的實現,是基於Buffer類。這個類我們後面再講

Sink

Sink與Source相似,只不過是寫資料。

public interface Sink extends Closeable, Flushable {
  /** Removes {@code byteCount} bytes from {@code source} and appends them to this. */
  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;
}

同樣,它也有個子類BufferedSink,定義了對資料的所有操作。它的直接類RealBufferedSink也同樣是使用Buffer來完成。

Buffer

Buffer是okio中非常重要的一個類,是整個okio庫的基石,很多的優化思想,都體現在這個類中。不多廢話,我們先看這個類的繼承關係。
這裡寫圖片描述

可以看到,這個Buffer是個集大成者,實現了BufferedSink和BufferedSource的介面,也就是說,即可以從中讀取資料,也可以向裡面寫入資料,其強大之處是毋庸置疑的。在前面提到的okio的優點,如低的cpu消耗,低頻的GC等,都是在這個類中做到的。後面的章節中我將詳細講述。

ByteString

byteString是相對獨立的一個類,也可以看作是一個工具類。它的功能我們看一下方法就一目瞭然。

這裡寫圖片描述

可以看到,有計算md5,sha1的摘要功能,也有大小寫轉換功能,還有十六進位制字元轉換功能等等。這個類我不打算細講,因為非常簡單,不過要提到一點的是它的幾個欄位。

  final byte[] data;
  transient String utf8; // Lazily computed.

由於此類是不可變的(建立後之後不能修改其資料),因些它是以byte[]為基礎。同時又包含了String,雖然是延時初始化,但也是包含了雙倍的字串資料,它的記憶體佔用相對比較大,它適用於不長的字串,又需要頻繁的編碼轉換的,用空間換時間,可以降低CPU的消耗,畢意new String(byte[] data)這樣的開銷還是比較大的。

ByteString還有個子類SegmentedByteString,後面在講Buffer再介紹。

Okio

Okio是入口類,提供一些從JavaAPI到OkioAPI的轉換,其作用是一個介面卡(adapter)。比如從File/Socket建立Sink/Source,從InputStream/OutputStream建立Source/Sink等,這樣我們就把這套API與Java聯絡在一起,可以使用了。

Buffer的設計原理

接下來我們來介紹這個Buffer。
這裡寫圖片描述

Buffer的實現,是通過一個迴圈雙向連結串列來實現的。每一個連結串列元素是一個Segment。

Seqment

final class Segment {
  /** 每一個segment所含資料的大小,固定的 */
  static final int SIZE = 8192;

  /** 用於共享的最小位元組數,後面再解釋 */
  static final int SHARE_MINIMUM = 1024;

  final byte[] data;  

  /** data陣列中下一個讀取的資料的位置 */
  int pos;

  /** data陣列中下一個寫入的資料的位置 */
  int limit;

  /** data陣列被其他segsment所共享的標誌 */
  boolean shared;

  /** 是否是自己是操作者 */
  boolean owner;

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

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

}

在segment中有幾個有意思的方法。

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

當Segment的前一個和自身的資料量都不足一半時,會對segement進行壓縮,把自身的資料寫入到前一個Segment中,然後將自身進行回收。

split

將一個Segment的資料拆成兩個,注意,這裡有trick。如果有兩個Segment相同的位元組超過了SHARE_MINIMUM (1024),那麼這兩個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

這是一個回收池,目前的設計是能存放64K的位元組,即8個Segment。在實際使用中,建議對其進行調整。

final class SegmentPool {
  /** The maximum number of bytes to pool. */
  // TODO: Is 64 KiB a good maximum size? Do we ever have that many idle segments?
  static final long MAX_SIZE = 64 * 1024; // 64 KiB.

  /** Singly-linked list of segments. */
  static Segment next;

  /** Total bytes in this pool. */
  static long byteCount;
    ...
}

講到這裡,整個Buffer的實現原理也就呼之欲出了。

Buffer的寫操作,實際上就是不斷增加Segment的一個過程,讀操作,就是不斷消耗Segment中的資料,如果資料讀取完,則使用SegmentPool進行回收。
當複製記憶體資料時,使用Segment的共享機制,多個Segment共享一份data[]。

Buffer更多的邏輯主要是跨Segment讀取資料,需要把前一個Segment的尾端和後一個Segment的前端拼接在一起,因此看起來程式碼量相對多,但其實開銷非常低。

TimeOut機制

在Okio中定義了一個類叫TimeOut,主要用於判斷時間是否超過閾值,超過之後就丟擲中斷異常。

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

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

有意思的是,定義了一個非同步的Timeout類AsyncTimeout。在其中使用了一個WatchDog的後臺執行緒。而AsyncTimeout本身是以有序連結串列的方式,按照超時的時間進行排序。在其head是一個佔位的AsyncTime,主要用於啟動WatchDog執行緒。這種非同步超時主要可以用在當時間到時,就可以立即獲得通知,不需要等待某阻塞方法返回時,才知道超時了。使用非同步超時,timeout方法在發生超時會進行回撥,需要過載timedOut()方法以處理超時事件。

小結

通過學習Okio的原始碼,我們可以瞭解常用的應用程式優化方法及技術細節。