OkHttp解析系列
OkHttp解析(一)從用法看清原理
OkHttp解析(二)網(wǎng)絡連接
OkHttp解析(三)關于Okio
從前兩篇文章我們知道,在OkHttp底層網(wǎng)絡連接是使用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
是非常基礎的毕籽,但在二進制數(shù)據(jù)的處理中抬闯,則沒有與之對應的存在,ByteString
應運而生关筒。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)雅的設計:流(stream)轰异,流可以一層一層套起來岖沛,不斷擴充能力,最終完成像加密和壓縮這樣復雜的操作搭独。這正是“修飾模式”的實踐婴削。
修飾模式,是面向?qū)ο缶幊填I域中牙肝,一種動態(tài)地往一個類中添加新的行為的設計模式唉俗。就功能而言嗤朴,修飾模式相比生成子類更為靈活,這樣可以給某個對象而不是整個類添加一些功能互躬。
Okio 有自己的流類型播赁,那就是 Source
和 Sink
,它們和 InputStream
與 OutputStream
類似吼渡,前者為輸入流容为,后者為輸出流。
它們還有一些新特性:
超時機制寺酪,所有的流都有超時機制坎背;
API 非常簡潔,易于實現(xiàn)寄雀;
Source
和Sink
的 API 非常簡潔得滤,為了應對更復雜的需求,Okio 還提供了BufferedSource
和BufferedSink
接口盒犹,便于使用(按照任意類型進行讀寫懂更,BufferedSource 還能進行查找和判等);不再區(qū)分字節(jié)流和字符流急膀,它們都是數(shù)據(jù)沮协,可以按照任意類型去讀寫;
便于測試卓嫂,
Buffer
同時實現(xiàn)了BufferedSource
(讀) 和BufferedSink
(寫) 接口慷暂,便于測試;
介紹完上面幾個類后晨雳,看個UML圖行瑞,理解他們之間的關系
可以看到Buffer
這里實現(xiàn)了兩個接口,它集 BufferedSource
和 BufferedSink
的功能于一身餐禁,為我們提供了訪問數(shù)據(jù)緩沖區(qū)所需要的一切 API血久。
而這里ReadBufferSource
和ReadBufferSink
雖然各自實現(xiàn)了單獨的接口,但他們內(nèi)部都保存了個成員變量Buffer
帮非,而Buffer
卻涵蓋了兩者洋魂。在ReadBufferSource
和ReadBufferSink
中調(diào)用讀寫實際上是調(diào)用到了Buffer
的讀寫。這種設計有點類似裝飾模式
官方例子
我們來看一下官方文檔中 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
,它則是把相應的十六進制字符串轉(zhuǎn)換為相應的字節(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)換為對應的字節(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)用畦娄?
讓我們從功能需求和設計方案來考慮。
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)絡連接是使用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
打開對應的輸入輸出流保存到BufferedSource source
和BufferedSink
中广凸。
之后再把創(chuàng)建出來的source和sink綁定到HttpStream阅茶,使得HttpStream擁有兩者的調(diào)用。
前一篇文章說到谅海,當Socket連接完成后脸哀,就會根據(jù)source和sink來選擇創(chuà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();
// 讀取響應 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
由蘑,這里就不擴展開了闽寡。