OkHttp解析系列
OkHttp解析(一)從用法看清原理
OkHttp解析(二)網(wǎng)絡(luò)連接
OkHttp解析(三)關(guān)于Okio
從前兩篇文章我們知道,在OkHttp底層網(wǎng)絡(luò)連接是使用Socket,連接成功后則通過Okio庫與遠程socket建立了I/O連接,接著調(diào)用
createTunnel創(chuàng)建代理隧道,在這里HttpStream與Okio建立了I/O連接。本篇文章就來看看Okio的使用
Okio
從最新的Okio上看它的說明
這里介紹到
Okio 補充了
java.io和java.nio的內(nèi)容,使得數(shù)據(jù)訪問、存儲和處理更加便捷。
ByteString and Buffer
Okio則建立在ByteStrings和Buffers上
-
ByteStrings:它是一個不可變的字節(jié)序列,對于字符數(shù)據(jù)來說,
String是非常基礎(chǔ)的,但在二進制數(shù)據(jù)的處理中,則沒有與之對應(yīng)的存在,ByteString應(yīng)運而生。ByteStrings很多方法與String用法一樣,它更容易把一些二進制數(shù)據(jù)當作一個值來處理,它更容易處理一些二進制數(shù)據(jù)。此外它也可以把二進制數(shù)據(jù)編解碼為十六進制(hex),base64和UTF-8格式。
它向我們提供了和String非常類似的 API:獲取字節(jié):指定位置,或者整個數(shù)組;
編解碼:hex,base64,UTF-8;
判等,查找,子串等操作;
Buffer:
Buffer是一個可變的字節(jié)序列,就像ArrayList一樣。我們使用時只管從它的頭部讀取數(shù)據(jù),往它的尾部寫入數(shù)據(jù)就行了,而無需考慮容量、大小、位置等其他因素。
Source and Sink
Okio 吸收了 java.io 一個非常優(yōu)雅的設(shè)計:流(stream),流可以一層一層套起來,不斷擴充能力,最終完成像加密和壓縮這樣復雜的操作。這正是“修飾模式”的實踐。
修飾模式,是面向?qū)ο缶幊填I(lǐng)域中,一種動態(tài)地往一個類中添加新的行為的設(shè)計模式。就功能而言,修飾模式相比生成子類更為靈活,這樣可以給某個對象而不是整個類添加一些功能。
Okio 有自己的流類型,那就是 Source 和 Sink,它們和 InputStream 與 OutputStream 類似,前者為輸入流,后者為輸出流。
它們還有一些新特性:
超時機制,所有的流都有超時機制;
API 非常簡潔,易于實現(xiàn);
Source和Sink的 API 非常簡潔,為了應(yīng)對更復雜的需求,Okio 還提供了BufferedSource和BufferedSink接口,便于使用(按照任意類型進行讀寫,BufferedSource 還能進行查找和判等);不再區(qū)分字節(jié)流和字符流,它們都是數(shù)據(jù),可以按照任意類型去讀寫;
便于測試,
Buffer同時實現(xiàn)了BufferedSource(讀) 和BufferedSink(寫) 接口,便于測試;
介紹完上面幾個類后,看個UML圖,理解他們之間的關(guān)系

