您的位置:首页 > 其它

为何 response.body().string() 只能调用一次

2018-01-10 21:47 621 查看
想必大家都用过或接触过 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
 中给出的回复:



就简单的一句话:
It's
documented on ResponseBody.
 于是我跑去看类注释文档,最后梳理如下:

在实际开发中,响应主体 
RessponseBody
 持有的资源可能会很大,所以 OkHttp
并不会将其直接保存到内存中,只是持有数据流连接。只有当我们需要时,才会从服务器获取数据并返回。同时,考虑到应用重复读取数据的可能性很小,所以将其设计为
一次性流(one-shot)
,读取后即 ‘关闭并释放资源’。


5.总结

最后,总结以下几点注意事项,划重点了:

响应体只能被使用一次;

响应体必须关闭:值得注意的是,在下载文件等场景下,当你以 
response.body().byteStream()
 形式获取输入流时,务必通过 
Response.close()
 来手动关闭响应体。

获取响应体数据的方法:使用 
bytes()
 或 
string()
 将整个响应读入内存;或者使用 
source()
byteStream()
charStream()
 方法以流的形式传输数据。

以下方法会触发关闭响应体;

Response.close()
Response.body().close()
Response.body().source().close()
Response.body().charStream().close()
Response.body().byteString().close()
Response.body().bytes()
Response.body().string()
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