美文网首页
Android OkHttp3流程分析(2)

Android OkHttp3流程分析(2)

作者: Bfmall | 来源:发表于2023-04-11 16:34 被阅读0次

一、前言

之前分析了okhttp3的基本工作流程,其中重点说明了分发器、高并发线程池设计、任务的分发和转换原理,后面还有一个比较重要的5大拦截器还没有具体深入研究,其实okhttp中核心工作基本上都是在拦截器中执行的,接下来这篇文章就带大家分析五大拦截器究竟是怎么协同工作的,每个拦截器究竟干了什么事情。

二、基于责任链模式的拦截器的工作流程

其实拦截器的工作流程就像是我们去办理业务时的流程,下面举一个简单的例子

新进社畜小王出差回来,带了一堆发票要公司报销,然后就出现了下面的事情

小王进入财务室。。。

小王:钱掌柜,我之前出差,住宿,吃饭,交通都产生了一笔费用,这些是发票,麻烦报销下

小王把一堆发票双手捧住递给了财务钱掌柜。。。

财务:你这不行啊,你得把这个申请单填好,而且你来我这报销必须得要人事部门同意啊,不然怎么知道你是不是真的去出差了

财务打出了一份报销申请单,递给了小王。。。

小王拿着申请单和一堆发票进入了人事部。。。

小王:人事小姐姐你好,这是我的报销申请单,需要你在上面签字,我才能找财务报销

人事:你这不行啊,没你的部门老大签字,鬼知道你去出差干嘛了

小王很烦躁地从人事部走出来,拿着申请单到了技术部经理的办公室。。。

小王:秃经理,我这里有份报销申请单,需要你签字后,人事才能签字,人事签字后,财务才能签字,然后报销才能到账,就问你签不签!

技术部经理:不要激动!你这几天出差帮公司解决了难题,我这不会卡你的

秃经理在报销申请单上签上了自己的名字。。。

人事在报销申请单上签上了自己的名字。。。

财务在报销申请单上签上了自己的名字。。。

小王银行卡到账1000元

上面的例子很简单地展示了一整个的报销流程,大概意思就是每个环节都必须要满足上一个环节,否则就不执行,下面是一个简单的流程图


image.png

这个流程图也类比了okhttp的五大拦截器之间的流转。

五大拦截器的流转原理是基于责任链模式的,一个请求过来首先会依次流经每个拦截器,但是每个拦截器都是要求下一个拦截器返回结果后再去往下走,接下来直接看源码

我们直接从RealCall的getResponseWithInterceptorChain方法开始看

final class RealCall implements Call {
    ...
    Response getResponseWithInterceptorChain() throws IOException {
        // Build a full stack of interceptors.
        List<Interceptor> interceptors = new ArrayList<>();//代码1
        interceptors.addAll(client.interceptors());
        interceptors.add(new RetryAndFollowUpInterceptor(client));
        interceptors.add(new BridgeInterceptor(client.cookieJar()));
        interceptors.add(new CacheInterceptor(client.internalCache()));
        interceptors.add(new ConnectInterceptor(client));
        if (!forWebSocket) {
          interceptors.addAll(client.networkInterceptors());
        }
        interceptors.add(new CallServerInterceptor(forWebSocket));//代码2

        Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0,
            originalRequest, this, client.connectTimeoutMillis(),
            client.readTimeoutMillis(), client.writeTimeoutMillis());//代码3

        boolean calledNoMoreExchanges = false;
        try {
          Response response = chain.proceed(originalRequest);//代码4
          if (transmitter.isCanceled()) {
            closeQuietly(response);
            throw new IOException("Canceled");
          }
          return response;
        } catch (IOException e) {
          calledNoMoreExchanges = true;
          throw transmitter.noMoreExchanges(e);
        } finally {
          if (!calledNoMoreExchanges) {
            transmitter.noMoreExchanges(null);
          }
        }
  }
    ...
}

代码流向:\color{red}{代码1}->\color{red}{代码2},这段代码执行的过程就是将拦截器加入到列表中的过程,执行完后,会得到这样一个拦截器列表:

