深入理解okio的優化思想
隨著越來越多的應用使用OKHttp來進行網路訪問,我們有必要去深入研究OKHTTP的基石,一套更加輕巧方便高效的IO庫okio.
OKIO的優點
有同學或會問,目前Java的IO已經非常成熟了,為什麼還要使用新的IO庫呢?筆者認為,答案有以下幾點:
- 低的CPU和記憶體消耗。後面我們會分析到,okio採用了segment的機制進行記憶體共享和複用,儘可能少的去申請記憶體,同時也就降低了GC的頻率。我們知道,過於頻繁的GC會給應用程式帶來效能問題。
- 使用方便。在OKIO中,提供了ByteString來處理不變的byte序列,在記憶體上做了優化,不管是從byte[]到String或是從String到byte[],操作都非常輕快,同時還提供瞭如hex字元,base64等工具。而Buffer是處理可變byte序列的利器,它可以根據使用情況自動增長,在使用過程中也不用去關心position等位置的處理。
- N合一。Java的原生IO,InputStream/OutputStream, 如果你需要讀取資料,如讀取一個整數,一個布林,或是一個浮點,你需要用DataInputStream來包裝,如果你是作為快取來使用,則為了高效,你需要使用BufferedOutputStream。在OKIO中BufferedSink/BufferedSource就具有以上基本所有的功能,不需要再串上一系列的裝飾類。
- 提供了一系列的方便工具,如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的原始碼,我們可以瞭解常用的應用程式優化方法及技術細節。