I believe everyone has used or contacted OkHttp. I recently stepped on a pit when using Okhttp and shared it here. In the future, you can get around when you encounter similar problems.
It is not enough to just solve the problem. This article will focus on analyzing the root of the problem from the source code perspective, and it is full of practical information.
1. Discover problems
During development, I initiated a request and added to the queue by constructing the OkHttpClient object. After the server responds, the callback interface triggers the onResponse() method, and then the return result is returned through the Response object in this method and implemented the business logic. The code is roughly as follows:
//Note: In order to focus on the issue, irrelevant code was deleted.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 () { (TAG, "onResponse: " + ().toString()); } //Solve the request body parseResponseStr(().string()); } });
In onResponse(), for easy debugging, I printed the return body and then parsed the return body through the parseResponseStr() method (note: ().string() was called twice here).
This code that seems to have no problem, but after actually running it, there is a problem: I saw through the console that the return volume data (json) was successfully printed, but then an exception was thrown:
: closed
2. Solve the problem
After checking the code, I found that the problem was when calling parseResponseStr(), and I used ().string() as the parameter again. Because I was in a hurry at that time, I found that ().string() could only be called once after checking it online. So I modified the logic in the onResponse() method and solved 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, save the response body to memory first String responseStr = ().string(); if () { (TAG, "onResponse: " + responseStr); } //Solve the request body parseReponseStr(responseStr); } });
3. Combined with source code analysis problems
The problem has been solved, and it still needs to be analyzed later. Since my previous understanding of OkHttp is limited to use and I have not carefully analyzed the details of its internal implementation, I took the time to look down on the weekend and figured out the reason for the problem.
Let’s analyze the most intuitive question first: Why can ().string() be called only once?
From the disassembly, first obtain the ResponseBody object (it is an abstract class, and we don’t need to care about the specific implementation class here), and then call the ResponseBody’s string() method to obtain the content of the response body.
After analysis, there is no problem with the body() method. Let's look at the string() method:
public final String string() throws IOException { return new String(bytes(), charset().name()); }
It is very simple. By specifying the character set (charset), the byte[] array returned by the byte() method is converted into a String object. There is no problem with the construction. Continue to read the byte() method:
public final byte[] bytes() throws IOException { //... BufferedSource source = source(); byte[] bytes; try { bytes = (); } finally { (source); } //... return bytes; } //... Indicates that irrelevant code has been deleted,Same below。
In the byte() method, the byte[] array is read and returned through the BufferedSource interface object. Combining the exception mentioned above, I noticed the () method in the finally code block. excuse me? Closed silently? ? ?
This method looks weird or not. Follow in and take a look:
public static void closeQuietly(Closeable closeable) { if (closeable != null) { try { (); } catch (RuntimeException rethrown) { throw rethrown; } catch (Exception ignored) { } } }
It turns out that the BufferedSource interface mentioned above can be understood as a resource buffer according to the code document comments, which implements the Closeable interface, and close and free resources by rewriting the close() method. Then look at what the close() method does (in the current scenario, the BufferedSource implementation class is RealBufferedSource):
//Hold Source objectpublic final Source source; @Override public void close() throws IOException { if (closed) return; closed = true; (); (); }
It is obvious that by () close and free up resources. Speaking of this, the role of the closeQuietly() method is self-evident, which is to close the BufferedSource interface object held by the ResponseBody subclass.
At this point, we suddenly realized: When we called ().string() for the first time, OkHttp returned the buffer resource of the response body and called the closeQuietly() method to silently release the resource.
In this way, when we call the string() method again, we still return to the above byte() method. This time the problem lies in the line of code bytes = (). Let's take a look at the readByteArray() method of RealBufferedSource:
@Override public byte[] readByteArray() throws IOException { (source); return (); }
Continue to read the writeAll() method:
@Override public long writeAll(Source source) throws IOException { //... long totalBytesRead = 0; for (long readCount; (readCount = (this, )) != -1; ) { totalBytesRead += readCount; } return totalBytesRead; }
The problem lies in the for loop () here. Remember when analyzing the close() method above, it called () to close and release 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 (sink, toRead); }
At this point, it matches the crash I encountered before:
: closed
Why do you need to design this way?
Through fuc*ing the source code, we found the root of the problem, but I still have a question: Why is OkHttp designed like this?
In fact, the best way to understand this problem is to check the comment document of ResponseBody, as JakeWharton gave in issues:
reply of JakeWharton in okhttp issues
In a simple sentence: It's documented on ResponseBody. So I went to read the class comment document and finally sorted out the following:
In actual development, the resource held by the response body ResponseBody may be very large, so OkHttp does not save it directly into memory, but only holds data flow connections. Only when we need it, the data will be fetched from the server and returned. At the same time, considering that the application has a small possibility to read data repeatedly, it is designed as a one-shot, and after reading it, it is 'closed and freed up resources'.
5. Summary
Finally, we will summarize the following precautions and highlight the key points:
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 obtain the input stream in the form of ().byteStream(), be sure to manually close the response body through () .
3. Methods to obtain response body data: use bytes() or string() to read the entire response into memory; or use the source() , byteStream() , charStream() methods to transmit data in a stream.
4. The following method will trigger the closing of the response body:
() ().close() ().source().close() ().charStream().close() ().byteString().close() ().bytes() ().string()
Summarize
The above is the editor’s introduction to why OkHttp essay ().string() can only be called once. I hope it will be helpful to everyone. If you have any questions, please leave me a message and the editor will reply to everyone in time. Thank you very much for your support for my website!