首頁  >  文章  >  web前端  >  為何response.body().string()不能實作多次呼叫?

為何response.body().string()不能實作多次呼叫?

亚连
亚连原創
2018-06-13 10:31:022504瀏覽

想必大家都用過或接觸過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;
@Override
public 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() 方法:

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

繼續往下看 writeAll() 方法:

@Override
public 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() 方法會發生什麼事:

@Override
public 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()

上面是我整理给大家的,希望今后会对大家有帮助。

相关文章:

在Javascript中如何实现网页抢红包

详细解读ES6语法中可迭代协议

详细解读在React组件“外”如何使用父组件

微信小程序如何实现涂鸦

以上是為何response.body().string()不能實作多次呼叫?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn