1 概述
Okio是一個對java.io和java.nio進行補充的庫,使數據訪問,保存和處理變得更容易。
Okio的主要功能是圍繞著ByteString 和 Buffer 兩個類展開的:
1> ByteString是一個immutable的字節序列。在java中,String代表的是字符串,ByteString和String很相似,只不過是用來處理字節串的,同時也提供了常用的操作,比如對數據進行十六進制(hex)、base64 和 UTF-8 格式的編碼和解碼,equals、substring等操作。
2> Buffer是一個mutable的字節序列。 和ArrayList類似,不需要提前設置緩沖區大小。讀取數據和寫入數據和隊列類似,從它的head讀取數據,往它的tail寫入數據,而且不用考慮容量、位置等因素。
java.io設計的一個優雅部分是如何將stream分層以進行加密和壓縮等轉換。 Okio包括自己的stream類型,稱為Source和Sink,和InputStream和OutputStream的工作方式類似,但有一些關鍵的區別:
1> Timeouts: 提供對底層I/O訪問的超時機制。
2> Source和Sink的API非常簡潔,易于實現。
3> 雖然Source和Sink的只提供了三個方法,但是BufferedSource和BufferedSink接口提供了更豐富的方法(比如針對不同類型的read和write方法),以應對更加復雜的場景。
4> 不在區分byte stream和char stream,它們都是數據,可以按照任意類型進行讀寫。
2 Segment和SegmentPool
Segment的源碼不到200行,直接通過源碼來理解Segment的實現原理也是很簡單的,首先來看一下Segment中的所有的字段:
/** Segment可以保存的最大字節數 */
static final int SIZE = 8192;
/** Segment被共享時最小的字節數 */
static final int SHARE_MINIMUM = 1024;
/** Segment中保存數據的字節數組 */
final byte[] data;
/** 字節數組data中被當前Segment實例使用的區間的第一個字節的下標 */
int pos;
/** 字節數組data中被當前Segment實例使用的區間之后的第一個字節的下標 */
int limit;
/** 代表字節數組data是否被 >=2 個Segment實例共用*/
boolean shared;
/** 代表字節數組data中最后一段被使用的區間是不是被當前Segment實例占有*/
boolean owner;
/** 當前Segment實例的后置節點 */
Segment next;
/** 當前Segment實例的前置節點 */
Segment prev;
shared、owner的作用:
在向Segment中寫入數據時,首先用owner判斷當前Segment實例對應的數據區間(字節數組data被使用的區間)之后是否可以寫入數據,接著用shared判斷當前Segment實例對應的數據區間之前是否可以寫入數據,體現在了Segment的writeTo方法中。
接下來依次分析Segment中的方法:
/**
* 從循環雙向鏈表中移除當前Segment實例,返回當前Segment實例的后置節點。
*/
public @Nullable Segment pop() {
Segment result = next != this ? next : null;
prev.next = next;
next.prev = prev;
next = null;
prev = null;
return result;
}
/**
* 在循環雙向鏈表中的當前Segment實例之后插入segment實例,返回被插入的segment實例。
*/
public Segment push(Segment segment) {
segment.prev = this;
segment.next = next;
next.prev = segment;
next = segment;
return segment;
}
上面的兩個方法相信大家一看就明白了,就不再贅敘了。
/**
* 將當前Segment實例中的字節數組data進行分割,從而得到兩個Segment實例.
* 字節數組data中[pos..pos+byteCount)區間的數據屬于第一個segment.
* [pos+byteCount..limit)區間的數據屬于第二個segment.
*
* 返回第一個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;
}
/**
* 當當前Segment實例的前置節點中的空閑空間可以容納當前Segment實例中的數據.
* 則將當前Segment實例中的數據拷貝到前置節點中并且將當前Segment實例回收到SegmentPool中。
*/
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實例中的前byteCount個字節的數據復制放到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方法中用到了SegmentPool.recycle(this)來回收Segment實例,那下面就來講解SegmentPool類,該類的存在就是為了避免GC churn(高頻率的創建和回收Segment實例會導致GC churn)和zero-fill(創建Segment實例時字節數組data需要zero-fill),SegmentPool實例中用一個單向的鏈表來保存回收的Segment實例,首先來看看Segment的源代碼:
/**
* 用于保存被回收的Segment實例,該類的存在就是為了避免GC churn和zero-fill
* SegmentPool實例是線程安全的靜態單例
*/
final class SegmentPool {
/** SegmentPool實例中保存的最大字節數,因此SegmentPool中最多保存8個Segment實例 */
// 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.
/** SegmentPool實例中是通過單向非循環的鏈表來保存數據的,next代表鏈表中的第一個Segment實例 */
static @Nullable Segment next;
/** SegmentPool實例中的字節總數. */
static long byteCount;
private SegmentPool() {
}
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.
}
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;
}
}
}
是不是很簡單,一共也不到70行,一共提供了兩個方法:
Segment take():從SegmentPool實例中獲取被回收的Segment實例,如果SegmentPool實例是空的,則創建一個Segment實例返回。
void recycle(Segment segment):回收segment實例。
3 Buffer
Buffer內部使用Segment的雙向鏈表來保存數據,Segment內部使用字節數組保存數據。 將數據從一個Buffer移動到另一個Buffer時,會通過轉讓Segment的所有權,而不用拷貝數據,從而節省性能上的開銷。下面通過一張圖來描述一下Buffer中雙向循環鏈表和SegmentPool單向非循環鏈表:
下面通過一張類圖來整體的描述一下Buffer:
為了更加清晰的理解上圖,就需要簡單的了解一下裝飾者模式:
1> 定義:Attach additional responsibilities to an object dynamically keeping the same interface.Decorators provide a flexible alternative to subclassing for extending functionality. (動態的給一個對象添加額外的職責。就增加功能來說,裝飾者模式相比生成子類更加靈活。)
2> 裝飾者模式通用類圖
說明一下類圖中的四個角色:
Component抽象組件:Component是一個接口或者是抽象類,在裝飾者模式中,必然有一個最基本、最核心、最原始的接口或抽象類充當Component抽象組件。對應于在Okio框架中的BufferedSource和BufferedSink接口。
ConcreteComponent具體組件:對Component抽象組件的實現,將要被裝飾的類。對應于Okio框架中的Buffer。
Decorator裝飾者:一般是一個抽象類,繼承至Component抽象組件。一定擁有一個指向Component抽象組件的priavte字段。
ConcreteDecorator具體裝飾者:對Decorator裝飾者的實現,用來裝飾ConcreteComponent具體組件。
在Okio框架中沒有細分Decorator和ConcreteDecorator,只有兩個具體裝飾類RealBufferedSource和RealBufferedSink。
Source是用來對數據來源的封裝,Sink是對數據消費的封裝,在Okio工具類中,為Source提供了四種數據來源:Socket、InputStream、File和Path,同樣為Sink提供了四種數據消費:Socket、OutputStream、File和Path,接下來針對Socket舉例分析:
public void testSocket(Socket socket) {
try {
Source source = Okio.source(socket);
BufferedSource bufferedSource = Okio.buffer(source);
bufferedSource.timeout().timeout(500, TimeUnit.MILLISECONDS);
String data = bufferedSource.readString(Charset.forName("UTF-8"));
} catch (IOException e) {
e.printStackTrace();
}
}
上面就是使用Okio框架讀數據的過程,下面我們就來看看源碼中是如何實現的:
// BufferedSource的方法:
@Override public String readString(Charset charset) throws IOException {
if (charset == null) throw new IllegalArgumentException("charset == null");
buffer.writeAll(source);
return buffer.readString(charset);
}
// Buffered的方法:
@Override public long writeAll(Source source) throws IOException {
if (source == null) throw new IllegalArgumentException("source == null");
long totalBytesRead = 0;
for (long readCount; (readCount = source.read(this, Segment.SIZE)) != -1; ) {
totalBytesRead += readCount;
}
return totalBytesRead;
}
@Override public String readString(Charset charset) {
try {
return readString(size, charset);
} catch (EOFException e) {
throw new AssertionError(e);
}
}
@Override public String readString(long byteCount, Charset charset) throws EOFException {
checkOffsetAndCount(size, 0, byteCount);
if (charset == null) throw new IllegalArgumentException("charset == null");
if (byteCount > Integer.MAX_VALUE) {
throw new IllegalArgumentException("byteCount > Integer.MAX_VALUE: " + byteCount);
}
if (byteCount == 0) return "";
Segment s = head;
if (s.pos + byteCount > s.limit) {
// If the string spans multiple segments, delegate to readBytes().
return new String(readByteArray(byteCount), charset);
}
String result = new String(s.data, s.pos, (int) byteCount, charset);
s.pos += byteCount;
size -= byteCount;
if (s.pos == s.limit) {
head = s.pop();
SegmentPool.recycle(s);
}
return result;
}
// Okio的方法:
/**
* Returns a new source that buffers reads from {@code source}. The returned
* source will perform bulk reads into its in-memory buffer. Use this wherever
* you read a source to get an ergonomic and efficient access to data.
*/
public static BufferedSource buffer(Source source) {
return new RealBufferedSource(source);
}
/**
* Returns a source that reads from {@code socket}. Prefer this over {@link
* #source(InputStream)} because this method honors timeouts. When the socket
* read times out, the socket is asynchronously closed by a watchdog thread.
*/
public static Source source(Socket socket) throws IOException {
if (socket == null) throw new IllegalArgumentException("socket == null");
AsyncTimeout timeout = timeout(socket);
Source source = source(socket.getInputStream(), timeout);
return timeout.source(source);
}
private static Source source(final InputStream in, final Timeout timeout) {
if (in == null) throw new IllegalArgumentException("in == null");
if (timeout == null) throw new IllegalArgumentException("timeout == null");
return new Source() {
@Override public long read(Buffer sink, long byteCount) throws IOException {
if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
if (byteCount == 0) return 0;
try {
timeout.throwIfReached();
Segment tail = sink.writableSegment(1);
int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);
int bytesRead = in.read(tail.data, tail.limit, maxToCopy);
if (bytesRead == -1) return -1;
tail.limit += bytesRead;
sink.size += bytesRead;
return bytesRead;
} catch (AssertionError e) {
if (isAndroidGetsocknameError(e)) throw new IOException(e);
throw e;
}
}
@Override public void close() throws IOException {
in.close();
}
@Override public Timeout timeout() {
return timeout;
}
@Override public String toString() {
return "source(" + in + ")";
}
};
}
上面方法的流程可以概括如下:
1> 利用Source的public long read(Buffer sink, long byteCount)方法從Socket輸入流中讀取數據到Buffer實例中。
2> 接著調用Buffer的public String readString(Charset charset)方法將Buffer實例中的數據讀取到String對象中并且返回。
通過Okio框架寫數據的過程與讀數據的過程類似,只不過過程相反,就不再贅敘了。
下面給出Okio框架讀寫String數據的流程圖:
對于Okio框架讀寫其他類型數據也是類似的過程。
在上面的例子中還用到了TimeOut機制,其實Okio實現了兩種超時機制:
1> TimeOut 同步超時機制
利用throwIfReached方法在數據讀取過程中輪詢判斷是否超時。
2> AsyncTimeout 異步超時機制
由于通過Socket來讀寫數據會阻塞線程,所以用的是異步超時機制。
有興趣的同學可以自己閱讀源碼來分析超時機制。