想必你们都用过或接触过 OkHttp,我最近在使用 Okhttp 时,就踩到一个坑,在这儿分享出来,之后你们遇到相似问题时就能够绕过去。java
只是解决问题是不够的,本文将 侧重从源码角度分析下问题的根本,干货满满。git
在开发时,我经过构造 OkHttpClient
对象发起一次请求并加入队列,待服务端响应后,回调 Callback
接口触发 onResponse()
方法,而后在该方法中经过 Response
对象处理返回结果、实现业务逻辑。代码大体以下:github
//注:为聚焦问题,删除了无关代码 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
这段看起来没有任何问题的代码,实际运行后却出了问题:经过控制台看到成功打印了返回体数据(json),但紧接着抛出了异常:数组
java.lang.IllegalStateException: closed
复制代码
检查代码后,发现问题出在调用 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); } }); 复制代码
问题解决了,过后仍是要分析的。因为以前对 OkHttp
的了解仅限于使用,没有仔细分析过其内部实现的细节,周末抽时间往下看了看,算是弄明白了问题发生的缘由。markdown
先分析最直观的问题:为什么 response.body().string()
只能调用一次?ide
拆解来看,先经过 response.body()
获得 ResponseBody
对象(其是一个抽象类,在此咱们不须要关心具体的实现类),而后调用 ResponseBody
的 string()
方法获得响应体的内容。oop
分析后 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
复制代码
经过 fuc*ing the source code
,咱们找到了问题的根本,但我还有一个疑问:OkHttp 为何要这么设计?
其实,理解这个问题最好的方式就是查看 ResponseBody
的注释文档,正如 JakeWharton
在 issues
中给出的回复:
reply of JakeWharton in okhttp issues
It's documented on ResponseBody.
因而我跑去看类注释文档,最后梳理以下:
在实际开发中,响应主体
RessponseBody
持有的资源可能会很大,因此 OkHttp 并不会将其直接保存到内存中,只是持有数据流链接。只有当咱们须要时,才会从服务器获取数据并返回。同时,考虑到应用重复读取数据的可能性很小,因此将其设计为一次性流(one-shot)
,读取后即 '关闭并释放资源'。
最后,总结如下几点注意事项,划重点了:
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()
复制代码
就酱,又是新的一周,加油!
最后,欢迎关注个人公众号「伯特说」