OkHttp 的一个 IllegalStateException 探索

在使用 OkHttp 框架处理请求时,容易会有如下写法:

1
2
val responseBody = response.body() ?: return null
Log.d("responseBody = " + response.body())

有时候,我们会直接写 response.body(),而不会再创建一个 String 对象,如果能确保只调用一次,这个是没什么问题的,但是如果向上面那样,在 Log 中又调用了一次,或者这个方法被多次调用,而且 response 是同一个,那么就会报如下错误:

1
java.lang.IllegalStateException: closed

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

拆解来看,先通过 response.body() 得到 ResponseBody 对象(是一个抽象类,在此我们不需要关心具体的实现类),然后调用 ResponseBody 的 string() 方法得到响应体的内容

分析后 body() 方法没有问题,我们往下看 string() 方法:

1
2
3
public final String string() throws IOException {
return new String(bytes(), charset().name());
}

很简单,通过指定字符集(charset)将 byte() 方法返回的 byte[] 数组转为 String 对象,构造没有问题,继续往下看 byte() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
public final byte[] bytes() throws IOException {
//...
BufferedSource source = source();
byte[] bytes;
try {
bytes = source.readByteArray();
} finally {
Util.closeQuietly(source);
}
//...
return bytes;
}

在 byte() 方法中,通过 BufferedSource 接口对象读取 byte[] 数组并返回,结合上面提到的异常信息 closed,可以注意到 finally 代码块中的 Util.closeQuietly() 方法,就是在执行完后,他会默默地关闭

再来看看这个方法

1
2
3
4
5
6
7
8
9
10
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):

1
2
3
4
5
6
7
8
9
10
//持有的 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() 方法:

1
2
3
4
5
@Override
public byte[] readByteArray() throws IOException {
buffer.writeAll(source);
return buffer.readByteArray();
}

继续往下看 writeAll() 方法:

1
2
3
4
5
6
7
8
9
@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() 方法会发生什么呢:

1
2
3
4
5
6
7
@Override
public long read(Buffer sink, long byteCount) throws IOException {
//...
if (closed) throw new IllegalStateException("closed");
//...
return buffer.read(sink, toRead);
}

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

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


OkHttp 的一个 IllegalStateException 探索
https://enderhoshi.github.io/2020/01/15/OkHttp 的一个 IllegalStateException 探索/
作者
HoshIlIlI
发布于
2020年1月15日
许可协议