拦截器列表:interceptors 释义
client.interceptors 用户自定义的拦截器
RetryAndFollowUpInterceptor 重试和重定向拦截器
BridgeInterceptor 桥接拦截器
CacheInterceptor 缓存拦截器
ConnectInterceptor 连接拦截器
client.networkInterceptors 当网络请求回来后,用户自定义的拦截器
CallServerInterceptor 和服务器通信的拦截器

上面的顺序十分重要,关系到拦截器的流转流程,在这里假设用户没有自定义拦截器列表即client.interceptors.size == 0

\color{red}{代码3}中创建了一个chain,翻译过来就是链条,这个chain的左右就是将5大拦截器串联起来
\color{red}{代码4}开始就是拦截器流转的开端,接下来我们具体看看,okhttp是如何将从一个拦截器流转到另一个拦截器的

public interface Interceptor {
    ...
    interface Chain {
        ...
        Response proceed(Request request) throws IOException;//代码5
        ...
    }
    ...
}
public final class RealInterceptorChain implements Interceptor.Chain {
    ...
    @Override public Response proceed(Request request) throws IOException {
        return proceed(request, transmitter, exchange);//代码6
    }
    public Response proceed(Request request, Transmitter transmitter, @Nullable Exchange exchange)
      throws IOException {
        ...
        RealInterceptorChain next = new RealInterceptorChain(interceptors, transmitter, exchange,
            index + 1, request, call, connectTimeout, readTimeout, writeTimeout);//代码7
        Interceptor interceptor = interceptors.get(index);//代码8
        Response response = interceptor.intercept(next);//代码9
        ...
        return response;
  }
}

Chain这个接口的唯一实现只有RealInterceptorChain所以这里的代码流向应该是:\color{red}{代码4->代码5->代码6->代码7}

先看下代码7这里生成了一个Chain的实例,其中第一个参数interceptors不变,还是原来的拦截器列表,变化的是index,这个成员变量是用来指定当前链条指向哪个拦截器,所以可以推测\color{red}{代码7}实际上就是生成指向下一个拦截器的链条。走到\color{red}{代码8}可以发现取出的是当前的(第index个)拦截器,而\color{red}{代码9}则是将下一个拦截器的链条做为参数传递到了拦截器的intercept()方法中执行。继续跟代码,看看interceptor.intercept(next)这里做了什么。在这之前记住当前index == client.interceptors.size而我们之前假设用户没有自定义拦截器所以index == 0,所以\color{red}{代码8}取出的拦截器是RetryAndFollowUpInterceptor。

public interface Interceptor {
    Response intercept(Chain chain) throws IOException;//代码10
    ...
}
public final class RetryAndFollowUpInterceptor implements Interceptor {
    @Override public Response intercept(Chain chain) throws IOException {
        ...
        RealInterceptorChain realChain = (RealInterceptorChain) chain;//代码11
        ...
        while (true) {
         ...
          Response response;
          ...
          try {
            response = realChain.proceed(request, transmitter, null);//代码12
            success = true;
          } catch (RouteException e) {
         ...
          }
          ...
  }
}

代码流向:\color{red}{代码9->代码10->代码11}

可以很清楚地知道\color{red}{代码11}中获得的realChain实际上就是\color{red}{代码9}中传递过来的下一个链表。我们再看看\color{red}{代码12}
好熟悉,这不就又回到了\color{red}{代码5->代码6->代码7}这个代码流向了吗?,但是不同的是之前就已经在代码9中将index+1,所以当前index == 1,所以代码8取到的拦截器应该是BridgeInterceptor,简单看一下BridgeInterceptor

public final class BridgeInterceptor implements Interceptor {
    ...
    @Override public Response intercept(Chain chain) throws IOException {
        ...
        Response networkResponse = chain.proceed(requestBuilder.build());//代码13     
        ...
     }
    ...
}

果然BridgeInterceptor也会通过链表chain来执行下一个拦截器,这样就形成了一个驱动型的链表,上一个链表会驱动下一个链表去执行拦截器中的intercept方法,而intercept()方法又会驱动下一个链表...由此拦截器列表就能执行起来了。

值得注意的是Interceptor接口中有个返回值Response,为啥要有这个?

还记得开篇讲过社畜小王报销流程吗,他先找财务财务要求人事签名才继续下面的工作,不然不给钱,这个签名就类比Response,在okhttp拦截器中的含义就是,我RetryAndFollowUpInterceptor拦截器得先有一个Response才能执行下面的操作,而这个Response得由下一个拦截器(BridgeInterceptor)给我,下一个拦截器也要一个Response,而这个拦截器得由下下一个(CacheInterceptor)给...所以一旦执行了只有当没有下一个拦截器时,拦截器才会终止往下传,一个个拦截器收到了Response后才会继续下面的工作。

到这里流程基本上就通了,接下来会围绕上面的列表,详细讲讲5大拦截器里每个拦截器做了哪些事情。

三、拦截器详解

3.1 RetryAndFollowUpInterceptor重试和重定向拦截器

RetryAndFollowUpInterceptor是什么
从名字上就能看出,这个拦截器的作用是用来重试和重定向的,上期分析出拦截器中主要用来执行的方法是intercept()方法,那就废话不多说直接看RetryAndFollowUpInterceptor在intercept()中做了什么。

看源码:

public final class RetryAndFollowUpInterceptor implements Interceptor {

