OkHttp踩坑记:为何 response.body().string() 只能调用一次?
想必大家都用过或接触过 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
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.总结
最后,总结以下几点注意事项,划重点了:
- 响应体只能被使用一次;
- 响应体必须关闭:值得注意的是,在下载文件等场景下,当你以
response.body().byteStream()
形式获取输入流时,务必通过Response.close()
来手动关闭响应体。 - 获取响应体数据的方法:使用
bytes()
或string()
将整个响应读入内存;或者使用source()
,byteStream()
,charStream()
方法以流的形式传输数据。 - 以下方法会触发关闭响应体:
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() 只能调用一次?的更多相关文章
- Vue + TypeScript + Element 搭建简洁时尚的博客网站及踩坑记
前言 本文讲解如何在 Vue 项目中使用 TypeScript 来搭建并开发项目,并在此过程中踩过的坑 . TypeScript 具有类型系统,且是 JavaScript 的超集,TypeScript ...
- Spark踩坑记——Spark Streaming+Kafka
[TOC] 前言 在WeTest舆情项目中,需要对每天千万级的游戏评论信息进行词频统计,在生产者一端,我们将数据按照每天的拉取时间存入了Kafka当中,而在消费者一端,我们利用了spark strea ...
- Spark踩坑记——从RDD看集群调度
[TOC] 前言 在Spark的使用中,性能的调优配置过程中,查阅了很多资料,之前自己总结过两篇小博文Spark踩坑记--初试和Spark踩坑记--数据库(Hbase+Mysql),第一篇概况的归纳了 ...
- djangorestframework+vue-cli+axios,为axios添加token作为headers踩坑记
情况是这样的,项目用的restful规范,后端用的django+djangorestframework,前端用的vue-cli框架+webpack,前端与后端交互用的axios,然后再用户登录之后,a ...
- 记一次 Spring 事务配置踩坑记
记一次 Spring 事务配置踩坑记 问题描述:(SpringBoot + MyBatisPlus) 业务逻辑伪代码如下.理论上,插入数据 t1 后,xxService.getXxx() 方法的查询条 ...
- Spark踩坑记:Spark Streaming+kafka应用及调优
前言 在WeTest舆情项目中,需要对每天千万级的游戏评论信息进行词频统计,在生产者一端,我们将数据按照每天的拉取时间存入了Kafka当中,而在消费者一端,我们利用了spark streaming从k ...
- Spark踩坑记——数据库(Hbase+Mysql)
[TOC] 前言 在使用Spark Streaming的过程中对于计算产生结果的进行持久化时,我们往往需要操作数据库,去统计或者改变一些值.最近一个实时消费者处理任务,在使用spark streami ...
- 【踩坑记】从HybridApp到ReactNative
前言 随着移动互联网的兴起,Webapp开始大行其道.大概在15年下半年的时候我接触到了HybridApp.因为当时还没毕业嘛,所以并不清楚自己未来的方向,所以就投入了HybridApp的怀抱. Hy ...
- Spark踩坑记——共享变量
[TOC] 前言 Spark踩坑记--初试 Spark踩坑记--数据库(Hbase+Mysql) Spark踩坑记--Spark Streaming+kafka应用及调优 在前面总结的几篇spark踩 ...
随机推荐
- hdu 1010 走到终点时刚好花掉所有时间 (DFS + 奇偶性剪枝 )
题意:输入一个n*m的迷宫,和一个T:可以在迷宫中生存的最大时间.S为起点,D为终点.并且,每个格子只能踩一次,且只能维持一秒,然后该块地板就会塌陷.所以你必须每秒走一步,且到D点时,所用时间为T.用 ...
- vtiger7新模块的创建和配置
vtiger出7.0了,以前的那些配置方法已经不管用了 下面是新的 模块创建及一些页面及功能配置的方法 下面介绍三个点 1.新建一个模块 2.实现单图片上传的功能 3.实现页面summary显示的功能 ...
- (APIO2014)序列分割
题解: 我也不知道为啥上午上课讲了我昨天看的3题 这题关键在于发现操作顺序无关的 可以发现最终答案是任意两段乘积的和 那这个东西显然是可以dp的 然后可以斜率优化一波 nklongn 另外上课讲的是当 ...
- python全栈开发day47-jqurey
一.昨日内容回顾 二.今日内容总结 1.jquery的介绍 1).为什么要用jquery? # window.onload 事件有事件覆盖的问题,因此只能写一个事件. # 代码容错性差 # 浏览器兼容 ...
- 【Java】 剑指offer(53-1) 数字在排序数组中出现的次数
正文 本文参考自<剑指offer>一书,代码采用Java语言. 更多:<剑指Offer>Java实现合集 题目 统计一个数字在排序数组中出现的次数.例如输入排序数组{1, ...
- Unity 之 添加背景音乐 以及 Slider控制
游戏音频分为背景音乐与环境音乐两种.Audio Clip(音频剪辑)有四种音乐格式.MP3:适合较长音频,作为背景音乐.Ogg:适合较长音频,作为背景音乐.Wav:适合较短音频,作为环境音乐.Ai ...
- mac电脑对ntfs格式硬盘进行写操作(简单说就是向ntfs硬盘拷贝东西)
使用mac电脑的童鞋应该都会遇到一个问题: 对ntfs格式的优盘或硬盘(移动硬盘默认ntfs)只能读不能写,也就是只能拷贝出数据,却没法拷贝数据到移动硬盘中. 下面是参考自网上的一种方法,无需第三方软 ...
- HQL count(*)
public int getTarPage() { String hql = "'"; Query query = getSession().creat ...
- ab,qps 并发连接数
并发连接数 = pv / 统计时间 * 页面衍生连接次数 * http响应时间 * 因数 / 其他web服务器 数量 pv = 并发连接数 * 统计时间 * 其他web服务器 数量/ 页面衍生连接次数 ...
- 弗洛伊德算法Floyed(求各顶点间最短路径):可打印最短路径
#include <iostream> #include <string> #include <iomanip> using namespace std; #def ...