您的位置:首页 > 理论基础 > 计算机网络

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

2018-01-08 01:00 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
中给出的回复:

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()


就酱,又是新的一周,加油!

============================

最后,欢迎关注我的公众号「伯特说」

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