  /**
   * How many redirects and auth challenges should we attempt? Chrome follows 21 redirects; Firefox,
   * curl, and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5.
   */
  private static final int MAX_FOLLOW_UPS = 20;

  @Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Call call = realChain.call();
    EventListener eventListener = realChain.eventListener();

    StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
        createAddress(request.url()), call, eventListener, callStackTrace);
    this.streamAllocation = streamAllocation;

    int followUpCount = 0;
    Response priorResponse = null;
    while (true) {
      ......

      boolean releaseConnection = true;
      //重试
      try {
        //如果抛出异常就说明可能需要重试,如果没有必要重试,拦截器会直接抛出异常
        response = realChain.proceed(request, streamAllocation, null, null);
        releaseConnection = false;
      } catch (RouteException e) {
        // The attempt to connect via a route failed. The request will not have been sent.
        if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
          throw e.getFirstConnectException();
        }
        releaseConnection = false;
        continue;
      } catch (IOException e) {
        // An attempt to communicate with a server failed. The request may have been sent.
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
        releaseConnection = false;
        continue;
      } finally {
        // We're throwing an unchecked exception. Release any resources.
        if (releaseConnection) {
          streamAllocation.streamFailed(null);
          streamAllocation.release();
        }
      }

      // Attach the prior response if it exists. Such responses never have a body.
      if (priorResponse != null) {
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                    .body(null)
                    .build())
            .build();
      }

      Request followUp;
      try {
        //重定向
        followUp = followUpRequest(response, streamAllocation.route());
      } catch (IOException e) {
        streamAllocation.release();
        throw e;
      }

      ......

      if (++followUpCount > MAX_FOLLOW_UPS) {
        streamAllocation.release();
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }

      ......
    }
  }

}

上述就是主要代码,其实也就只做了两件事情
1.重试
2.重定向

重试

首先明确一点,为什么重试?首先肯定是程序员要求你这么做,如果说不需要重试那肯定就不用去重试了。其次肯定是因为超时等原因,我们的网络连接失败而抛出异常。那每个异常都要重试吗?并不是,如果说你压根连host的格式都不对,这样的重试肯定毫无意义。基于以上两点RetryAndFollowUpInterceptor拦截器为我们做了一些处理。

response = realChain.proceed(request, streamAllocation, null, null);执行后,如果抛出异常就说明可能需要重试,如果没有必要重试,拦截器会直接抛出异常,如果有必要重试,就会继续往下走。

