OkHttp解析(三)關于Okio

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.iojava.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 有自己的流類型播赁,那就是 SourceSink,它們和 InputStreamOutputStream 類似吼渡,前者為輸入流容为,后者為輸出流。

它們還有一些新特性:

  • 超時機制寺酪,所有的流都有超時機制坎背;

  • API 非常簡潔,易于實現(xiàn)寄雀;

  • SourceSink 的 API 非常簡潔得滤,為了應對更復雜的需求,Okio 還提供了 BufferedSourceBufferedSink 接口盒犹,便于使用(按照任意類型進行讀寫懂更,BufferedSource 還能進行查找和判等);

  • 不再區(qū)分字節(jié)流和字符流急膀,它們都是數(shù)據(jù)沮协,可以按照任意類型去讀寫;

  • 便于測試卓嫂,Buffer 同時實現(xiàn)了 BufferedSource(讀) 和 BufferedSink(寫) 接口慷暂,便于測試;

介紹完上面幾個類后晨雳,看個UML圖行瑞,理解他們之間的關系

Okio類圖

可以看到Buffer這里實現(xiàn)了兩個接口,它集 BufferedSourceBufferedSink 的功能于一身餐禁,為我們提供了訪問數(shù)據(jù)緩沖區(qū)所需要的一切 API血久。
而這里ReadBufferSourceReadBufferSink雖然各自實現(xiàn)了單獨的接口,但他們內(nèi)部都保存了個成員變量Buffer帮非,而Buffer卻涵蓋了兩者洋魂。在ReadBufferSourceReadBufferSink中調(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.sourceOkio.sink打開對應的輸入輸出流保存到BufferedSource sourceBufferedSink中广凸。

之后再把創(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由蘑,這里就不擴展開了闽寡。

參考資料

拆輪子系列:拆 Okio
官方Okio

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市尼酿,隨后出現(xiàn)的幾起案子爷狈,更是在濱河造成了極大的恐慌,老刑警劉巖裳擎,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件涎永,死亡現(xiàn)場離奇詭異,居然都是意外死亡鹿响,警方通過查閱死者的電腦和手機羡微,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來惶我,“玉大人妈倔,你說我怎么就攤上這事〕窆保” “怎么了盯蝴?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長听怕。 經(jīng)常有香客問我捧挺,道長,這世上最難降的妖魔是什么尿瞭? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任松忍,我火速辦了婚禮,結(jié)果婚禮上筷厘,老公的妹妹穿的比我還像新娘鸣峭。我一直安慰自己,他們只是感情好酥艳,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布摊溶。 她就那樣靜靜地躺著,像睡著了一般充石。 火紅的嫁衣襯著肌膚如雪莫换。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天骤铃,我揣著相機與錄音拉岁,去河邊找鬼。 笑死惰爬,一個胖子當著我的面吹牛喊暖,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播撕瞧,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼陵叽,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了丛版?” 一聲冷哼從身側(cè)響起巩掺,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎页畦,沒想到半個月后胖替,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡豫缨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年独令,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片州胳。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡记焊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出栓撞,到底是詐尸還是另有隱情遍膜,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布瓤湘,位于F島的核電站瓢颅,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏弛说。R本人自食惡果不足惜挽懦,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望木人。 院中可真熱鬧信柿,春花似錦冀偶、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至形病,卻和暖如春客年,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背漠吻。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工量瓜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人途乃。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓绍傲,卻偏偏與公主長得像,于是被迫代替她去往敵國和親欺劳。 傳聞我的和親對象是個殘疾皇子唧取,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容