OkHttp踩坑记:为何 response.body().string() 只能调用一次?

栏目: Java · 发布时间: 7年前

内容简介: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

OkHttp踩坑记:为何 response.body().string() 只能调用一次?

就简单的一句话: 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() 只能调用一次?


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Inside Larry's and Sergey's Brain

Inside Larry's and Sergey's Brain

Richard Brandt / Portfolio / 17 Sep 2009 / USD 24.95

You’ve used their products. You’ve heard about their skyrocketing wealth and “don’t be evil” business motto. But how much do you really know about Google’s founders, Larry Page and Sergey Brin? Inside......一起来看看 《Inside Larry's and Sergey's Brain》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具