okhttp文件上传失败,居然是AS背锅?太难了~
微信改了推动机制,真爱请星标本公号 公众号回复加入BATcoder技术群 BAT
作者:不怕天黑
https://juejin.cn/post/6981210499815833631#comment
1、前言
本案例是我本人遇到的真实案例,因查找原因的过程一度让我崩溃,我相信不少人也遇到过相同的问题,故将其记录下来,希望对大家有帮助,本案例使用RxHttp 2.6.4[1] + OkHttp 4.9.1 + Android Studio 4.2.2
版本,当然,如果你使用Retrofit等其它基于OkHttp封装的框架,且用到监听上传进度功能,那么很大概率你也会遇到这个问题,请耐心看完,如果你想直接看到结果,划到文章末尾即可。
2、问题描述
事情是这样的,有一段文件上传的代码,如下:
fun uploadFiles(fileList: List<File>) {
RxHttp.postForm("/server/...")
.add("key", "value")
.addFiles("files", fileList)
.upload {
//上传进度回调
}
.asString()
.subscribe({
//成功回调
}, {
//失败回调
})
}
这段代码在写完后很长一段时间内都是ok的,突然有一天,执行这段代码居然报错了,日志如下:
这个异常是100%出现的,很熟悉的异常,具体原因就是,数据流被关闭了,但依然往里面写数据,来看看最后抛异常的地方,如下:
可以看到,方法里面第一行代码就判断数据流是否已关闭,是的话,抛出异常。
注:如果你是RxHttp使用者,正在尝试这段代码,发现没问题,也不要惊讶,因为这需要在Android Studio特定场景下执行才会出现,而且是相对高频使用的场景,请待我一步步揭晓答案
3、一探究竟
本着出现问题,先定位到自己代码的原则,打开ProgressRequestBody
类76行看看,如下:
public class ProgressRequestBody extends RequestBody {
//省略相关代码
private BufferedSink bufferedSink;
@Override
public void writeTo(BufferedSink sink) throws IOException {
if (bufferedSink == null) {
bufferedSink = Okio.buffer(sink(sink));
}
requestBody.writeTo(bufferedSink); //这里是76行
bufferedSink.flush();
}
}
ProgressRequestBody
继承了okhttp3.RequestBody
类,作用是监听上传进度;显然最后执行到这里时,数据流已经被关闭了,从日志里可以看到,最后一次调用ProgressRequestBody#writeTo(BufferedSink)
方法的地方在CallServerInterceptor
拦截器的59行,打开看看
class CallServerInterceptor(private val forWebSocket: Boolean) : Interceptor {
//省略相关代码
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
//省略相关代码
if (responseBuilder == null) {
if (requestBody.isDuplex()) {
exchange.flushRequest()
val bufferedRequestBody = exchange.createRequestBody(request, true).buffer()
requestBody.writeTo(bufferedRequestBody)
} else {
val bufferedRequestBody = exchange.createRequestBody(request, false).buffer()
requestBody.writeTo(bufferedRequestBody) //这里是59行
bufferedRequestBody.close() //数据写完,将数据流关闭
}
}
}
}
熟悉OkHttp原理的同学应该知道,CallServerInterceptor
拦截器是okhttp拦截器链的最后一个拦截器,将客户端数据写出到服务端,就是在这里实现的,也就是59行,那问题就来了,数据都还没写出去,数据流怎么就关闭了呢?这令我百思不得其解,毫无头绪。
于是乎,我做了很多无用功,如:重新检查代码,看看是否有手动关闭数据流的地方,显然没有找到;接着,实在没有办法,代码回滚,回滚到最初写这段代码的版本,我满怀期待的以为,这下应该没问题了,可尝试过后,依旧报java.lang.IllegalStateException: closed
,成年人的崩溃就在这一瞬间,我陷入了绝境,已经消耗5个小时在这个问题上,此时已晚上23:30,看来又是一个不眠夜。
习惯告诉我,一个问题很久没查出来,可以先放弃,好吧,拔手机关电脑,洗澡睡觉。
半小时后,我躺在床上,很难受,于是我拿出手机,打开app,再试了试上传功能,惊奇的发现,可以了,上传成功了,这。。。。一脸懵逼,我找谁说理去,虽然没问题了,但问题没找到,作为一名初级程序员,这我无法接受。
精神的力量把我从床上扶了起来,再次打开电脑,连上手机,这次,果然有了新的收获,也一下子刷新了我的世界观;当我再次打开app,尝试上传文件时,一样的错误出现在我眼前,What??? 刚才还好好的,连上电脑就不行了?
ok,我彻底没脾气了,拔掉手机,重启app,再试,没问题了,再次连上电脑,再试,问题又出来了。。
此时,我的心态有了些许的好转,毕竟有了新的调查方向,我再次查看错误日志,发现了一个很奇怪的地方,如下:
com.android.tools.profiler.agent.okhttp.OkHttp3Interceptor
是从哪冒出来的?在我的认知里,OkHttp3
是没有这个拦截器的,为了验证我的认知,再次查看okhttp3源码,如下:
那就只能开启调试,看看OkHttp3Interceptor
是否在OkHttpClient
对象的networkInterceptors
网络拦截器列表里,一调试,果然有发现,如下:
调试点击下一步,神奇的事情就发生了,如下:
这怎么解释?networkInterceptors.size
始终是0,interceptors.size
是如何加1变为5的?再来看看,加的1是什么,如下:
很熟悉,就是我们之前提到的OkHttp3Interceptor
,这是如何做到的?只有一个解释,OkHttpClient#networkInterceptors()
方法被字节码插桩技术插入了新的代码,为了验证我的想法,我做了以下实验:
可以看到,我直接new了一个OkHttpClient
对象,啥也没配置,调用networkInterceptors()
方法,就获取了OkHttp3Interceptor
拦截器,但OkHttpClient
对象里的networkInterceptors
列表中是没有这个拦截器的,这就证实了我的想法。
那现在的问题就是,OkHttp3Interceptor
是谁注入的?跟文件上传失败是否有直接的关系?
OkHttp3Interceptor是谁注入的?
先来探索第一个问题,通过OkHttp3Interceptor
类的包名class com.android.tools.profiler.agent.okhttp
,我有以下3点猜测
-
包名有
com.android.tools
,应该跟 Android 官方有关系 -
包名有
agent
,又是拦截器,应该跟网络代理,也就是网络监控有关 -
最后一点,也是最重要的,包名有
profiler
,这让我联想到了Android Studio
(以下简称AS)里Profiler
网络分析器
果然,在Google
的源码中,真找到了OkHttp3Interceptor[2]类,看看相关代码:
public final class OkHttp3Interceptor implements Interceptor {
//省略相关代码
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
HttpConnectionTracker tracker = null;
try {
tracker = trackRequest(request); //1、追踪请求体
} catch (Exception ex) {
StudioLog.e("Could not track an OkHttp3 request", ex);
}
Response response;
try {
response = chain.proceed(request);
} catch (IOException ex) {
}
try {
if (tracker != null) {
response = trackResponse(tracker, response); //2、追踪响应体
}
} catch (Exception ex) {
StudioLog.e("Could not track an OkHttp3 response", ex);
}
return response;
}
可以确定它就是一个网络监控器,但它是不是AS
的网络监听器,我却还持怀疑态度,因为我这个项目没开启Profiler
分析器,但我最近在开发room
数据库相关功能,开启了数据分析器Database Inspector
,难道跟这个有关?我尝试关掉Database Inspector
,并且重启app,再次尝试文件上传,居然成功了,是真的成功了,你能信?我也不信,于是,再次开启Database Inspector
,再次尝试文件上传,失败了,异常跟之前的一模一样;接着,我关闭Database Inspector
,并且打开Profiler
分析器,再次尝试文件上传,一样失败了。
我想到这里,基本可以认定OkHttp3Interceptor
就是Profiler
里面的网络监控器,但也好像缺乏直接证据,于是,我尝试改了下ProgressRequestBody
类,如下:
public class ProgressRequestBody extends RequestBody {
//省略相关代码
private BufferedSink bufferedSink;
@Override
public void writeTo(BufferedSink sink) throws IOException {
//如果调用方是OkHttp3Interceptor,不写请求体,直接返回
if (sink.toString().contains(
"com.android.tools.profiler.support.network.HttpTracker$OutputStreamTracker"))
return;
if (bufferedSink == null) {
bufferedSink = Okio.buffer(sink(sink));
}
requestBody.writeTo(bufferedSink);
bufferedSink.flush();
}
}
以上代码,仅仅加了一句if
语句,这条语句可以判断当前调用方是不是OkHttp3Interceptor
,是的话,不写请求体,直接返回;如果OkHttp3Interceptor
就是Profiler
里的网络监控器,那么此时Profiler里应该是看不到请求体的,也就是看不到请求参数,如下:
可以看到,Profiler
里的网络监控器,没有监控到请求参数。
这就证实了OkHttp3Interceptor
的确是Profiler
里的网络监控器,也就是AS
动态注入的。
OkHttp3Interceptor 与文件上传是否有直接的关系?
通过上面的案例分析,显然是有直接关系的,当你未打开Database Inspector
、Profiler
时,文件上传一切正常。
OkHttp3Interceptor是如何影响文件上传的?
回到正题,OkHttp3Interceptor是如何影响文件上传的?这个就需要继续分析OkHttp3Interceptor的源码,来看看追踪请求体的代码:
public final class OkHttp3Interceptor implements Interceptor {
private HttpConnectionTracker trackRequest(Request request) throws IOException {
StackTraceElement[] callstack =
OkHttpUtils.getCallstack(request.getClass().getPackage().getName());
HttpConnectionTracker tracker =
HttpTracker.trackConnection(request.url().toString(), callstack);
tracker.trackRequest(request.method(), toMultimap(request.headers()));
if (request.body() != null) {
OutputStream outputStream =
tracker.trackRequestBody(OkHttpUtils.createNullOutputStream());
BufferedSink bufferedSink = Okio.buffer(Okio.sink(outputStream));
request.body().writeTo(bufferedSink); // 1、将请求体写入到BufferedSink中
bufferedSink.close(); // 2、关闭BufferedSink
}
return tracker;
}
}
想到这里问题就很清楚了,上面备注的第一代码中request.body()
,拿到的就是ProgressRequestBody
对象,随后调用其writeTo(BufferedSink)
方法,传入BufferedSink
对象,方法执行完,就将BufferedSink
对象关闭了,然而,ProgressRequestBody
里却将BufferedSink
声明为成员变量,并且为空时才会赋值,这就导致后续CallServerInterceptor
调用其writeTo(BufferedSink)
方法时,使用的还是上一个已关闭的BufferedSink
对象,此时再往里面写数据,自然就java.lang.IllegalStateException: closed
异常了。
4、如何解决
知道了具体的原因,就好解决,将ProgressRequestBody
里面的BufferedSink
对象改为局部变量即可,如下:
public class ProgressRequestBody extends RequestBody {
//省略相关代码
@Override
public void writeTo(BufferedSink sink) throws IOException {
BufferedSink bufferedSink = Okio.buffer(sink(sink));
requestBody.writeTo(bufferedSink);
bufferedSink.colse();
}
}
改完后,开启Profiler
里的网络监控器,再次尝试文件上传,ok成功了,但又有一个新的问题,ProgressRequestBody
是用于监听上传进度的,OkHttp3Interceptor
、CallServerInterceptor
先后调用了其writeTo(BufferedSink)
方法,这就会导致请求体写两次,也就是进度监听会收到两遍,而我们真正需要的是CallServerInterceptor
调用的那次,咋整?好办,我们前面就判断过调用方是否OkHttp3Interceptor
于是,做出如下更改:
public class ProgressRequestBody extends RequestBody {
//省略相关代码
@Override
public void writeTo(BufferedSink sink) throws IOException {
//如果调用方是OkHttp3Interceptor,直接写请求体,不再通过包装类来处理请求进度
if (sink.toString().contains(
"com.android.tools.profiler.support.network.HttpTracker$OutputStreamTracker")) {
requestBody.writeTo(bufferedSink);
} else {
BufferedSink bufferedSink = Okio.buffer(sink(sink));
requestBody.writeTo(bufferedSink);
bufferedSink.colse();
}
}
}
你以为这样就完了?相信很多人都会用到com.squareup.okhttp3:logging-interceptor
日志拦截器,当你添加该日志拦截器后,再次上传文件,会发现,进度回调又执行了两遍,为啥?因为该日志拦截器,也会调用ProgressRequestBody#writeTo(BufferedSink)
方法,看看代码:
//省略部分代码
class HttpLoggingInterceptor @JvmOverloads constructor(
private val logger: Logger = Logger.DEFAULT
) : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val requestBody = request.body
if (logHeaders) {
if (!logBody || requestBody == null) {
logger.log("--> END ${request.method}")
} else if (bodyHasUnknownEncoding(request.headers)) {
logger.log("--> END ${request.method} (encoded body omitted)")
} else if (requestBody.isDuplex()) {
logger.log("--> END ${request.method} (duplex request body omitted)")
} else if (requestBody.isOneShot()) {
logger.log("--> END ${request.method} (one-shot body omitted)")
} else {
val buffer = Buffer()
//1、这里调用了RequestBody的writeTo方法,并传入了Buffer对象
requestBody.writeTo(buffer)
}
}
val response: Response
try {
response = chain.proceed(request)
} catch (e: Exception) {
throw e
}
return response
}
}
可以看到,HttpLoggingInterceptor
内部也会调用RequestBody#writeTo
方法,并传入Buffer
对象,到这,我们就好办了,在ProgressRequestBody
类增加一个Buffer
的判断逻辑即可,如下:
public class ProgressRequestBody extends RequestBody {
//省略相关代码
@Override
public void writeTo(BufferedSink sink) throws IOException {
//如果调用方是OkHttp3Interceptor,或者传入的是Buffer对象,直接写请求体,不再通过包装类来处理请求进度
if (sink instanceof Buffer
|| sink.toString().contains(
"com.android.tools.profiler.support.network.HttpTracker$OutputStreamTracker")) {
requestBody.writeTo(bufferedSink);
} else {
BufferedSink bufferedSink = Okio.buffer(sink(sink));
requestBody.writeTo(bufferedSink);
bufferedSink.colse();
}
}
}
这样就完了?也不见得,如果后续又遇到什么拦截器调用其writeTo
方法,还是会出现进度回调执行两遍的情况,只能在遇到这种情况时,加入对应的判断逻辑
到这,也许有人会问,为啥不直接判断调用方是不是CallServerInterceptor
,是的话监听进度回调,否则,直接写入请求体。想法很好,也是可行的,如下:
public class ProgressRequestBody extends RequestBody {
//省略相关代码
@Override
public void writeTo(BufferedSink sink) throws IOException {
//如果调用方是CallServerInterceptor,监听上传进度
if (sink.toString().contains("RequestBodySink(okhttp3.internal")) {
BufferedSink bufferedSink = Okio.buffer(sink(sink));
requestBody.writeTo(bufferedSink);
bufferedSink.colse();
} else {
requestBody.writeTo(bufferedSink);
}
}
}
但是该方案有个致命的缺陷,如果okhttp未来版本更改了目录结构,ProgressRequestBody
类就完全失效。
两个方案就由大家自己去选择,这里给出ProgressRequestBody完整源码,需要自取[3]
5、小结
本案例上传失败的直接原因就是在AS
开启了Database Inspector
数据库分析器或Profiler
网络监控器时,AS
就会通过字节码插桩技术,对OkHttpClient#networkInterceptors()
方法注入新的字节码,使其多返回一个com.android.tools.profiler.agent.okhttp.OkHttp3Interceptor
拦截器(用于监听网络),该拦截器会调用ProgressRequestBody#writeTo(BufferedSink)
方法,并传入BufferedSink
对象,writeTo方法执行完毕后,立即将BufferedSink
对象关闭,在随后的CallServerInterceptor
拦截又调用ProgressRequestBody#writeTo(BufferedSink)
方法往已关闭的BufferedSink
对象写数据,最终导致java.lang.IllegalStateException: closed
异常。
但有个有疑惑,我却未找到答案,那就是为啥开启Database Inspector
也会导致AS
去监听网络?有知道的小伙伴可以评论区留言。
参考资料
https://github.com/liujingxing/rxhttp: https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fliujingxing%2Frxhttp
[2]https://android.googlesource.com/platform/tools/base/+/studio-master-dev/profiler/app/perfa-okhttp/src/main/java/com/android/tools/profiler/agent/okhttp/OkHttp3Interceptor.java: https://link.juejin.cn/?target=https%3A%2F%2Fandroid.googlesource.com%2Fplatform%2Ftools%2Fbase%2F%2B%2Fstudio-master-dev%2Fprofiler%2Fapp%2Fperfa-okhttp%2Fsrc%2Fmain%2Fjava%2Fcom%2Fandroid%2Ftools%2Fprofiler%2Fagent%2Fokhttp%2FOkHttp3Interceptor.java
[3]https://github.com/liujingxing/rxhttp/blob/master/rxhttp/src/main/java/rxhttp/wrapper/progress/ProgressRequestBody.java: https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fliujingxing%2Frxhttp%2Fblob%2Fmaster%2Frxhttp%2Fsrc%2Fmain%2Fjava%2Frxhttp%2Fwrapper%2Fprogress%2FProgressRequestBody.java
[4]https://juejin.im/post/6844904016380428302: https://juejin.im/post/6844904016380428302
[5]https://juejin.cn/post/6844904100090347528: https://juejin.cn/post/6844904100090347528
[6]https://juejin.cn/post/6884986439587594247: https://juejin.cn/post/6884986439587594247
[7]https://github.com/liujingxing/rxhttp: https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fliujingxing%2Frxhttp
推荐阅读
• 耗时2年,Android进阶三部曲第三部《Android进阶指北》出版!
• 『BATcoder』做了多年安卓还没编译过源码?一个视频带你玩转!
BATcoder技术群,让一部分人先进大厂
大家好,我是刘望舒,腾讯TVP,著有三本技术畅销书,连续四年蝉联电子工业出版社年度优秀作者,谷歌开发者社区特邀讲师。
前华为技术专家,现大厂技术负责人。
想要加入 BATcoder技术群,公号回复BAT
即可。
为了防止失联,欢迎关注我的小号
微信改了推送机制,真爱请星标本公号👇