I believe everyone has used or come into contact with OkHttp. When I was using Okhttp recently, I stepped on a pit. I will share it here so that everyone can bypass it when they encounter similar problems in the future.
Just a solution Problems are not enough. This article will focus on analyzing the root of the problem from the source code perspective, which is full of useful information.
1. Found the problem
#During development, I initiated a request by constructing the OkHttpClient object and added it to the queue. After the server responded, The Callback interface triggers the onResponse() method, and then uses the Response object to process the return results and implement business logic in this method. The code is roughly as follows:
//注:为聚焦问题,删除了无关代码 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()); } });
In onResponse(), for the convenience of debugging, I printed the return body, and then parsed the return body through the parseResponseStr() method (note: response.body() is called twice here. string() ).
This code, which seems to have no problem, actually has a problem after running: through the console, it can be seen that the return body data (json) is successfully printed, but then an exception is thrown:
java.lang.IllegalStateException: closed
2. Solve the problem
After checking the code, I found that the problem lies in calling parseResponseStr() and using response.body().string again. () as parameter. Because I was in a hurry, I checked online and found that response.body().string() can only be called once, 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 { //此处,先将响应体保存到内存中 String responseStr = response.body().string(); if (BuildConfig.DEBUG) { Log.d(TAG, "onResponse: " + responseStr); } //解析请求体 parseReponseStr(responseStr); } });
3. Combined with the source code analysis problem
After the problem is solved, it still needs to be analyzed afterwards. Since my previous understanding of OkHttp was limited to its use, and I had not carefully analyzed the details of its internal implementation, I took the time to look down on it over the weekend and figured out the cause of the problem.
Let’s first analyze the most intuitive question: Why can response.body().string() only be called once?
Disassembly, first get the ResponseBody object (it is an abstract class, we don’t need to care about the specific implementation class here) through response.body(), and then call the string() method of ResponseBody to get the response body content.
After analysis, there is no problem with the body() method. Let’s look down at the string() method:
public final String string() throws IOException { return new String(bytes(), charset().name()); }
It’s very simple. The byte() method returns the byte[ by specifying the character set (charset). ] The array is converted into a String object. There is no problem with the construction. Continue to look at the byte() method:
public final byte[] bytes() throws IOException { //... BufferedSource source = source(); byte[] bytes; try { bytes = source.readByteArray(); } finally { Util.closeQuietly(source); } //... return bytes; } //... 表示删减了无关代码,下同。
In the byte() method, read the byte[] array through the BufferedSource interface object and return it. Combined with the exception mentioned above, I noticed the Util.closeQuietly() method in the finally code block. excuse me? Close silently? ? ?
This method looks weird. Is it right? Follow up and have a look:
public static void closeQuietly(Closeable closeable) { if (closeable != null) { try { closeable.close(); } 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 documentation comments. Implemented the Closeable interface and closed and released resources by overriding the close() method. Then look down to see what the close() method does (in the current scenario, the BufferedSource implementation class is RealBufferedSource):
//持有的 Source 对象 public final Source source; @Override public void close() throws IOException { if (closed) return; closed = true; source.close(); buffer.clear(); }
Obviously, close and release the resource through source.close(). Speaking of which, the function of the closeQuietly() method is self-evident, which is to close the BufferedSource interface object held by the ResponseBody subclass.
Analysis at this point, we suddenly realize: when we call response.body().string() for the first time, OkHttp returns the buffer resources of the response body and calls the closeQuietly() method to silently release the resources.
In this way, when we call the string() method again, we still return to the byte() method above. This time the problem lies in the bytes = source.readByteArray() line of code. Let’s take a look at the readByteArray() method of RealBufferedSource:
@Override public byte[] readByteArray() throws IOException { buffer.writeAll(source); return buffer.readByteArray(); }
Continue to look at 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; }
The problem lies in the source.read() of the for loop. Remember when analyzing the close() method above, it called source.close() to close and release the resource. So, what happens when the read() method is called again:
@Override public long read(Buffer sink, long byteCount) throws IOException { //... if (closed) throw new IllegalStateException("closed"); //... return buffer.read(sink, toRead); }
At this point, it meets the crash I encountered earlier:
java.lang.IllegalStateException: closed
4.OkHttp Why is it designed like this?
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 problem is to view the annotation documentation of ResponseBody, as JakeWharton responded in issues:
reply of JakeWharton in okhttp issues
In a simple sentence: It's documented on ResponseBody. So I ran to read the class annotation documentation, and finally summarized it as follows:
在实际开发中,响应主体 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()
上面是我整理给大家的,希望今后会对大家有帮助。
相关文章:
The above is the detailed content of Why can't response.body().string() be called multiple times?. For more information, please follow other related articles on the PHP Chinese website!