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 相关文章推荐
js post方式传递提交的实现代码
May 31 Javascript
Javascript(AJAX)解析XML的代码(兼容FIREFOX/IE)
Jul 11 Javascript
Javascript 面试题随笔
Mar 31 Javascript
jQuery性能优化的38个建议
Mar 04 Javascript
js中定义一个变量并判断其是否为空的方法
May 13 Javascript
Jquery中的$.each获取各种返回类型数据的使用方法
May 03 Javascript
Vue 仿百度搜索功能实现代码
Feb 16 Javascript
JS实现小球的弹性碰撞效果
Nov 11 Javascript
vue.js内置组件之keep-alive组件使用
Jul 10 Javascript
js中比较两个对象是否相同的方法示例
Sep 02 Javascript
微信小程序间使用navigator跳转传值问题实例分析
Mar 27 Javascript
原生js实现购物车
Sep 23 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读取数据库信息的几种方法
2008/05/24 PHP
Linux fgetcsv取得的数组元素为空字符串的解决方法
2011/11/25 PHP
PHP基于DOM创建xml文档的方法示例
2017/02/08 PHP
Nigma vs Liquid BO3 第二场2.14
2021/03/10 DOTA
asp.net+js 实现无刷新上传解析csv文件的代码
2010/05/17 Javascript
jQuery实现数字加减效果汇总
2014/12/16 Javascript
javaScript中Math()函数注意事项
2015/06/18 Javascript
浅谈JavaScript的Polymer框架中的事件绑定
2015/07/29 Javascript
使用jQuery给input标签设置默认值
2016/06/20 Javascript
ajax异步请求详解
2017/01/06 Javascript
JavaScript三种绑定事件方式及相互之间的区别分析
2017/01/10 Javascript
详解webpack 如何集成第三方js库
2017/06/29 Javascript
利用jQuery实现简单的拖曳效果实例代码
2017/10/20 jQuery
浅析Visual Studio Code断点调试Vue
2018/02/27 Javascript
详解 javascript对象创建模式
2020/10/30 Javascript
php使用递归与迭代实现快速排序示例
2014/01/23 Python
python实现udp数据报传输的方法
2014/09/26 Python
python抓取网页中图片并保存到本地
2015/12/01 Python
python Django批量导入数据
2016/03/25 Python
Django返回json数据用法示例
2016/09/18 Python
代码分析Python地图坐标转换
2018/02/08 Python
python 直接赋值和copy的区别详解
2019/08/07 Python
Python列表删除元素del、pop()和remove()的区别小结
2019/09/11 Python
常用python爬虫库介绍与简要说明
2020/01/25 Python
tensorflow2.0与tensorflow1.0的性能区别介绍
2020/02/07 Python
Python xlwt模块使用代码实例
2020/06/10 Python
python 绘制场景热力图的示例
2020/09/23 Python
Pycharm在指定目录下生成文件和删除文件的实现
2020/12/28 Python
LINUX下线程,GDI类的解释
2012/04/17 面试题
房地产促销活动方案
2014/03/01 职场文书
团支部建设方案
2014/05/02 职场文书
师德师风的心得体会
2014/09/02 职场文书
学校党的群众路线教育实践活动个人整改方案
2014/10/31 职场文书
2014年人事行政工作总结
2014/12/03 职场文书
2014年办公室人员工作总结
2014/12/09 职场文书
SqlServer常用函数及时间处理小结
2023/05/08 SQL Server