可以看到Buffer這里實現(xiàn)了兩個接口,它集 BufferedSource 和 BufferedSink 的功能于一身,為我們提供了訪問數(shù)據(jù)緩沖區(qū)所需要的一切 API。
而這里ReadBufferSource和ReadBufferSink雖然各自實現(xiàn)了單獨的接口,但他們內(nèi)部都保存了個成員變量Buffer,而Buffer卻涵蓋了兩者。在ReadBufferSource和ReadBufferSink中調(diào)用讀寫實際上是調(diào)用到了Buffer的讀寫。這種設(shè)計有點類似裝飾模式
官方例子
我們來看一下官方文檔中 PNG 解碼的例子:
private static final ByteString PNG_HEADER = ByteString.decodeHex("89504e470d0a1a0a");
public void decodePng(InputStream in) throws IOException {
try (BufferedSource pngSource = Okio.buffer(Okio.source(in))) {
ByteString header = pngSource.readByteString(PNG_HEADER.size());
if (!header.equals(PNG_HEADER)) {
throw new IOException("Not a PNG.");
}
...
}
我們先一點一點看,這里有個靜態(tài)成員變量PNG_HEADER,它則是把相應(yīng)的十六進制字符串轉(zhuǎn)換為相應(yīng)的字節(jié)串。
public static ByteString decodeHex(String hex) {
if (hex == null) throw new IllegalArgumentException("hex == null");
if (hex.length() % 2 != 0) throw new IllegalArgumentException("Unexpected hex string: " + hex);
byte[] result = new byte[hex.length() / 2];
for (int i = 0; i < result.length; i++) {
int d1 = decodeHexDigit(hex.charAt(i * 2)) << 4;
int d2 = decodeHexDigit(hex.charAt(i * 2 + 1));
result[i] = (byte) (d1 + d2);
}
return of(result);
}
public static ByteString of(byte... data) {
if (data == null) throw new IllegalArgumentException("data == null");
return new ByteString(data.clone());
}
可以看到,這里把十六進制中每個字符通過decodeHexDigit方法轉(zhuǎn)換為對應(yīng)的字節(jié),再存放到字節(jié)數(shù)組中,最后調(diào)用of方法來創(chuàng)建出ByteString
繼續(xù)看官方例子
public void decodePng(InputStream in) throws IOException {
try (BufferedSource pngSource = Okio.buffer(Okio.source(in))) {
ByteString header = pngSource.readByteString(PNG_HEADER.size());
if (!header.equals(PNG_HEADER)) {
throw new IOException("Not a PNG.");
}
while (true) {
Buffer chunk = new Buffer();
// Each chunk is a length, type, data, and CRC offset.
int length = pngSource.readInt();
String type = pngSource.readUtf8(4);
pngSource.readFully(chunk, length);
int crc = pngSource.readInt();
decodeChunk(type, chunk);
if (type.equals("IEND")) break;
}
}
}
我們先來看下Okio.buffer(Okio.source(in))這里
private static Source source(final InputStream in, final Timeout timeout) {
return new Source() {
@Override public long read(Buffer sink, long byteCount) throws IOException {
...
}
...
};
}
public static BufferedSource buffer(Source source) {
return new RealBufferedSource(source);
}
可以看到,首先調(diào)用Okio.source(in)把InputStream輸入流轉(zhuǎn)換為Source,接著調(diào)用buffer方法創(chuàng)建了RealBufferedSource它實現(xiàn)了BufferSource方法。
此時這個pngSource則代表了圖片的輸入流信息
接著調(diào)用ByteString header = pngSource.readByteString(PNG_HEADER.size());
來讀取圖片首部的字節(jié)串
@Override public ByteString readByteString(long byteCount) throws IOException {
require(byteCount);
return buffer.readByteString(byteCount);
}
可以看到,最終的讀取轉(zhuǎn)換則是通過Buffer來進行調(diào)用。而Buffer同樣也實現(xiàn)了和ReadBufferdSource的接口BufferedSource。
為什么要這么折騰呢?明明可以簡單的調(diào)用ReadBufferedSource為什么還要通過Buffer來調(diào)用?
讓我們從功能需求和設(shè)計方案來考慮。
BufferedSource 要提供各種形式的讀取操作,還有查找與判等操作。大家可能會想,那我就在實現(xiàn)類中自己實現(xiàn)不就好了嗎?干嘛要經(jīng)過 Buffer 中轉(zhuǎn)呢?這里我們實現(xiàn)的時候,需要考慮效率的問題,而且不僅 BufferedSource 需要高效實現(xiàn),BufferedSink 也需要高效實現(xiàn),這兩者的高效實現(xiàn)技巧,很大部分都是共通的,所以為了避免同樣的邏輯重復兩遍,Okio 就直接把讀寫操作都實現(xiàn)在了 Buffer 這一個類中,這樣邏輯更加緊湊,更加內(nèi)聚。而且還能直接滿足我們對于“兩用數(shù)據(jù)緩沖區(qū)”的需求:既可以從頭部讀取數(shù)據(jù),也能向尾部寫入數(shù)據(jù)。至于我們單獨的讀寫操作需求,Okio 就為 Buffer 分別提供了委托類:RealBufferedSource 和 RealBufferedSink,實現(xiàn)好 Buffer 之后,它們兩者的實現(xiàn)將非常簡潔(前者 450 行,后者 250 行)。
OkHttp里面Okio的使用
前面說到
在OkHttp底層網(wǎng)絡(luò)連接是使用Socket,連接成功后則通過Okio庫與遠程socket建立了I/O連接,接著調(diào)用
createTunnel創(chuàng)建代理隧道,在這里HttpStream與Okio建立了I/O連接。
我們直接定位到RealConnection.connectSocket方法這里
private void connectSocket(int connectTimeout, int readTimeout) throws IOException {
Proxy proxy = route.proxy();
Address address = route.address();
rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
? address.socketFactory().createSocket()
: new Socket(proxy);
rawSocket.setSoTimeout(readTimeout);
try {
Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
} catch (ConnectException e) {
throw new ConnectException("Failed to connect to " + route.socketAddress());
}
source = Okio.buffer(Okio.source(rawSocket));
sink = Okio.buffer(Okio.sink(rawSocket));
}
可以看到,這里根據(jù)挑選出來的線路代理,創(chuàng)建完Socket后,調(diào)用了連接,連接成功后,則使用Okio.source和Okio.sink打開對應(yīng)的輸入輸出流保存到BufferedSource source和BufferedSink中。
之后再把創(chuàng)建出來的source和sink綁定到HttpStream,使得HttpStream擁有兩者的調(diào)用。
前一篇文章說到,當Socket連接完成后,就會根據(jù)source和sink來選擇創(chuàng)建對應(yīng)HttpStream
public HttpStream newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {
...
HttpStream resultStream;
if (resultConnection.framedConnection != null) {
resultStream = new Http2xStream(client, this, resultConnection.framedConnection);
} else {
resultConnection.socket().setSoTimeout(readTimeout);
resultConnection.source.timeout().timeout(readTimeout, MILLISECONDS);
resultConnection.sink.timeout().timeout(writeTimeout, MILLISECONDS);
resultStream = new Http1xStream(
client, this, resultConnection.source, resultConnection.sink);
...
}
之后就可以進行讀取和寫入數(shù)據(jù)。
寫入數(shù)據(jù)的話,由第一篇文章可知道是在CallServerInterceptor中,在里面寫入我們的請求體
// CallServerInterceptor#intercept
// 發(fā)送請求 body
Sink requestBodyOut = httpCodec.createRequestBody(request,
request.body().contentLength());
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
// 讀取響應(yīng) body
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))
.build();
可以看到在這里,先調(diào)用了createRequestBody來根據(jù)request創(chuàng)建一個Sink不過此時還未寫入數(shù)據(jù),里面只是空的,只是根據(jù)request來選擇創(chuàng)建Sink而已
@Override public Sink createRequestBody(Request request, long contentLength) {
if ("chunked".equalsIgnoreCase(request.header("Transfer-Encoding"))) {
// Stream a request body of unknown length.
return newChunkedSink();
}
if (contentLength != -1) {
// Stream a request body of a known length.
return newFixedLengthSink(contentLength);
}
...
}
接著把創(chuàng)建好的Sink包裝到BufferedSink中,最終調(diào)用request.body().writeTo(bufferedRequestBody);來把自己的請求體寫入BufferedSink,這里也就是寫入到Socket里面了。
同理,讀取數(shù)據(jù)到Response則是使用BufferedSource,這里就不擴展開了。