OkHttp踩坑随笔为何 response.body().string() 只能调用一次


Posted in Javascript onJanuary 08, 2018

想必大家都用过或接触过 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() 只能调用一次,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!

Javascript 相关文章推荐
JavaScript变量声明详解
Nov 27 Javascript
AngularJS入门教程之学习环境搭建
Dec 06 Javascript
JavaScript模拟实现键盘打字效果
Jun 29 Javascript
jQuery插件windowScroll实现单屏滚动特效
Jul 14 Javascript
基于jquery实现表格无刷新分页
Jan 07 Javascript
javascript计时器编写过程与实现方法
Feb 29 Javascript
基于jQuery实现收缩展开功能
Mar 18 Javascript
Bootstrap3.0学习教程之JS折叠插件
May 27 Javascript
AngularJS用户选择器指令实例分析
Nov 04 Javascript
Bootstrap3.3.7导航栏下拉菜单鼠标滑过展开效果
Oct 31 Javascript
深入浅析Vue.js 中的 v-for 列表渲染指令
Nov 19 Javascript
JS创建或填充任意长度数组的小技巧汇总
Oct 24 Javascript
Vue 组件(component)教程之实现精美的日历方法示例
Jan 08 #Javascript
一步步教你利用webpack如何搭一个vue脚手架(超详细讲解和注释)
Jan 08 #Javascript
深入理解 webpack 文件打包机制(小结)
Jan 08 #Javascript
webpack构建的详细流程探底
Jan 08 #Javascript
详解ES6中的代理模式——Proxy
Jan 08 #Javascript
Vue v2.4中新增的$attrs及$listeners属性使用教程
Jan 08 #Javascript
实例解析ES6 Proxy使用场景介绍
Jan 08 #Javascript
You might like
解析PHP实现多进程并行执行脚本
2013/06/18 PHP
php 在线导入mysql大数据程序
2015/06/11 PHP
php封装的mysqli类完整实例
2016/10/18 PHP
使用prototype.js进行异步操作
2007/02/07 Javascript
javascript 数组排序函数
2009/08/20 Javascript
使用js+jquery实现无限极联动
2013/05/23 Javascript
ComboBox 和 DateField 在IE下消失的解决方法
2013/08/30 Javascript
js获取系统的根路径实现介绍
2013/09/08 Javascript
jQuery拖拽div实现思路
2014/02/19 Javascript
jquery 实现两级导航菜单附效果图
2014/03/07 Javascript
js图片翻书效果代码分享
2015/08/20 Javascript
基于JS实现数字+字母+中文的混合排序方法
2016/06/06 Javascript
jQuery实现手机上输入后隐藏键盘功能
2017/01/04 Javascript
详解Angular.js数据绑定时自动转义html标签及内容
2017/03/30 Javascript
微信小程序 转发功能的实现
2017/08/04 Javascript
js实现轮播图的两种方式(构造函数、面向对象)
2017/09/30 Javascript
js循环map 获取所有的key和value的实现代码(json)
2018/05/09 Javascript
node.js 模块和其下载资源的镜像设置的方法
2018/09/06 Javascript
jQuery实现的模仿雨滴下落动画效果
2018/12/11 jQuery
详解如何使用微信小程序云函数发送短信验证码
2019/03/13 Javascript
js实现数字滚动特效
2019/12/16 Javascript
在antd中setFieldsValue和defaultVal的用法
2020/10/29 Javascript
解决js中的setInterval清空定时器不管用问题
2020/11/17 Javascript
进一步探究Python中的正则表达式
2015/04/28 Python
在Python中操作列表之List.append()方法的使用
2015/05/20 Python
Python开发如何在ubuntu 15.10 上配置vim
2016/01/25 Python
Anaconda多环境多版本python配置操作方法
2017/09/12 Python
Python读取MRI并显示为灰度图像实例代码
2018/01/03 Python
python:按行读入,排序然后输出的方法
2019/07/20 Python
matplotlib.pyplot.plot()参数使用详解
2020/07/28 Python
css3 transform及原生js实现鼠标拖动3D立方体旋转
2016/06/20 HTML / CSS
初中教师业务学习材料
2014/05/12 职场文书
教师党员先进性教育自我剖析材料思想汇报
2014/09/24 职场文书
党的群众路线教育实践活动专题组织生活会发言材料
2014/10/17 职场文书
2015年个人自我剖析材料
2014/12/29 职场文书
世界十大动漫制作公司排行榜,迪士尼上榜,第二是美国代表性文化符
2022/03/18 欧美动漫