安庆大理运城常德铜陵江西
投稿投诉
江西南阳
嘉兴昆明
铜陵滨州
广东西昌
常德梅州
兰州阳江
运城金华
广西萍乡
大理重庆
诸暨泉州
安庆南充
武汉辽宁

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

7月15日 莫思归投稿
  作者:不怕天黑一、前言
  本案例是我本人遇到的真实案例,因查找原因的过程一度让我崩溃,我相信不少人也遇到过相同的问题,故将其记录下来,希望对大家有帮助,本案例使用RxHttp2。6。4OkHttp4。9。1AndroidStudio4。2。2版本,当然,如果你使用Retrofit等其它基于OkHttp封装的框架,且用到监听上传进度功能,那么很大概率你也会遇到这个问题,请耐心看完,如果你想直接看到结果,划到文章末尾即可。二、问题描述
  事情是这样的,有一段文件上传的代码,如下:funuploadFiles(fileList:ListFile){RxHttp。postForm(server。。。)。add(key,value)。addFiles(files,fileList)。upload{上传进度回调}。asString()。subscribe({成功回调},{失败回调})}
  这段代码在写完后很长一段时间内都是ok的,突然有一天,执行这段代码居然报错了,日志如下:
  这个异常是100出现的,很熟悉的异常,具体原因就是,数据流被关闭了,但依然往里面写数据,来看看最后抛异常的地方,如下:
  可以看到,方法里面第一行代码就判断数据流是否已关闭,是的话,抛出异常。
  注:如果你是RxHttp使用者,正在尝试这段代码,发现没问题,也不要惊讶,因为这需要在AndroidStudio特定场景下执行才会出现,而且是相对高频使用的场景,请待我一步步揭晓答案三、一探究竟
  本着出现问题,先定位到自己代码的原则,打开ProgressRequestBody类76行看看,如下:publicclassProgressRequestBodyextendsRequestBody{省略相关代码privateBufferedSinkbufferedSOverridepublicvoidwriteTo(BufferedSinksink)throwsIOException{if(bufferedSinknull){bufferedSinkOkio。buffer(sink(sink));}requestBody。writeTo(bufferedSink);这里是76行bufferedSink。flush();}}
  ProgressRequestBody继承了okhttp3。RequestBody类,作用是监听上传进度;显然最后执行到这里时,数据流已经被关闭了,从日志里可以看到,最后一次调用ProgressRequestBodywriteTo(BufferedSink)方法的地方在CallServerInterceptor拦截器的59行,打开看看classCallServerInterceptor(privatevalforWebSocket:Boolean):Interceptor{省略相关代码Throws(IOException::class)overridefunintercept(chain:Interceptor。Chain):Response{省略相关代码if(responseBuildernull){if(requestBody。isDuplex()){exchange。flushRequest()valbufferedRequestBodyexchange。createRequestBody(request,true)。buffer()requestBody。writeTo(bufferedRequestBody)}else{valbufferedRequestBodyexchange。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,这是如何做到的?只有一个解释,OkHttpClientnetworkInterceptors()方法被字节码插桩技术插入了新的代码,为了验证我的想法,我做了以下实验:
  可以看到,我直接new了一个OkHttpClient对象,啥也没配置,调用networkInterceptors()方法,就获取了OkHttp3Interceptor拦截器,但OkHttpClient对象里的networkInterceptors列表中是没有这个拦截器的,这就证实了我的想法。
  那现在的问题就是,OkHttp3Interceptor是谁注入的?跟文件上传失败是否有直接的关系?
  OkHttp3Interceptor是谁注入的?
  先来探索第一个问题,通过OkHttp3Interceptor类的包名classcom。android。tools。profiler。agent。okhttp,我有以下3点猜测包名有com。android。tools,应该跟Android官方有关系包名有agent,又是拦截器,应该跟网络代理,也就是网络监控有关最后一点,也是最重要的,包名有profiler,这让我联想到了AndroidStudio(以下简称AS)里Profiler网络分析器
  果然,在Google的源码中,真找到了OkHttp3Interceptor类,看看相关代码:publicfinalclassOkHttp3InterceptorimplementsInterceptor{省略相关代码OverridepublicResponseintercept(Interceptor。Chainchain)throwsIOException{Requestrequestchain。request();HttpConnectionTtry{trackertrackRequest(request);1、追踪请求体}catch(Exceptionex){StudioLog。e(CouldnottrackanOkHttp3request,ex);}Rtry{responsechain。proceed(request);}catch(IOExceptionex){}try{if(tracker!null){responsetrackResponse(tracker,response);2、追踪响应体}}catch(Exceptionex){StudioLog。e(CouldnottrackanOkHttp3response,ex);}}
  可以确定它就是一个网络监控器,但它是不是AS的网络监听器,我却还持怀疑态度,因为我这个项目没开启Profiler分析器,但我最近在开发room数据库相关功能,开启了数据分析器DatabaseInspector,难道跟这个有关?我尝试关掉DatabaseInspector,并且重启app,再次尝试文件上传,居然成功了,是真的成功了,你能信?我也不信,于是,再次开启DatabaseInspector,再次尝试文件上传,失败了,异常跟之前的一模一样;接着,我关闭DatabaseInspector,并且打开Profiler分析器,再次尝试文件上传,一样失败了。
  我想到这里,基本可以认定OkHttp3Interceptor就是Profiler里面的网络监控器,但也好像缺乏直接证据,于是,我尝试改了下ProgressRequestBody类,如下:publicclassProgressRequestBodyextendsRequestBody{省略相关代码privateBufferedSinkbufferedSOverridepublicvoidwriteTo(BufferedSinksink)throwsIOException{如果调用方是OkHttp3Interceptor,不写请求体,直接返回if(sink。toString()。contains(com。android。tools。profiler。support。network。HttpTrackerOutputStreamTracker))if(bufferedSinknull){bufferedSinkOkio。buffer(sink(sink));}requestBody。writeTo(bufferedSink);bufferedSink。flush();}}
  以上代码,仅仅加了一句if语句,这条语句可以判断当前调用方是不是OkHttp3Interceptor,是的话,不写请求体,直接返回;如果OkHttp3Interceptor就是Profiler里的网络监控器,那么此时Profiler里应该是看不到请求体的,也就是看不到请求参数,如下:
  可以看到,Profiler里的网络监控器,没有监控到请求参数。
  这就证实了OkHttp3Interceptor的确是Profiler里的网络监控器,也就是AS动态注入的。
  OkHttp3Interceptor与文件上传是否有直接的关系?
  通过上面的案例分析,显然是有直接关系的,当你未打开DatabaseInspector、Profiler时,文件上传一切正常。
  OkHttp3Interceptor是如何影响文件上传的?
  回到正题,OkHttp3Interceptor是如何影响文件上传的?这个就需要继续分析OkHttp3Interceptor的源码,来看看追踪请求体的代码:publicfinalclassOkHttp3InterceptorimplementsInterceptor{privateHttpConnectionTrackertrackRequest(Requestrequest)throwsIOException{StackTraceElement〔〕callstackOkHttpUtils。getCallstack(request。getClass()。getPackage()。getName());HttpConnectionTrackertrackerHttpTracker。trackConnection(request。url()。toString(),callstack);tracker。trackRequest(request。method(),toMultimap(request。headers()));if(request。body()!null){OutputStreamoutputStreamtracker。trackRequestBody(OkHttpUtils。createNullOutputStream());BufferedSinkbufferedSinkOkio。buffer(Okio。sink(outputStream));request。body()。writeTo(bufferedSink);1、将请求体写入到BufferedSink中bufferedSink。close();2、关闭BufferedSink}}}
  想到这里问题就很清楚了,上面备注的第一代码中request。body(),拿到的就是ProgressRequestBody对象,随后调用其writeTo(BufferedSink)方法,传入BufferedSink对象,方法执行完,就将BufferedSink对象关闭了,然而,ProgressRequestBody里却将BufferedSink声明为成员变量,并且为空时才会赋值,这就导致后续CallServerInterceptor调用其writeTo(BufferedSink)方法时,使用的还是上一个已关闭的BufferedSink对象,此时再往里面写数据,自然就java。lang。IllegalStateException:closed异常了。四、如何解决
  知道了具体的原因,就好解决,将ProgressRequestBody里面的BufferedSink对象改为局部变量即可,如下:publicclassProgressRequestBodyextendsRequestBody{省略相关代码OverridepublicvoidwriteTo(BufferedSinksink)throwsIOException{BufferedSinkbufferedSinkOkio。buffer(sink(sink));requestBody。writeTo(bufferedSink);bufferedSink。colse();}}
  改完后,开启Profiler里的网络监控器,再次尝试文件上传,ok成功了,但又有一个新的问题,ProgressRequestBody是用于监听上传进度的,OkHttp3Interceptor、CallServerInterceptor先后调用了其writeTo(BufferedSink)方法,这就会导致请求体写两次,也就是进度监听会收到两遍,而我们真正需要的是CallServerInterceptor调用的那次,咋整?好办,我们前面就判断过调用方是否OkHttp3Interceptor
  于是,做出如下更改:publicclassProgressRequestBodyextendsRequestBody{省略相关代码OverridepublicvoidwriteTo(BufferedSinksink)throwsIOException{如果调用方是OkHttp3Interceptor,直接写请求体,不再通过包装类来处理请求进度if(sink。toString()。contains(com。android。tools。profiler。support。network。HttpTrackerOutputStreamTracker)){requestBody。writeTo(bufferedSink);}else{BufferedSinkbufferedSinkOkio。buffer(sink(sink));requestBody。writeTo(bufferedSink);bufferedSink。colse();}}}
  你以为这样就完了?相信很多人都会用到com。squareup。okhttp3:logginginterceptor日志拦截器,当你添加该日志拦截器后,再次上传文件,会发现,进度回调又执行了两遍,为啥?因为该日志拦截器,也会调用ProgressRequestBodywriteTo(BufferedSink)方法,看看代码:省略部分代码classHttpLoggingInterceptorJvmOverloadsconstructor(privatevallogger:LoggerLogger。DEFAULT):Interceptor{Throws(IOException::class)overridefunintercept(chain:Interceptor。Chain):Response{valrequestchain。request()valrequestBodyrequest。bodyif(logHeaders){if(!logBodyrequestBodynull){logger。log(END{request。method})}elseif(bodyHasUnknownEncoding(request。headers)){logger。log(END{request。method}(encodedbodyomitted))}elseif(requestBody。isDuplex()){logger。log(END{request。method}(duplexrequestbodyomitted))}elseif(requestBody。isOneShot()){logger。log(END{request。method}(oneshotbodyomitted))}else{valbufferBuffer()1、这里调用了RequestBody的writeTo方法,并传入了Buffer对象requestBody。writeTo(buffer)}}valresponse:Responsetry{responsechain。proceed(request)}catch(e:Exception){throwe}returnresponse}}
  可以看到,HttpLoggingInterceptor内部也会调用RequestBodywriteTo方法,并传入Buffer对象,到这,我们就好办了,在ProgressRequestBody类增加一个Buffer的判断逻辑即可,如下:publicclassProgressRequestBodyextendsRequestBody{省略相关代码OverridepublicvoidwriteTo(BufferedSinksink)throwsIOException{如果调用方是OkHttp3Interceptor,或者传入的是Buffer对象,直接写请求体,不再通过包装类来处理请求进度if(sinkinstanceofBuffersink。toString()。contains(com。android。tools。profiler。support。network。HttpTrackerOutputStreamTracker)){requestBody。writeTo(bufferedSink);}else{BufferedSinkbufferedSinkOkio。buffer(sink(sink));requestBody。writeTo(bufferedSink);bufferedSink。colse();}}}
  这样就完了?也不见得,如果后续又遇到什么拦截器调用其writeTo方法,还是会出现进度回调执行两遍的情况,只能在遇到这种情况时,加入对应的判断逻辑
  到这,也许有人会问,为啥不直接判断调用方是不是CallServerInterceptor,是的话监听进度回调,否则,直接写入请求体。想法很好,也是可行的,如下:publicclassProgressRequestBodyextendsRequestBody{省略相关代码OverridepublicvoidwriteTo(BufferedSinksink)throwsIOException{如果调用方是CallServerInterceptor,监听上传进度if(sink。toString()。contains(RequestBodySink(okhttp3。internal)){BufferedSinkbufferedSinkOkio。buffer(sink(sink));requestBody。writeTo(bufferedSink);bufferedSink。colse();}else{requestBody。writeTo(bufferedSink);}}}
  但是该方案有个致命的缺陷,如果okhttp未来版本更改了目录结构,ProgressRequestBody类就完全失效。
  两个方案就由大家自己去选择,这里给出ProgressRequestBody完整源码,需要自取小结
  本案例上传失败的直接原因就是在AS开启了DatabaseInspector数据库分析器或Profiler网络监控器时,AS就会通过字节码插桩技术,对OkHttpClientnetworkInterceptors()方法注入新的字节码,使其多返回一个com。android。tools。profiler。agent。okhttp。OkHttp3Interceptor拦截器(用于监听网络),该拦截器会调用ProgressRequestBodywriteTo(BufferedSink)方法,并传入BufferedSink对象,writeTo方法执行完毕后,立即将BufferedSink对象关闭,在随后的CallServerInterceptor拦截又调用ProgressRequestBodywriteTo(BufferedSink)方法往已关闭的BufferedSink对象写数据,最终导致java。lang。IllegalStateException:closed异常。
  但有个有疑惑,我却未找到答案,那就是为啥开启DatabaseInspector也会导致AS去监听网络?有知道的小伙伴可以评论区留言。最后
  还分享一份由大佬亲自收录整理的Android学习PDF架构视频面试文档源码笔记,高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料
  这些都是我现在闲暇时还会反复翻阅的精品资料。里面对近几年的大厂面试高频知识点都有详细的讲解。相信可以有效地帮助大家掌握知识、理解原理,帮助大家在未来取得一份不错的答卷。
  当然,你也可以拿去查漏补缺,提升自身的竞争力。
  真心希望可以帮助到大家,Android路漫漫,共勉!
  如果你有需要的话,只需私信我【进阶】即可获取
投诉 评论 转载

iPhoneXR分辨率真的很差吗?为什么?不用怀疑,而是真的很低。XR作为X的替代版,在OLED屏可能会烧屏刺眼耗电的缺点下,苹果公司特意为用户贴心准备了一块6。1英寸,分辨率为1792X828像素的LCD屏。……一些常用的MySQL语句描述:有一个会员表,有个birthday字段,值为YYYYMMDD格式,现在要查询一个时间段内过生日的会员,比如0603到0708这个时间段内所有过生日的会员。SQL语句……okhttp文件上传失败,居然是AndroidStudio背作者:不怕天黑一、前言本案例是我本人遇到的真实案例,因查找原因的过程一度让我崩溃,我相信不少人也遇到过相同的问题,故将其记录下来,希望对大家有帮助,本案例使用RxHttp……美星链卫星今年两次接近中国空间站,专家可能是试探性摸底通信1、苹果或从明年起开始转为去实体SIM卡据国外媒体报道,上周有分析师预测称苹果可能会在部分国家和地区尝试推出没有实体SIM卡的iPhone,使用eSIM技术,具……骁龙芯的华为nova9SE,来自华为的妥协?自从华为被美国缴械之后,手机业务基本上是撒哈拉的庄稼蔫了。原定的P50系列出原本预售日期延迟了几个月,现在主要的旗舰系列能否继续更新都是个问题。虽说华为被缴械之后手机业务……晓有见地老炮儿重出江湖1、71岁的王石在视频号里很活跃,每天推广运动、教人戒酒,时不时来点心灵鸡汤,似乎在过着逍遥的退休生活。转头,阔别万科五年后,老王就再次杀回了他熟悉的商战场。这次,……天猫店铺转让成功之后多久可以使用贷款功能?现在的做电商其实跟做实体操作上没什么区别,首先就是要选平台,现在各种电商平台各分秋色,要找到最适合自己的才能利益最大化,其次就是申请店铺,有些平台申请平台非常难,比如天猫。跨过……梧桐树再度携手信泰人寿发布如意尊(星光版)迭代寿险产品近日,互联网人身险新规正式落地,引起了行业的广泛关注。据了解,新规明确划定了保险公司可通过互联网开展人身保险的业务范围,互联网人身保险产品范围限于意外险、健康险(除护理险)、定……通底层逻辑的全域自研七年前,零跑科技创始人朱江明决定跨入智能汽车行业。他曾是浙江大华技术股份有限公司的创始人之一。作为功成名就的创业老兵,他毅然决定从零开始,投入造车新势力的浪潮。朱江明曾说……一个延迟库恢复的案例导语在日常工作中可能会存在误删数据的情况,今天就简单介绍下如何利用延迟库进行数据库的快速恢复。步骤1。环境准备建立一个测试的主从库,写入一些测试数据,非本文要点,过……中国移动邵松手机摄像头将向全功能全场景全覆盖方面实现随着5G网络的深入覆盖,以及5G与产业的深度融合,如今中国智能硬件市场已处于高速发展阶段。国内各大厂商纷纷布局智能领域,智能硬件的质量也越来越受大众关注。作为智能硬件产品发展风……阿里女员工到底是好人啊,还是坏人啊?女的不是简单的人,男的不是无辜的人。受害者说谎,妨碍司法公正。她从一开始把公司描绘成血汗工厂,故意用职场潜规则来吸引眼球,强制丈夫接受好人人设,很可能有隐情。媒体早……
佳能28702。8L和适马17502。8哪个好?最近网络舆论大面积喷著名企业,不正常吧?五菱又出了一款微型电动车!全新五菱AirEV实拍图曝光华为P30定价曝光3888元起步真吉利啊!12月16日(周四)股市消息个股资讯破发,iPhone13的价格彻底崩了特斯拉又出新发明,用激光代替雨刷,人脸会受到伤害吗?芯讯通5G模组助力广电700M行业落地潜力无限千金难买龙回头,把握好元宇宙龙头回调后的上车机会海思麒麟芯片终于跌价了,现在入手正合适鸿蒙系统初长成,正式拒绝谷歌GMS,华为硬气老婆想买个上下班代步车,纠结于奕跑的颜值,飞度和致炫的油耗,

友情链接:中准网聚热点快百科快传网快生活快软网快好知文好找七猫云易事利