想必你們都用過或接觸過 OkHttp,我最近在使用 Okhttp 時,就踩到一個坑,在這兒分享出來,之後你們遇到相似問題時就能夠繞過去。java
只是解決問題是不夠的,本文將 側重從源碼角度分析下問題的根本,乾貨滿滿。git
在開發時,我經過構造 OkHttpClient
對象發起一次請求並加入隊列,待服務端響應後,回調 Callback
接口觸發 onResponse()
方法,而後在該方法中經過 Response
對象處理返回結果、實現業務邏輯。代碼大體以下:github
//注:爲聚焦問題,刪除了無關代碼 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
這段看起來沒有任何問題的代碼,實際運行後卻出了問題:經過控制檯看到成功打印了返回體數據(json),但緊接着拋出了異常:數組
java.lang.IllegalStateException: closed
檢查代碼後,發現問題出在調用 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); } });
問題解決了,過後仍是要分析的。因爲以前對 OkHttp
的瞭解僅限於使用,沒有仔細分析過其內部實現的細節,週末抽時間往下看了看,算是弄明白了問題發生的緣由。ide
先分析最直觀的問題:爲什麼 response.body().string()
只能調用一次?源碼分析
拆解來看,先經過 response.body()
獲得 ResponseBody
對象(其是一個抽象類,在此咱們不須要關心具體的實現類),而後調用 ResponseBody
的 string()
方法獲得響應體的內容。ui
分析後 body()
方法沒有問題,咱們往下看 string()
方法:this
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
經過 fuc*ing the source code
,咱們找到了問題的根本,但我還有一個疑問:OkHttp 爲何要這麼設計?
其實,理解這個問題最好的方式就是查看 ResponseBody
的註釋文檔,正如 JakeWharton
在 issues
中給出的回覆:
reply of JakeWharton in okhttp issues
就簡單的一句話:**`It's documented on ResponseBody.
`** 因而我跑去看類註釋文檔,最後梳理以下:
在實際開發中,響應主體RessponseBody
持有的資源可能會很大,因此 OkHttp 並不會將其直接保存到內存中,只是持有數據流鏈接。只有當咱們須要時,纔會從服務器獲取數據並返回。同時,考慮到應用重複讀取數據的可能性很小,因此將其設計爲一次性流(one-shot)
,讀取後即 '關閉並釋放資源'。
最後,總結如下幾點注意事項,劃重點了:
response.body().byteStream()
形式獲取輸入流時,務必經過 Response.close()
來手動關閉響應體。bytes()
或 string()
將整個響應讀入內存;或者使用 source()
, byteStream()
, charStream()
方法以流的形式傳輸數據。Response.close() Response.body().close() Response.body().source().close() Response.body().charStream().close() Response.body().byteString().close() Response.body().bytes() Response.body().string()
就醬,又是新的一週,加油!
最後,歡迎關注個人公衆號「伯特說」