OkHttp踩坑記:爲何 response.body().string() 只能調用一次?

原文鏈接:https://blog.csdn.net/my_truelove/article/details/80133556

轉載自   https://blog.csdn.net/my_truelove/article/details/80133556
原文鏈接:https://blog.csdn.net/my_truelove/article/details/80133556

想必大家都用過或接觸過 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

    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?默默地關閉???
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
————————————————
版權聲明:本文爲CSDN博主「伯特」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/my_truelove/article/details/80133556

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章