整个重试和重定向的逻辑都是在while循环里的,所以满足条件的重试肯定不会一直循环下去,代码中有个MAX_FOLLOW_UPS,这个的意思是允许最大重试的次数,这个次数是20,如果重试次数大于了20次,也会报错ProtocolException,定为20的原因如下:
意思是,我们这个20次重试不是瞎搞的,这是参考了Chrome、Firefox、Safari这几个大佬开发的浏览器的重试次数综合考虑后得来的。

那怎么样的重试才是值得去重试的呢?我们每次catch异常时,在Catch后都会执行一个recover()方法,这个方法的返回值直接决定了是不是要直接报错(跳出循环,不再重试),接下来看看这个方法究竟干了什么:

/**
* 简单来说这个方法的意义就是判断当前是否值得去重试,如果值得重试就返回true,如果不值得重试就返回false
*/
private boolean recover(IOException e, StreamAllocation streamAllocation,
      boolean requestSendStarted, Request userRequest) {
    streamAllocation.streamFailed(e);

    //意思是如果程序员不让我重试,那我就不重试
    // The application layer has forbidden retries.
    if (!client.retryOnConnectionFailure()) return false;

    //意思是同一个请求(request)不能被调用两次,如果请求是不重复的并且已经发送过了(requestSendStarted==true),那就不允许重试了
    // We can't send the request body again.
    if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;

    //意思是出现了严重的问题,我们跟进去看看
    // This exception is fatal.
    if (!isRecoverable(e, requestSendStarted)) return false;

    //意思是如果没有可供重试的路由那也不能重试
    // No more routes to attempt.
    if (!streamAllocation.hasMoreRoutes()) return false;

    // For failure recovery, use the same route selector with a new connection.
    return true;
  }

看下isRecoverable方法:

private boolean isRecoverable(IOException e, boolean requestSendStarted) {
    //意思是如果是协议错误,那不能重试
    // If there was a protocol problem, don't recover.
    if (e instanceof ProtocolException) {
      return false;
    }

    //如果不是超时异常那也不能重试
    // If there was an interruption don't recover, but if there was a timeout connecting to a route
    // we should try the next route (if there is one).
    if (e instanceof InterruptedIOException) {
      return e instanceof SocketTimeoutException && !requestSendStarted;
    }

    //ssl握手异常不能重试
    // Look for known client-side or negotiation errors that are unlikely to be fixed by trying
    // again with a different route.
    if (e instanceof SSLHandshakeException) {
      // If the problem was a CertificateException from the X509TrustManager,
      // do not retry.
      if (e.getCause() instanceof CertificateException) {
        return false;
      }
    }

    //ssl握手未授权异常,不能重试
    if (e instanceof SSLPeerUnverifiedException) {
      // e.g. a certificate pinning error.
      return false;
    }

    // An example of one we might want to retry with a different route is a problem connecting to a
    // proxy and would manifest as a standard IOException. Unless it is one we know we should not
    // retry, we return true and try a new route.
    return true;
  }

简而言之,就是那种很严重的错误,你重试100遍都没结果的,索性就不要重试。
重试的判断基本上就是这些逻辑,接下来我们回过去看看调用followUpRequest重定向的代码。

重定向

try {
        followUp = followUpRequest(response, streamAllocation.route());
      } catch (IOException e) {
        streamAllocation.release();
        throw e;
      }

followUpRequest方法:

