想必大家都用過或接觸過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()
上面是我整理给大家的,希望今后会对大家有帮助。
相关文章:
以上是為何response.body().string()不能實作多次呼叫?的詳細內容。更多資訊請關注PHP中文網其他相關文章!