最近在學習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的整體結構圖,該圖是源自于推薦的第一篇文章中的圖片,這里特別說明。
從圖中我們可以看到結構很清晰,上面是輸出流,下面是輸入流,每個都只有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的結構,其代碼為:
/**
* 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,線程的同步具有很高的學習價值。