OkHttp踩坑记:为何 response.body().string() 只能调用一次?
2018-01-08 01:00
621 查看
想必大家都用过或接触过 OkHttp,我最近在使用 Okhttp 时,就踩到一个坑,在这儿分享出来,以后大家遇到类似问题时就可以绕过去。
只是解决问题是不够的,本文将 侧重从源码角度分析下问题的根本,干货满满。
在
这段看起来没有任何问题的代码,实际运行后却出了问题:通过控制台看到成功打印了返回体数据(json),但紧接着抛出了异常:
先分析最直观的问题:为何
拆解来看,先通过
分析后
很简单,通过指定字符集(charset)将
在
这个方法看起来很诡异有木有,跟进去看看:
原来,上面提到的
很明显,通过
分析至此,我们恍然大悟:当我们第一次调用
如此一来,当我们再次调用
继续往下看
问题出在
至此,与我在前面遇到的崩溃对上了:
其实,理解这个问题最好的方式就是查看
reply of JakeWharton in okhttp issues
就简单的一句话:
在实际开发中,响应主体
1. 响应体只能被使用一次;
2. 响应体必须关闭:值得注意的是,在下载文件等场景下,当你以
3. 获取响应体数据的方法:使用
4. 以下方法会触发关闭响应体:
就酱,又是新的一周,加油!
============================
最后,欢迎关注我的公众号「伯特说」
只是解决问题是不够的,本文将 侧重从源码角度分析下问题的根本,干货满满。
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
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?默默地关闭???
这个方法看起来很诡异有木有,跟进去看看:
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
就简单的一句话:
It's documented on ResponseBody.于是我跑去看类注释文档,最后梳理如下:
在实际开发中,响应主体
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()
就酱,又是新的一周,加油!
============================
最后,欢迎关注我的公众号「伯特说」
相关文章推荐
- OkHttp踩坑随笔为何 response.body().string() 只能调用一次
- 为何 response.body().string() 只能调用一次
- OkHttp中 response.body().string()只能调用一次分析~~~
- OkHttp请求回调中response.body().string()只能有效调用一次问题
- OkHttp请求回调中response.body().string()只能有效调用一次,为什么?
- OkHttp请求回调中response.body().string()只能有效调用一次
- 使用AJAX调用Spring controller 并返回map对象,@ResponseBody 只能返回string的解决方案:
- Spring @ResponseBody只能返回String类型数据解决办法
- OkHttp请求回调中response.body().string()如何调用两次
- @ResponseBody修饰String和对象,前台接收的格式
- Spring MVC中默认的ResponseBody为String的乱码问
- ExtJS Button调用Ext.window.Window后,只能点开一次,第二次就点不开
- springmvc Spring3 MVC @ResponseBody返回,jquery ajax调用中文乱码问题解决
- Spring Mvc @ResponseBody String返回中文字符串乱码
- 人生犹如一本书,愚蠢者草草翻过,聪明人细细阅读。为何如此,因为他们只能读它一次。
- Spring MVC中默认的ResponseBody为String的乱码问
- Android异常:ResponseBody.string()导致的OutofMemory、IllegalStateException异常
- @ResponseBody注解使用返回类型为String时出现中文乱码
- python远程发送syslog到某个机器上514端口,并注意addHandler()在循环执行时,调用只能一次
- Spring MVC中默认的ResponseBody为String的乱码问