okhttp 文件上传失败,居然是 Android Studio 背锅?,灵魂一问 - 如何彻底防止 APK 反编译
事情是这样的,有一段文件上传的代码,如下:
fun uploadFiles(fileList: List<File>) {
RxHttp.postForm("/server/...")
.add("key", "value")
.addFiles("files", fileList)
.upload {
//上传进度回调
}
.asString()
.subscribe({
//成功回调
}, {
//失败回调
})
}
这段代码在写完后很长一段时间内都是 ok 的,突然有一天,执行这段代码居然报错了,日志如下:

这个异常是 100%出现的,很熟悉的异常,具体原因就是,数据流被关闭了,但依然往里面写数据,来看看最后抛异常的地方,如下:

可以看到,方法里面第一行代码就判断数据流是否已关闭,是的话,抛出异常。
注:如果你是 RxHttp 使用者,正在尝试这段代码,发现没问题,也不要惊讶,因为这需要在 Android Studio 特定场景下执行才会出现,而且是相对高频使用的场景,请待我一步步揭晓答案
==================================================================
本着出现问题,先定位到自己代码的原则,打开 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 在 CallServerInterceptor、ConnectInterceptor 之间执行的,那就只有一个解释,OkHttp3Interceptor 是通过 addNetworkInterceptor 方法添加,现在就好办了,全局搜索 addNetworkInterceptor 就知道是谁添加的,哪里添加的,很可惜,未找到调用此方法的源码,似乎又陷入了绝境。

那就只能开启调试,看看 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 类,看看相关代码:
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 异常了。
==================================================================
知道了具体的原因,就好解决,将 ProgressRequestBody 里面的 BufferedSink 对象改为局部变量即可,如下:
public class Progress
《Android 学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享
RequestBody 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)方法,看看代码:
评论