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面向对象之静态与非静态类
Feb 03 Javascript
THREE.JS入门教程(2)着色器-上
Jan 24 Javascript
CSS3,HTML5和jQuery搜索框集锦
Dec 02 Javascript
javascript事件模型实例分析
Jan 30 Javascript
JavaScript 动态加载脚本和样式的方法
Apr 13 Javascript
JavaScript_ECMA5数组新特性详解
Jun 12 Javascript
JQuery控制DIV的选取实现方法
Sep 18 Javascript
Node.js中使用mongoose操作mongodb数据库的方法
Sep 12 Javascript
json数据传到前台并解析展示成列表的方法
Aug 06 Javascript
使用Vue开发自己的Chrome扩展程序过程详解
Jun 21 Javascript
微信小程序实现弹框效果
May 26 Javascript
Javascript生成器(Generator)的介绍与使用
Jan 31 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/08/06 PHP
总结PHP中DateTime的常用方法
2016/08/11 PHP
php技巧小结【推荐】
2017/01/19 PHP
JavaScript 闭包深入理解(closure)
2009/05/27 Javascript
动态读取JSON解析键值对的方法
2014/06/03 Javascript
jQuery实现table隔行换色和鼠标经过变色的两种方法
2014/06/15 Javascript
DOM节点删除函数removeChild()用法实例
2015/01/12 Javascript
常用的js验证和数据处理总结
2016/08/02 Javascript
AngularJS实现页面定时刷新
2017/03/14 Javascript
Vue.js实战之使用Vuex + axios发送请求详解
2017/04/04 Javascript
ES6中箭头函数的定义与调用方式详解
2017/06/02 Javascript
详解VueRouter进阶之导航钩子和路由元信息
2017/09/13 Javascript
bootstrap 通过加减按钮实现输入框组功能
2017/11/15 Javascript
Layer组件多个iframe弹出层打开与关闭及参数传递的方法
2019/09/25 Javascript
Vue实现小购物车功能
2020/12/21 Vue.js
基于vuex实现购物车功能
2021/01/10 Vue.js
安装python3的时候就是输入python3死活没有反应的解决方法
2018/01/24 Python
selenium在执行phantomjs的API并获取执行结果的方法
2018/12/17 Python
Django中如何防范CSRF跨站点请求伪造攻击的实现
2019/04/28 Python
python制作简单五子棋游戏
2019/06/18 Python
使用python分析统计自己微信朋友的信息
2019/07/19 Python
python使用nibabel和sitk读取保存nii.gz文件实例
2020/07/01 Python
python时间time模块处理大全
2020/10/25 Python
惠普美国官方商店:HP Official Store
2016/08/28 全球购物
美国东北部户外服装和设备零售商:Eastern Mountain Sports
2016/10/05 全球购物
Banggood官网:面向全球客户的综合商城
2017/04/19 全球购物
办理退休介绍信
2014/01/09 职场文书
高二物理教学反思
2014/02/08 职场文书
五一促销活动总结
2014/07/01 职场文书
党员领导干部民主生活会批评与自我批评发言
2014/09/28 职场文书
授权委托书协议书
2014/10/16 职场文书
工人先锋号事迹材料
2014/12/24 职场文书
风之谷观后感
2015/06/11 职场文书
导游词之长城八达岭
2019/09/24 职场文书
SpringBoot连接MySQL获取数据写后端接口的操作方法
2021/11/02 MySQL
Django中celery的使用项目实例
2022/07/07 Python