/**
* 代码走到这里后其实服务端已经返回的数据,这段代码的目的就是看看是否还
* 需要重定向,如果不需要重定向就返回null。
*/
private Request followUpRequest(Response userResponse, Route route) throws IOException {
    if (userResponse == null) throw new IllegalStateException();
    //服务器返回的状态码
    int responseCode = userResponse.code();

    final String method = userResponse.request().method();
    switch (responseCode) {
      case HTTP_PROXY_AUTH://看有没有代理,并返回
        Proxy selectedProxy = route != null
            ? route.proxy()
            : client.proxy();
        if (selectedProxy.type() != Proxy.Type.HTTP) {
          throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
        }
        return client.proxyAuthenticator().authenticate(route, userResponse);

      case HTTP_UNAUTHORIZED://需要身份验证的
        return client.authenticator().authenticate(route, userResponse);

      case HTTP_PERM_REDIRECT:
      case HTTP_TEMP_REDIRECT://意思是如果返回的状态码为永久或临时重定向的,而重定向的方法不是GET或者HEAD的就不用管
        // "If the 307 or 308 status code is received in response to a request other than GET
        // or HEAD, the user agent MUST NOT automatically redirect the request"
        if (!method.equals("GET") && !method.equals("HEAD")) {
          return null;
        }
        // fall-through
      case HTTP_MULT_CHOICE:
      case HTTP_MOVED_PERM:
      case HTTP_MOVED_TEMP:
      case HTTP_SEE_OTHER:
        //如果用户不让重定向就不用管
        // Does the client allow redirects?
        if (!client.followRedirects()) return null;
        
        //是取出重定向的地址,如果是null则不管,如果不为null,
        //就直接通过HttpUrl url = userResponse.request().url().resolve(location);拼接成一个httpUrl
        String location = userResponse.header("Location");
        if (location == null) return null;
        HttpUrl url = userResponse.request().url().resolve(location);

        // Don't follow redirects to unsupported protocols.
        if (url == null) return null;

        //判断当前重定向是否是http和https之间的重定向
        // If configured, don't follow redirects between SSL and non-SSL.
        boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
        //表示如果确实是http和https之间的重定向,那就得看用户是否让这种重定向,如果不让那就返回null不用管
        if (!sameScheme && !client.followSslRedirects()) return null;

        // Most redirects don't include a request body.
        Request.Builder requestBuilder = userResponse.request().newBuilder();
        if (HttpMethod.permitsRequestBody(method)) {
          //redirectsWithBody的方法是判断这个方法既不是GET也不是HEAD
          final boolean maintainBody = HttpMethod.redirectsWithBody(method);
          //判断改方法是不是不等于PROPFIND
          if (HttpMethod.redirectsToGet(method)) {
            requestBuilder.method("GET", null);
          } else {
            RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
            requestBuilder.method(method, requestBody);
          }
          if (!maintainBody) {
            requestBuilder.removeHeader("Transfer-Encoding");
            requestBuilder.removeHeader("Content-Length");
            requestBuilder.removeHeader("Content-Type");
          }
        }

        // When redirecting across hosts, drop all authentication headers. This
        // is potentially annoying to the application layer since they have no
        // way to retain them.
        if (!sameConnection(userResponse, url)) {
          requestBuilder.removeHeader("Authorization");
        }

        return requestBuilder.url(url).build();

      case HTTP_CLIENT_TIMEOUT:
        // 408's are rare in practice, but some servers like HAProxy use this response code. The
        // spec says that we may repeat the request without modifications. Modern browsers also
        // repeat the request (even non-idempotent ones.)
        if (!client.retryOnConnectionFailure()) {
          // The application layer has directed us not to retry the request.
          return null;
        }

        if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
          return null;
        }

        if (userResponse.priorResponse() != null
            && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
          // We attempted to retry and got another timeout. Give up.
          return null;
        }

        if (retryAfter(userResponse, 0) > 0) {
          return null;
        }

        return userResponse.request();

      case HTTP_UNAVAILABLE:
        if (userResponse.priorResponse() != null
            && userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
          // We attempted to retry and got another timeout. Give up.
          return null;
        }

        if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
          // specifically received an instruction to retry without delay
          return userResponse.request();
        }

        return null;

      default:
        return null;
    }
  }

重定向的判断大致就是这么多

再回过去看看,这个拦截器其实主要的工作都集中在了判断上面,判断是否能重试,判断是否能重定向,而每个判断又分为是否是用户(程序员)不让重试或者重定向,还是因为外在条件导致报错。

3.2 BridgeInterceptor桥连接拦截器

BridgeInterceptor做的事情就比较简单了,从字面意思上讲Bridge是桥的意思,这个桥连接着两边,一边是用户端的请求一边是服务端的响应,所以这个拦截器的作用大概就像下面这个样子


image.png
public final class BridgeInterceptor implements Interceptor {
  private final CookieJar cookieJar;

