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制作简单弹出层DIV在页面居中 中间显示遮罩的具体方法
Aug 08 Javascript
浅析JavaScript中作用域和作用域链
Dec 06 Javascript
微信小程序 安全包括(框架、功能模块、账户使用)详解
Jan 16 Javascript
jQuery实现一个简单的轮播图
Feb 19 Javascript
详解使用React进行组件库开发
Feb 06 Javascript
在vue中使用SockJS实现webSocket通信的过程
Aug 29 Javascript
详解vue使用插槽分发内容slot的用法
Mar 28 Javascript
Echarts实现多条折线可拖拽效果
Dec 19 Javascript
jQuery实现手风琴效果(蒙版)
Jan 11 jQuery
JS实现横向轮播图(初级版)
Jun 24 Javascript
在Echarts图中给坐标轴加一个标识线markLine
Jul 20 Javascript
Vue——解决报错 Computed property "****" was assigned to but it has no setter.
Dec 19 Vue.js
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使用gettimeofday函数返回当前时间并存放在关联数组里
2015/03/19 PHP
JQuery上传插件Uploadify使用详解及错误处理
2010/04/27 Javascript
jQuery 阴影插件代码分享
2012/01/09 Javascript
jQuery表单域属性过滤器用法分析
2015/02/10 Javascript
基于jQuery实现Div窗口震动特效代码-代码简单
2015/08/28 Javascript
你不知道的高性能JAVASCRIPT
2016/01/18 Javascript
Bootstrap3 Grid system原理及应用详解
2016/09/30 Javascript
求js数组的最大值和最小值的四种方法
2017/03/03 Javascript
Vue的移动端多图上传插件vue-easy-uploader的示例代码
2017/11/27 Javascript
React Native悬浮按钮组件的示例代码
2018/04/05 Javascript
解决微信小程序scroll-view组件无横向滚动的问题
2020/02/04 Javascript
jquery实现上传图片功能
2020/06/29 jQuery
JavaScript使用setTimeout实现倒计时效果
2021/02/19 Javascript
python+selenium开发环境搭建图文教程
2017/08/11 Python
浅谈关于Python3中venv虚拟环境
2018/08/01 Python
pyspark操作MongoDB的方法步骤
2019/01/04 Python
对python多线程中互斥锁Threading.Lock的简单应用详解
2019/01/11 Python
Django框架创建mysql连接与使用示例
2019/07/29 Python
python爬虫模拟浏览器的两种方法实例分析
2019/12/09 Python
keras 自定义loss损失函数,sample在loss上的加权和metric详解
2020/05/23 Python
Python3创建Django项目的几种方法(3种)
2020/06/03 Python
使用py-spy解决scrapy卡死的问题方法
2020/09/29 Python
开水果连锁店创业计划书
2013/12/29 职场文书
农村党支部先进事迹
2014/01/14 职场文书
小学音乐教学反思
2014/02/05 职场文书
信访工作经验交流材料
2014/05/23 职场文书
个人诉讼委托书范本
2014/10/17 职场文书
2014年司法局工作总结
2014/12/11 职场文书
2015年房地产销售工作总结
2015/04/20 职场文书
2015年机械设备管理工作总结
2015/05/04 职场文书
幼儿园园长工作总结2015
2015/05/25 职场文书
海上钢琴师的观后感
2015/06/11 职场文书
紧急迫降观后感
2015/06/15 职场文书
因个人工作失误检讨书
2019/06/21 职场文书
母婴行业实体、电商模式全面解析
2019/08/01 职场文书
教你在 Java 中实现 Dijkstra 最短路算法的方法
2022/04/08 Java/Android