写点什么

okhttp 文件上传失败,居然是 Android Studio 背锅?,灵魂一问 - 如何彻底防止 APK 反编译

作者:嘟嘟侠客
  • 2021 年 11 月 28 日
  • 本文字数:5078 字

    阅读完需:约 17 分钟

事情是这样的,有一段文件上传的代码,如下:


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)方法,看看代码:



本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

用户头像

嘟嘟侠客

关注

还未添加个人签名 2021.03.19 加入

还未添加个人简介

评论

发布
暂无评论
okhttp文件上传失败,居然是Android Studio背锅?,灵魂一问-如何彻底防止APK反编译