OkHttp踩坑记:为何 response.body().string() 只能调用一次?
想必大家都用过或接触过 OkHttp,我最近在使用 Okhttp 时,就踩到一个坑,在这儿分享出来,以后大家遇到类似问题时就可以绕过去。
只是解决问题是不够的,本文将 侧重从源码角度分析下问题的根本,干货满满。
1.发现问题
在开发时,我通过构造 OkHttpClient
对象发起一次请求并加入队列,待服务端响应后,回调 Callback
接口触发 onResponse()
方法,然后在该方法中通过 Response
对象处理返回结果、实现业务逻辑。代码大致如下:
//注:为聚焦问题,删除了无关代码
getHttpClient().newCall(request).enqueue(new Callback() {@Overridepublic 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() {@Overridepublic 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()
转载于:https://www.cnblogs.com/dongweiq/p/10373100.html
OkHttp踩坑记:为何 response.body().string() 只能调用一次?相关推荐
- Vue + TypeScript + Element 搭建简洁时尚的博客网站及踩坑记
前言 本文讲解如何在 Vue 项目中使用 TypeScript 来搭建并开发项目,并在此过程中踩过的坑 . TypeScript 具有类型系统,且是 JavaScript 的超集,TypeScript ...
- Spring @Transactional踩坑记
@Transactional踩坑记 总述 Spring在1.2引入@Transactional注解, 该注解的引入使得我们可以简单地通过在方法或者类上添加@Transactional注解,实现事务 ...
- go nil json.marshal 完是null_字节跳动踩坑记#3:Go服务灵异panic
这个坑比较新鲜,刚填完,还冒着冷气. - 1 - 在字节跳动,我们服务的所有 log 都通过统一的日志库采集到流式日志服务.落地 ES 集群,配上字节云超(sang)级(xin)强(bing)大(ku ...
- m3u8 video ios h5_移动端H5页面踩坑记
移动端H5页面踩坑记 移动端的样式问题 「1. 安卓 font-weight:700;以上才被认为是加粗.」 「2. border 在 1px 以内是不被安卓识别的一些 0.5px 的下划线怎么搞?」 ...
- 在百度地图中叠加CAD图及GIS数据展示踩坑记
前言 在之前的几篇博客中分别介绍了 在Cesium中实现与CAD的DWG图叠加显示分析 https://www.cnblogs.com/vjmap/p/16541751.html . 高德地图与CAD ...
- 微信小程序踩坑记——ColorUI组件的使用
微信小程序踩坑记--组件的使用 组件类型 ColorUI Vant weapp ColorUI 首先贴上官网链接:官网链接,GitHub链接 简介 ColorUI是一个css库!!!在你引入样式后可以 ...
- GoDB开发踩坑记(代码实现)
前言 之前写了一篇GoDB开发踩坑记但是内容有些不全,所以来补充一下.所以没看过GoDB开发踩坑记的可以先看一下那篇文章. 正文 golang encode_josn--把map[string]int ...
- Go 语言踩坑记——panic 与 recover
题记 Go 语言自发布以来,一直以高性能.高并发著称.因为标准库提供了 http 包,即使刚学不久的程序员,也能轻松写出 http 服务程序. 不过,任何事情都有两面性.一门语言,有它值得骄傲的优点, ...
- python从入门到实践django看不懂_Python编程:从入门到实践踩坑记 Django
<>踩坑记 Django Django Python 19.1.1.5 模板new_topic 做完书上的步骤后,对主题添加页面经行测试,但是浏览器显示 服务器异常. 个人采用的开发环境是 ...
最新文章
- 购买阿里云服务器地域如何选择?
- IE7一样可以轻易重装
- 如何理解Library List
- Spring Boot 集成测试
- oracle中如何分页,Oracle中操作分页
- 单词接龙(信息学奥赛一本通-T1220)
- 常用的sublime text 3插件
- springboot 事务嵌套问题_在springboot中写单元测试解决依赖注入和执行后事务回滚问题...
- springboot情操陶冶-@SpringBootApplication注解解析
- 为什么我在实时编码时失败了?
- jdbc:mysql:replication_使用Mysql的Replication功能实现数据库同步
- 数字温度传感器DS18B20的数据手册理解
- python下载文件的三种方法
- Python 并发简介(多线程、多进程)
- 3GPP LTE/NR信道模型
- 指数型组织:打造独角兽公司的11个最强属性
- 内网地址映射为外网地址方法
- android 色彩搭配,色彩搭配利器:最好用的配色工具App Top5
- 码云上不错的几个支付相关的项目
- A Swift Tour, 苹果新推出的开发语言
热门文章
- Linux个人用户安装JDK(与root用户不同版本)
- Python 技术篇-解决转化后的exe程序被360误杀问题实例演示,python生成的exe被360识别为勒索病毒原因及解决方法
- Python将图片转成pdf(高清)论文党用~
- 【解决方案】无法将grub-efi-amd64-signed软件包安装到/target/【安装Ubuntu】
- Python剑指offer:和为s的连续整数序列
- MATLAB环境认识实验报告,matlab实验报告ljg
- 第五次会议记录:开始进一步需求分析,及初步分工
- fileinput 的总结
- css3动画:animation
- Memcached内存存储