okhttp文件上传失败,居然是Android Studio背锅?

2023-10-08 06:40

本文主要是介绍okhttp文件上传失败,居然是Android Studio背锅?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

本案例是我本人遇到的真实案例,因查找原因的过程一度让我崩溃,我相信不少人也遇到过相同的问题,故将其记录下来,希望对大家有帮助,本案例使用RxHttp 2.6.4 + OkHttp 4.9.1 + Android Studio 4.2.2版本,当然,如果你使用Retrofit等其它基于OkHttp封装的框架,且用到监听上传进度功能,那么很大概率你也会遇到这个问题,请耐心看完,如果你想直接看到结果,划到文章末尾即可。

https://github.com/liujingxing/rxhttp

1、问题描述

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

fun uploadFiles(fileList: List<File>) {RxHttp.postForm("/server/...")     .add("key", "value")           .addFiles("files", fileList)   .upload {                       //上传进度回调                   }                              .asString()                    .subscribe({                    //成功回调                     }, {                            //失败回调                     })                             
}                                                                                  

这段代码在写完后很长一段时间内都是ok的,突然有一天,执行这段代码居然报错了,日志如下:

图片

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

图片

可以看到,方法里面第一行代码就判断数据流是否已关闭,是的话,抛出异常。

注:如果你是RxHttp使用者,正在尝试这段代码,发现没问题,也不要惊讶,因为这需要在Android Studio特定场景下执行才会出现,而且是相对高频使用的场景,请待我一步步揭晓答案。

2、一探究竟

本着出现问题,先定位到自己代码的原则,打开ProgressRequestBody类76行看看,如下:

fun uploadFiles(fileList: List<File>) {RxHttp.postForm("/server/...")     .add("key", "value")           .addFiles("files", fileList)   .upload {                       //上传进度回调                   }                              .asString()                    .subscribe({                    //成功回调                     }, {                            //失败回调                     })                             
}                                                                                  

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点猜测:

1、包名有com.android.tools,应该跟 Android 官方有关系。

2、包名有agent,又是拦截器,应该跟网络代理,也就是网络监控有关。

3、最后一点,也是最重要的,包名有profiler,这让我联想到了Android Studio(以下简称AS)里Profiler网络分析器。

果然,在Google的源码中,真找到了OkHttp3Interceptor类,看看相关代码:

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

public final class OkHttp3Interceptor implements Interceptor {//省略相关代码@Overridepublic 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;@Overridepublic 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异常了。

3 、如何解决

知道了具体的原因,就好解决,将ProgressRequestBody里面的BufferedSink对象改为局部变量即可,如下:

public class ProgressRequestBody extends RequestBody {//省略相关代码@Overridepublic 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。

于是,做出如下更改:

你以为这样就完了?相信很多人都会用到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.bodyif (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: Responsetry {response = chain.proceed(request)} catch (e: Exception) {throw e}return response}}

可以看到,HttpLoggingInterceptor内部也会调用RequestBody#writeTo方法,并传入Buffer对象,到这,我们就好办了,在ProgressRequestBody类增加一个Buffer的判断逻辑即可,如下:

public class ProgressRequestBody extends RequestBody {//省略相关代码@Overridepublic 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 {//省略相关代码@Overridepublic 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完整源码,需要自取。

https://github.com/liujingxing/rxhttp/blob/master/rxhttp/src/main/java/rxhttp/wrapper/progress/ProgressRequestBody.java

小结

本案例上传失败的直接原因就是在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://mp.weixin.qq.com/s/lwn5RIK_G_R8UV-S_tQcBw

这篇关于okhttp文件上传失败,居然是Android Studio背锅?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/163408

相关文章

Java文件上传的多种实现方式

《Java文件上传的多种实现方式》文章主要介绍了文件上传接收接口的使用方法,包括获取文件信息、创建文件夹、保存文件到本地的两种方法,以及如何使用Postman进行接口调用... 目录Java文件上传的多方式1.文件上传接收文件接口2.接口主要内容部分3.postman接口调用总结Java文件上传的多方式1

Xshell远程连接失败以及解决方案

《Xshell远程连接失败以及解决方案》本文介绍了在Windows11家庭版和CentOS系统中解决Xshell无法连接远程服务器问题的步骤,在Windows11家庭版中,需要通过设置添加SSH功能并... 目录一.问题描述二.原因分析及解决办法2.1添加ssh功能2.2 在Windows中开启ssh服务2

Redis连接失败:客户端IP不在白名单中的问题分析与解决方案

《Redis连接失败:客户端IP不在白名单中的问题分析与解决方案》在现代分布式系统中,Redis作为一种高性能的内存数据库,被广泛应用于缓存、消息队列、会话存储等场景,然而,在实际使用过程中,我们可能... 目录一、问题背景二、错误分析1. 错误信息解读2. 根本原因三、解决方案1. 将客户端IP添加到Re

使用Python实现大文件切片上传及断点续传的方法

《使用Python实现大文件切片上传及断点续传的方法》本文介绍了使用Python实现大文件切片上传及断点续传的方法,包括功能模块划分(获取上传文件接口状态、临时文件夹状态信息、切片上传、切片合并)、整... 目录概要整体架构流程技术细节获取上传文件状态接口获取临时文件夹状态信息接口切片上传功能文件合并功能小

Android数据库Room的实际使用过程总结

《Android数据库Room的实际使用过程总结》这篇文章主要给大家介绍了关于Android数据库Room的实际使用过程,详细介绍了如何创建实体类、数据访问对象(DAO)和数据库抽象类,需要的朋友可以... 目录前言一、Room的基本使用1.项目配置2.创建实体类(Entity)3.创建数据访问对象(DAO

Android WebView的加载超时处理方案

《AndroidWebView的加载超时处理方案》在Android开发中,WebView是一个常用的组件,用于在应用中嵌入网页,然而,当网络状况不佳或页面加载过慢时,用户可能会遇到加载超时的问题,本... 目录引言一、WebView加载超时的原因二、加载超时处理方案1. 使用Handler和Timer进行超

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

android-opencv-jni

//------------------start opencv--------------------@Override public void onResume(){ super.onResume(); //通过OpenCV引擎服务加载并初始化OpenCV类库,所谓OpenCV引擎服务即是 //OpenCV_2.4.3.2_Manager_2.4_*.apk程序包,存