范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

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

  作者:不怕天黑一、前言
  本案例是我本人遇到的真实案例,因查找原因的过程一度让我崩溃,我相信不少人也遇到过相同的问题,故将其记录下来,希望对大家有帮助,本案例使用RxHttp 2.6.4 + OkHttp 4.9.1 + Android Studio 4.2.2版本,当然,如果你使用Retrofit等其它基于OkHttp封装的框架,且用到监听上传进度功能,那么很大概率你也会遇到这个问题,请耐心看完,如果你想直接看到结果,划到文章末尾即可。二、问题描述
  事情是这样的,有一段文件上传的代码,如下:fun uploadFiles(fileList: List) {     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 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完整源码,需要自取小结
  本案例上传失败的直接原因就是在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去监听网络?有知道的小伙伴可以评论区留言。最后
  还分享一份由大佬亲自收录整理的Android学习PDF+架构视频+面试文档+源码笔记,高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料
  这些都是我现在闲暇时还会反复翻阅的精品资料。里面对近几年的大厂面试高频知识点都有详细的讲解。相信可以有效地帮助大家掌握知识、理解原理,帮助大家在未来取得一份不错的答卷。
  当然,你也可以拿去查漏补缺,提升自身的竞争力。
  真心希望可以帮助到大家,Android路漫漫,共勉!
  如果你有需要的话,只需私信我【进阶】即可获取

爱眼日丨全国儿童一半以上近视,呵护孩子的视力你做对了吗?互联网热潮,让儿童接触电子设备越来越早,电子设备上的早教课程,更是许多父母的选择。而幼小的孩子,对电子设备非常容易上瘾,一旦沉迷,就难以自拔,还特别难管教过早让孩子使用电子设备,出读懂了美元为什么能称霸,就知道我国为啥要搞内循环在聊正题之前,还是想先简单地介绍一下背景,怕有些小伙伴不太清楚,对背景熟悉的可以自行跳过,不影响阅读。背景1944年7月,美国带着一班西方小弟,在美国的新罕布什尔州布雷顿森林,召开三国里关羽张飞有儿子,却没有妻子?原来是刘备搞得鬼三国里有一个细思极恐的细节,就是每次刘关张三人在一起的时候,都是出则同车,寝则同席,很少看到有关他们家室的介绍。即便是后来当了皇帝的刘备,其夫人的出场率,还不如诸葛亮骑马的次数多。曹操一生爱睡别人媳妇,睡了这个女人后,他后悔了曹操好色是出了名的,走到哪里,都不忘找女人,而且这厮有个特别的好爱独好别人的老婆,用现在的话说,就是好玩不过嫂子。据说,曹操一共有13个老婆,其中有10个都是别人的老婆。在抢别人的每日优鲜破发,叮咚买菜上市首日股价不容乐观,原来做的是生鲜外卖第一观察讯(文李楠)近日,每日优鲜成功上市,叮咚买菜也在火速准备上市事宜。从现在来看每日优鲜力压叮咚买菜一头。每日优鲜成立于2014年,而叮咚买菜则成立于2017年,前者稍微早于后行业分析国产机器人输在了减速机?(关注我,获取每日最新原创推送)近年,我国的机器人产业发展势头迅猛,技术水平不断突破,大有取替进口机器人之势。根据工信部发布的2020年112月机器人行业运行情况显示,2020年中行业信息工业走向互联网,不可避免的安全之痛(点击头像关注集成侠)美国最大的成品油管道运营商支撑着美国东海岸45燃油供应的ColonialPipeline,因被勒索软件攻击系统,致使燃油管道系统被迫关闭。美国17个州及华盛顿行业思考自动化的内卷与躺平(点击上方头像关注集成侠)曾经看过一个这样的故事有人花费数十万设计了一个智能检测并分拣空肥皂盒的方案,中国工人用一个大风扇就解决了问题。这不由得让人思索在智能制造的大潮下,全中国成行业看点面对芯荒,谁更心慌?(点击上方头像关注集成侠)你玩过多米诺骨牌吗?这个牵一发而动全身的游戏,目前正在全球范围的产业链中传导。随着游戏的深入,每倒下的一张牌,都在引起新的连锁反应去年9月,美国政府在同年行业分析工业机器人国产替代正加速崛起随着我国制造业人口红利逐渐消退,中国智能制造产业政策和下游行业的需求增长,预计到2025年我国制造业重点领域将全面实现智能化,其中关键岗位将由机器人替代,以机器换人的概念深入人心。美国遏制中国芯片发展,反推中国企业芯片技术变革科联社讯(文刘然)随着我国通讯技术的不断发展和进步,5G技术已经超越美国,成为全球最大5G商用国。同时,华为已经完成6G技术的研发,通讯技术已经完全超过美国,领先全球。手机销量来看
用电可视化?智能断路器和智慧用电监测平台能轻松实现电是最基本的能源,人民生活需要电,社会进步更需要电力支持。如今经济高速发展科技日新月异,人类社会也进入了电气新时代。电力方便人们生产生活的同时也威胁着人们的生命财产安全,过去进行电老黄的指甲刀,GTX1650SUPER评测2019年真是神奇的一年,老黄的手术刀是一次又一次伸向了图灵架构。先是7月发布了RTX208020702060super,各种满血核心,架构优化,瞬间让旧卡失去了光泽。之后没多久的先是天猫,再是百度,互联网巨头这波瓜吃的有点撑昨天刚吃完百度网盘用户激励计划的瓜,觉得它应该会消停一阵子了。结果早上一看,这不,它来了,它来了,它带着另一个瓜又来了百度职业道德委员会在4月21日发表通报,通报称集团副总裁韦方涉新iPhoneSE京东预售100抵扣,是真香还是愁卖?苹果作为行业龙头,旗下的每代新品发布会都是数码爱好者的盛宴。这次虽然因为疫情的原因取消了新品发布会,但官网悄悄上架的新一代iPhoneSE,仍然掀起了一波讨论热潮。近几年,苹果的每供给与匹配错位,小红书社区建设任重道远武侠小说有句名言有人的地方就有江湖,有江湖的地方就有纷争。如今进入互联网时代,人们被定义为流量,集中在一个个互联网内容社区,成为新时代的江湖所在地,上演着一出出人间魔幻剧。近日,一庞大吸金兽,小气米哈游据移动应用数据平台SensorTower最新数据显示,9月米哈游原神移动端海外收入超过2。34亿美元,是本期海外收入排名第2的糖果传奇收入的两倍。再加上中国ios市场的收入,原神以拼多多为什么要致力于知识普惠?书中自有黄金屋,书中自有颜如玉。自古以来,我国对于读书都极为重视。周易山海经等著作蕴含着古代先人对万事万物的探索,三字经论语等著作讲述着先贤关于做人做事的道理总结。得益于从古至今的华为发布首款儿童教育机器人,展现强大的硬件实力今年4月初,华为发布了以全屋智能及智慧屏旗舰新品为主导的春季发布会,发布会主要介绍了全新的全屋智能系统和最新款的华为智慧屏,但是在发布会的结尾,却给用户带来了一颗不小的彩蛋华为首款华为AI音箱2e,一款适合儿童使用的AI智能音箱9月23日,华为发布了nova9系列手机,此次还发布了一个新品音箱华为AI音箱2e,不管是从产品的外观还在使用功能上,都是一款非常适合小朋友使用的AI音箱。首先,它有一个能显示表情华为AI音箱2e,为什么是一款儿童智能音箱?9月底,在华为Nova手机的新品发布会上,一款长得和天猫精灵差不多的音箱登台,让人关注的是,音箱打着儿童音箱的标签,它到底和儿童音箱有什么联系呢?这个打着儿童语音交互的智能音箱,在音响器材使用的6个小窍门阜新声艺视听随着人们生活水平的日益提高,各种各样的音响器材进入我们的日常生活中,动辄几万甚至十几万的音响器材,国产的甚至国外进口的,每天下班回家打开音响听音乐,驱除疲劳,是多么美好的事情!但是