  public BridgeInterceptor(CookieJar cookieJar) {
    this.cookieJar = cookieJar;
  }

  @Override public Response intercept(Chain chain) throws IOException {
    Request userRequest = chain.request();
    Request.Builder requestBuilder = userRequest.newBuilder();

     //------------------请求数据加工开始------------------
    RequestBody body = userRequest.body();
    if (body != null) {
      MediaType contentType = body.contentType();
      if (contentType != null) {
        requestBuilder.header("Content-Type", contentType.toString());
      }

      long contentLength = body.contentLength();
      if (contentLength != -1) {
        requestBuilder.header("Content-Length", Long.toString(contentLength));
        requestBuilder.removeHeader("Transfer-Encoding");
      } else {
        requestBuilder.header("Transfer-Encoding", "chunked");
        requestBuilder.removeHeader("Content-Length");
      }
    }

    if (userRequest.header("Host") == null) {
      requestBuilder.header("Host", hostHeader(userRequest.url(), false));
    }

    if (userRequest.header("Connection") == null) {
      requestBuilder.header("Connection", "Keep-Alive");
    }

    // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
    // the transfer stream.
    boolean transparentGzip = false;
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true;
      requestBuilder.header("Accept-Encoding", "gzip");
    }

    List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
    if (!cookies.isEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies));
    }

    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", Version.userAgent());
    }

    //------------------请求数据加工结束------------------


    //------------------返回数据加工开始------------------
    Response networkResponse = chain.proceed(requestBuilder.build());

    HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());

    Response.Builder responseBuilder = networkResponse.newBuilder()
        .request(userRequest);

    if (transparentGzip
        && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
        && HttpHeaders.hasBody(networkResponse)) {
      GzipSource responseBody = new GzipSource(networkResponse.body().source());
      Headers strippedHeaders = networkResponse.headers().newBuilder()
          .removeAll("Content-Encoding")
          .removeAll("Content-Length")
          .build();
      responseBuilder.headers(strippedHeaders);
      String contentType = networkResponse.header("Content-Type");
      responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
    }
    
    //------------------返回数据加工结束------------------
  
    return responseBuilder.build();
  }

}

代码如上,请求数据加工主要是对一些header做了处理,下图表格对应了这几个header的释义

请求头 释义
Content-Type 请求体类型,如: application/x-www-form-urlencoded
Content-Length 或 Transfer-Encoding 请求体解析方式
Host 请求的主机站点
Connection: Keep-Alive 保持长连接
Accept-Encoding: gzip 接受响应支持gzip压缩
Cookie cookie身份辨别
User-Agent 请求的用户信息,如:操作系统、浏览器等

其中Accept-Encoding: gzip这个请求头的意思是服务端可以返回被压缩后的数据,如果数据量过大,只要服务端配合好,使用这个请求头会大幅提升传输的效率

User-Agent这个请求头是系统信息,这个信息是可以随意修改的,比如我们的okhttp就做了这样的事情

public final class Version {
  public static String userAgent() {
    return "okhttp/3.14.9";//如果你不去设置,就默认给你okhttp
  }

  private Version() {
  }
}

Connection: Keep-Alive这个是保持长连接的意思,为啥要保持长连接?首先一个完整的tcp可靠连接一定会经历下面的步骤

三次握手->传输数据->四次挥手

这就产生了一个问题,我一个页面多图,文字,多视频,gif等,每下载一张图就来一轮握手挥手,岂不是很浪费时间和资源,所以这个头的意思是,我跟你握手一次建立连接后,你图片文字啥的该传传,我先等会儿再挥手。

数据返回时做的工作有两个:
1.保存cookie(默认不实现)
2.如果是gzip返回并且客户端这里是支持的,那就得包装一层GzipSource方便后面解压缩用

未完,看下篇

参考:
https://www.jianshu.com/p/9b87c07e9ce6

相关文章

网友评论

      本文标题:Android OkHttp3流程分析(2)

      本文链接:https://www.haomeiwen.com/subject/lljgddtx.html