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.总结
最后,总结以下几点注意事项,划重点了:
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() 只能调用一次,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对脚本之家网站的支持!
- 这些奇技浮巧,助你优化前端应用性能
- Stepping.js——两步完成前后端分离架构设计
- 我的职业是前端工程师【十】客户端存储艺术:数据存储与模型
- 【开源】2md:将复制的内容、网页转成 markdown
- React Native 持续部署实践— push 代码构建出新版的 Growth
- 技巧 - 如何好一个 Git 提交信息及几种不同的规范
- React、Vue、Ember 及其他前端开发者,请暂缓更新到 Chrome 59 浏览器
- 微软开源全新的文档生成工具DocFX
- 使用 MimeKit 和 MailKit 发送邮件
- 使用 React Native 重写大型 Ionic 应用后,我们想分享一下这八个经验
- 基于OWin的Web服务器Katana发布版本3
- 【工具推荐】图像界的魔术师 ImageMagick
- 使用Metrics.NET 构建 ASP.NET MVC 应用程序的性能指标
- 如何设计完善的构建系统,为日常开发提速一倍
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- OkHttp请求耗时统计
- Ubuntu 18.04 通过 Docker 快速部署 Smokeping 2.6.11 教程
- MySQL 8.0新特性 — 函数索引
- Docker快速上手指北(一)【技术创作101训练营】
- leetcode树之二叉搜索树的最近公共祖先
- 【技术创作101训练营】技术角 | 在CentOS 8上使用Nginx 1.18: 基本配置
- Java诊断应用之Arthas实战(技术创作101训练营)
- 突击并发编程JUC系列-数组类型AtomicLongArray
- 个人量化投资体系搭建(一)
- 服务端的 WebAssembly 与 Rust 入门篇
- pImpl
- Flask+requests发起页面请求示例
- 【技术创作101训练营】Git 如何成功配置SSH key连接多个代码平台?
- 深入浅出iOS内存管理-技术创作101训练营
- 聊聊原型 Prototype | 技术创作101训练营