I’m sure you’ve all used or touched on OkHttp. I recently stepped into a pit when using OkHttp. I’ll share it here so you can navigate around similar problems in the future.

Just solve the problem is not enough, this article will focus on the source point of view of the root of the problem, full of dry goods.

1. Identify the problem

During development, I make a request and queue it by constructing OkHttpClient object. After the server responds, the Callback interface triggers onResponse() method, and then the result is returned through the Response object in this method to realize the business logic. The code looks like this:

// Note: Extraneous code was removed to focus on the problem
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());
        }
        // Parse the request bodyparseResponseStr(response.body().string()); }});Copy the code

In onResponse(), I print the return body for debugging purposes, and then parse it through the parseResponseStr() method (note: Response.body ().string() is called twice here).

This code, which appears to have no problems, actually runs into a problem: the console sees that the returned body data (JSON) was successfully printed, but then throws an exception:

java.lang.IllegalStateException: closed
Copy the code

2. Solve problems

When you examine the code, the problem is that parseResponseStr() is called with response.body().string() again. Since I was in a hurry, I found that response.body().string() could only be called once, so I modified the logic in onResponse() to solve the problem:

getHttpClient().newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {}
    
    @Override
    public void onResponse(Call call, Response response) throws IOException {
        // Here, the response body is saved to memory
        String responseStr = response.body().string();
        if (BuildConfig.DEBUG) {
            Log.d(TAG, "onResponse: " + responseStr);
        }
        // Parse the request bodyparseReponseStr(responseStr); }});Copy the code

3. Analyze problems with source code

Once the problem is solved, it has to be analyzed afterwards. Since my previous knowledge of OkHttp was limited to usage and I didn’t have a detailed analysis of the details of its internal implementation, I took some time over the weekend to take a look and figure out why the problem was occurring.

Let’s start with the most straightforward question: why can response.body().string() only be called once?

ResponseBody () gets the ResponseBody object (which is an abstract class, so we don’t need to worry about the implementation class here), and then calls ResponseBody’s String () method to get the content of the response body.

After analyzing the body() method, we can move on to the string() method:

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

Is very simple, with a specified character set (charset) will byte () method returns the byte [] array into a String object, structure is no problem, continue to look down byte () method:

public final byte[] bytes() throws IOException {
  / /...
  BufferedSource source = source();
  byte[] bytes;
  try {
    bytes = source.readByteArray();
  } finally {
    Util.closeQuietly(source);
  }
  / /...
  return bytes;
}
Copy the code

/ /… Extraneous code is deleted, same as below.

In the byte() method, the array of Byte [] is read through the BufferedSource interface object and returned. In conjunction with the exception mentioned above, I notice the util.closequietly () method in the finally code block. Excuse me? Silently shut down??

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

Originally, the BufferedSource interface mentioned above, understood as a resource buffer according to the code documentation, implements the Closeable interface, which overwrites the close() method to close and release the resource. Now look at what the close() method does (in the current scenario, the BufferedSource implementation class is RealBufferedSource) :

// Hold the Source object
public final Source source;

@Override
public void close(a) throws IOException {
  if (closed) return;
  closed = true;
  source.close();
  buffer.clear();
}
Copy the code

Obviously, close and release the resource by source.close(). At this point, the closeQuietly() method is going to close the BufferedSource interface object held by the ResponseBody subclass.

When we call response.body().string() for the first time, OkHttp returns the buffer resource in the response body, while calling closeQuietly frees the resource.

This way, when we call the string() method again, we return to the byte() method above, this time with the bytes = source.readBytearray () line. Take a look at the readByteArray() method of RealBufferedSource:

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

Read on for the writeAll() method:

@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;
}
Copy the code

The problem is with source.read() in the for loop. Remember that when the close() method was analyzed above, it called source.close() to close and free the resource. So what happens when you call the read() method again:

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

This is the breakdown I encountered earlier:

java.lang.IllegalStateException: closed
Copy the code

4. Why is OkHttp designed this way?

By fuC *ing the source code, we found the root of the problem, but I still have a question: why is OkHttp designed this way?

In fact, the best way to understand this is to look at the ResponseBody comment documentation, as JakeWharton wrote in response to issues:

reply of JakeWharton in okhttp issues

It's documented on ResponseBody.

In real development, the resources held by the response body, RessponseBody, can be so large that OkHttp does not store them directly in memory, just hold the data flow connection. Data is retrieved from the server and returned only when we need it. At the same time, considering that it is very unlikely to apply repeated reads, it is designed as a one-shot stream that ‘closes and releases resources’ after reading.

5. To summarize

Finally, the following points for attention are summarized and highlighted:

  1. The response body can only be used once;
  2. The response body must be closed: It is worth noting that in scenarios such as downloading files, when you useresponse.body().byteStream()Form to get the input stream, be sure to passResponse.close()To manually close the response body.
  3. Method of obtaining the response body data: usingbytes()string()Read the entire response into memory; Or usesource().byteStream().charStream()Method transmits data as a stream.
  4. The following methods trigger the closing of the response body:
Response.close()
Response.body().close()
Response.body().source().close()
Response.body().charStream().close()
Response.body().byteString().close()
Response.body().bytes()
Response.body().string()
Copy the code

It’s a new week, come on!


And finally, welcome to my official account “Bert said.”