okhttp文件上传失败,居然是AS背锅?太难了~

刘望舒

共 24388字,需浏览 49分钟

 · 2021-07-17

   
微信改了推动机制,真爱请星标本公号
公众号回复加入BATcoder技术群BAT


作者:不怕天黑 

https://juejin.cn/post/6981210499815833631#comment

1、前言

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

2、问题描述

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

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

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

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

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

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

3、一探究竟

本着出现问题,先定位到自己代码的原则,打开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,看来又是一个不眠夜。

question1.jpeg

习惯告诉我,一个问题很久没查出来,可以先放弃,好吧,拔手机关电脑,洗澡睡觉。

半小时后,我躺在床上,很难受,于是我拿出手机,打开app,再试了试上传功能,惊奇的发现,可以了,上传成功了,这。。。。一脸懵逼,我找谁说理去,虽然没问题了,但问题没找到,作为一名初级程序员,这我无法接受。

精神的力量把我从床上扶了起来,再次打开电脑,连上手机,这次,果然有了新的收获,也一下子刷新了我的世界观;当我再次打开app,尝试上传文件时,一样的错误出现在我眼前,What??? 刚才还好好的,连上电脑就不行了?

question2.jpeg

ok,我彻底没脾气了,拔掉手机,重启app,再试,没问题了,再次连上电脑,再试,问题又出来了。。

此时,我的心态有了些许的好转,毕竟有了新的调查方向,我再次查看错误日志,发现了一个很奇怪的地方,如下:

com.android.tools.profiler.agent.okhttp.OkHttp3Interceptor是从哪冒出来的?在我的认知里,OkHttp3是没有这个拦截器的,为了验证我的认知,再次查看okhttp3源码,如下:

确定是没有添加这个拦截器的,仔细看日志发现,OkHttp3InterceptorCallServerInterceptor、ConnectInterceptor之间执行的,那就只有一个解释,OkHttp3Interceptor是通过addNetworkInterceptor方法添加,现在就好办了,全局搜索addNetworkInterceptor就知道是谁添加的,哪里添加的,很可惜,未找到调用此方法的源码,似乎又陷入了绝境。

那就只能开启调试,看看OkHttp3Interceptor是否在OkHttpClient对象的networkInterceptors网络拦截器列表里,一调试,果然有发现,如下:

调试点击下一步,神奇的事情就发生了,如下:

image.png

这怎么解释?networkInterceptors.size始终是0,interceptors.size是如何加1变为5的?再来看看,加的1是什么,如下:

image.png

很熟悉,就是我们之前提到的OkHttp3Interceptor,这是如何做到的?只有一个解释,OkHttpClient#networkInterceptors()方法被字节码插桩技术插入了新的代码,为了验证我的想法,我做了以下实验:

image.png
image.png

可以看到,我直接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[2]类,看看相关代码:

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里应该是看不到请求体的,也就是看不到请求参数,如下:

image.png

可以看到,Profiler里的网络监控器,没有监控到请求参数。

这就证实了OkHttp3Interceptor的确是Profiler里的网络监控器,也就是AS动态注入的。

OkHttp3Interceptor 与文件上传是否有直接的关系?

通过上面的案例分析,显然是有直接关系的,当你未打开Database InspectorProfiler时,文件上传一切正常。

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异常了。

4、如何解决

知道了具体的原因,就好解决,将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是用于监听上传进度的,OkHttp3InterceptorCallServerInterceptor先后调用了其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完整源码,需要自取[3]

5、小结

本案例上传失败的直接原因就是在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去监听网络?有知道的小伙伴可以评论区留言。

参考资料

[1]

https://github.com/liujingxing/rxhttp: https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fliujingxing%2Frxhttp

[2]

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: https://link.juejin.cn/?target=https%3A%2F%2Fandroid.googlesource.com%2Fplatform%2Ftools%2Fbase%2F%2B%2Fstudio-master-dev%2Fprofiler%2Fapp%2Fperfa-okhttp%2Fsrc%2Fmain%2Fjava%2Fcom%2Fandroid%2Ftools%2Fprofiler%2Fagent%2Fokhttp%2FOkHttp3Interceptor.java

[3]

https://github.com/liujingxing/rxhttp/blob/master/rxhttp/src/main/java/rxhttp/wrapper/progress/ProgressRequestBody.java: https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fliujingxing%2Frxhttp%2Fblob%2Fmaster%2Frxhttp%2Fsrc%2Fmain%2Fjava%2Frxhttp%2Fwrapper%2Fprogress%2FProgressRequestBody.java

[4]

https://juejin.im/post/6844904016380428302: https://juejin.im/post/6844904016380428302

[5]

https://juejin.cn/post/6844904100090347528: https://juejin.cn/post/6844904100090347528

[6]

https://juejin.cn/post/6884986439587594247: https://juejin.cn/post/6884986439587594247

[7]

https://github.com/liujingxing/rxhttp: https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fliujingxing%2Frxhttp




·················END·················

推荐阅读

耗时2年,Android进阶三部曲第三部《Android进阶指北》出版!

『BATcoder』做了多年安卓还没编译过源码?一个视频带你玩转!

鸿蒙来了,拜拜了,Powered by Android!

重生!进阶三部曲第一部《Android进阶之光》第2版 出版!

BATcoder技术群,让一部分人先进大厂

大家,我是刘望舒,腾讯TVP,著有三本技术畅销书,连续四年蝉联电子工业出版社年度优秀作者,谷歌开发者社区特邀讲师。

前华为技术专家,现大厂技术负责人。

想要加入 BATcoder技术群,公号回复BAT 即可。

为了防止失联,欢迎关注我的小号


       
  微信改了推送机制,真爱请星标本公号👇
浏览 62
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报