OkHttp踩坑隨筆為何 response.body().string() 只能調用一次,

來源:互聯網
上載者:User

OkHttp踩坑隨筆為何 response.body().string() 只能調用一次,

想必大家都用過或接觸過 OkHttp,我最近在使用 Okhttp 時,就踩到一個坑,在這兒分享出來,以後大家遇到類似問題時就可以繞過去。

只是解決問題是不夠的,本文將 側重從源碼角度分析下問題的根本,乾貨滿滿。

1.發現問題

在開發時,我通過構造 OkHttpClient 對象發起一次請求並排入佇列,待服務端響應後,回調  Callback 介面觸發  onResponse() 方法,然後在該方法中通過  Response 對象處理返回結果、實現商務邏輯。代碼大致如下:

//註:為聚焦問題,刪除了無關代碼getHttpClient().newCall(request).enqueue(new Callback() {  @Override  public void onFailure(Call call, IOException e) {}  @Override  public void onResponse(Call call, Response response) throws IOException {    if (BuildConfig.DEBUG) {      Log.d(TAG, "onResponse: " + response.body().toString());    }    //解析請求體    parseResponseStr(response.body().string());  }});

在 onResponse() 中,為便於調試,我列印了返回體,然後通過  parseResponseStr() 方法解析返回體(注意:這兒兩次調用了  response.body().string() )。

這段看起來沒有任何問題的代碼,實際運行後卻出了問題:通過控制台看到成功列印了返回體資料(json),但緊接著拋出了異常:

java.lang.IllegalStateException: closed

2.解決問題

檢查代碼後,發現問題出在調用 parseResponseStr() 時,再次使用了  response.body().string() 作為參數。由於當時趕時間,上網查閱後發現  response.body().string() 只能調用一次,於是修改  onResponse() 方法中的邏輯後解決了問題:

getHttpClient().newCall(request).enqueue(new Callback() {  @Override  public void onFailure(Call call, IOException e) {}  @Override  public void onResponse(Call call, Response response) throws IOException {    //此處,先將響應體儲存到記憶體中    String responseStr = response.body().string();    if (BuildConfig.DEBUG) {      Log.d(TAG, "onResponse: " + responseStr);    }    //解析請求體    parseReponseStr(responseStr);  }});

3.結合源碼分析問題

問題解決了,事後還是要分析的。由於之前對 OkHttp 的瞭解僅限於使用,沒有仔細分析過其內部實現的細節,周末抽時間往下看了看,算是弄明白了問題發生的原因。

先分析最直觀的問題:為何 response.body().string() 只能調用一次?

拆解來看,先通過 response.body() 得到  ResponseBody 對象(其是一個抽象類別,在此我們不需要關心具體的實作類別),然後調用  ResponseBody 的  string() 方法得到響應體的內容。

分析後 body() 方法沒有問題,我們往下看  string() 方法:

public final String string() throws IOException { return new String(bytes(), charset().name());}

很簡單,通過指定字元集(charset)將 byte() 方法返回的  byte[] 數組轉為  String 對象,構造沒有問題,繼續往下看  byte() 方法:

public final byte[] bytes() throws IOException { //... BufferedSource source = source(); byte[] bytes; try {  bytes = source.readByteArray(); } finally {  Util.closeQuietly(source); } //... return bytes;}//... 表示刪減了無關代碼,下同。

在 byte() 方法中,通過  BufferedSource 介面對象讀取  byte[] 數組並返回。結合上面提到的異常,我注意到  finally 代碼塊中的  Util.closeQuietly() 方法。excuse me?默默地關閉???

這個方法看起來很詭異有木有,跟進去看看:

public static void closeQuietly(Closeable closeable) { if (closeable != null) {  try {   closeable.close();  } catch (RuntimeException rethrown) {   throw rethrown;  } catch (Exception ignored) {  } }}

原來,上面提到的 BufferedSource 介面,根據代碼文檔注釋,可以理解為 資源緩衝區,其實現了  Closeable 介面,通過複寫  close() 方法來 關閉並釋放資源。接著往下看  close() 方法做了什麼(在當前情境下, BufferedSource 實作類別為  RealBufferedSource ):

//持有的 Source 對象public final Source source;@Overridepublic void close() throws IOException { if (closed) return; closed = true; source.close(); buffer.clear();}

很明顯,通過 source.close() 關閉並釋放資源。說到這兒,  closeQuietly() 方法的作用就不言而喻了,就是關閉  ResponseBody 子類所持有的  BufferedSource 介面對象。

分析至此,我們恍然大悟:當我們第一次調用 response.body().string() 時,OkHttp 將響應體的緩衝資源返回的同時,調用  closeQuietly() 方法默默釋放了資源。

如此一來,當我們再次調用 string() 方法時,依然回到上面的  byte() 方法,這一次問題就出在了  bytes = source.readByteArray() 這行代碼。一起來看看  RealBufferedSource 的  readByteArray() 方法:

@Overridepublic byte[] readByteArray() throws IOException { buffer.writeAll(source); return buffer.readByteArray();}

繼續往下看 writeAll() 方法:

@Overridepublic long writeAll(Source source) throws IOException {  //...  long totalBytesRead = 0;  for (long readCount; (readCount = source.read(this, Segment.SIZE)) != -1; ) {   totalBytesRead += readCount;  }  return totalBytesRead;}

問題出在 for 迴圈的  source.read() 這兒。還記得在上面分析  close() 方法時,其調用了  source.close() 來關閉並釋放資源。那麼,再次調用  read() 方法會發生什麼呢:

@Overridepublic long read(Buffer sink, long byteCount) throws IOException {  //...  if (closed) throw new IllegalStateException("closed");  //...  return buffer.read(sink, toRead);}

至此,與我在前面遇到的崩潰對上了:

java.lang.IllegalStateException: closed

4.OkHttp 為什麼要這麼設計?

通過 fuc*ing the source code ,我們找到了問題的根本,但我還有一個疑問:OkHttp 為什麼要這麼設計?

其實,理解這個問題最好的方式就是查看 ResponseBody 的注釋文檔,正如  JakeWharton 在  issues 中給出的回複:

reply of JakeWharton in okhttp issues

就簡單的一句話: It's documented on ResponseBody. 於是我跑去看類注釋文檔,最後梳理如下:

在實際開發中,響應主體 RessponseBody 持有的資源可能會很大,所以 OkHttp 並不會將其直接儲存到記憶體中,只是持有資料流串連。只有當我們需要時,才會從伺服器擷取資料並返回。同時,考慮到應用重複讀取資料的可能性很小,所以將其設計為 一次性流(one-shot) ,讀取後即 '關閉並釋放資源'。

5.總結

最後,總結以下幾點注意事項,劃重點了:

1.響應體只能被使用一次;

2.響應體必須關閉:值得注意的是,在下載檔案等情境下,當你以  response.body().byteStream()  形式擷取輸入資料流時,務必通過  Response.close()  來手動關閉響應體。

3.擷取響應體資料的方法:使用  bytes()  或  string()  將整個響應讀入記憶體;或者使用  source() ,  byteStream() ,  charStream()  方法以流的形式傳輸資料。

4.以下方法會觸發關閉響應體:

Response.close()Response.body().close()Response.body().source().close()Response.body().charStream().close()Response.body().byteString().close()Response.body().bytes()Response.body().string()

總結

以上所述是小編給大家介紹的OkHttp踩坑隨筆為何 response.body().string() 只能調用一次,希望對大家有所協助,如果大家有任何疑問請給我留言,小編會及時回複大家的。在此也非常感謝大家對幫客之家網站的支援!

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.