作者:vivo 互联网服务器团队- Tie Qinrui

OkHttp 在 Java 和 Android 国际中被广泛运用,深化学习源代码有助于把握软件特性和提高编程水平。

本文首先从源代码下手扼要剖析了一个恳求建议过程中的中心代码,接着经过流程图和架构图概括地介绍了OkHttp的全体结构,要点剖析了阻拦器的职责链形式规划,最终列举了OkHttp阻拦器在项目中的实践运用。

一、背景介绍

在生产实践中,常常会遇到这样的场景:需求针对某一类 Http 恳求做一致的处理,例如在 Header 里增加恳求参数或许修改恳求呼应等等。这类问题的一种比较优雅的解决方案是运用阻拦器来对恳求和呼应做一致处理。

在 Android 和 Java 国际里 OkHttp 凭借其高效性和易用性被广泛运用。作为一款优异的开源 Http 恳求结构,深化了解它的完结原理,能够学习优异软件的规划和编码经历,协助咱们更好到地运用它的特性,而且有助于特别场景下的问题排查。本文测验从源代码动身探求 OkHttp 的根本原理,并排举了一个简略的比如阐明阻拦器在咱们项目中的实践运用。本文源代码基于 OkHttp 3.10.0。

二、OkHttp 根本原理

2.1 从一个恳求示例动身

OkHttp 能够用来发送同步或异步的恳求,异步恳求与同步恳求的首要差异在于异步恳求会交由线程池来调度恳求的履行。运用 OkHttp 发送一个同步恳求的代码恰当简洁,示例代码如下:

  • 同步 GET 恳求示例
// 1.创立OkHttpClient客户端
OkHttpClient client = new OkHttpClient();
public String getSync(String url) throws IOException {
      OkHttpClient client = new OkHttpClient();
      // 2.创立一个Request目标
      Request request = new Request.Builder()
              .url(url)
              .build();
      // 3.创立一个Call目标并调用execute()办法
      try (Response response = client.newCall(request).execute()) {
          return response.body().string();
      }
  }

其间 execute() 办法是恳求建议的进口,RealCall 目标的 execute() 办法的源代码如下:

  • RealCall 的 execute() 办法源代码
@Override public Response execute() throws IOException {
  synchronized (this) { // 同步锁定当时目标,将当时目标标记为“已履行”
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
  captureCallStackTrace(); // 捕获调用栈
  eventListener.callStart(this); // 事情监听器记载“调用开端”事情
  try {
    client.dispatcher().executed(this); // 调度器将当时目标放入“运转中”行列
    Response result = getResponseWithInterceptorChain(); // 经过阻拦器建议调用并获取呼应
    if (result == null) throw new IOException("Canceled");
    return result;
  } catch (IOException e) {
    eventListener.callFailed(this, e); // 反常时记载“调用失利事情”
    throw e;
  } finally {
    client.dispatcher().finished(this); // 将当时目标从“运转中”行列移除
  }
}

execute() 办法首先将当时恳求标记为“已履行”,然后会为重试跟踪阻拦器增加堆栈追踪信息,接着事情监听器记载“调用开端”事情,调度器将当时目标放入“运转中”行列,之后经过阻拦器建议调用并获取呼应,最终在 finally 块中将当时恳求从“运转中”行列移除,反常发生时事情监听器记载“调用失利”事情。其间要害的办法是getResponseWithInterceptorChain() ,其源代码如下:

Response getResponseWithInterceptorChain() throws IOException {
    // 构建一个全栈的阻拦器列表
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    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));
    Interceptor.Chain chain = new RealInterceptorChain(interceptors, ……);
    return chain.proceed(originalRequest);
  }

该办法中依照特定的次序创立了一个有序的阻拦器列表,之后运用阻拦器列表创立阻拦器链并建议proceed() 办法调用。在chain.proceed() 办法中会运用递归的办法将列表中的阻拦器串联起来依次对恳求目标进行处理。阻拦器链的完结是 OkHttp 的一个巧妙所在,在后文咱们会用一小节专门讨论。在继续往下剖析之前,经过以上的代码片段咱们已经大致看到了一个恳求建议的全体流程。

2.2 OkHttp 中心履行流程

一个 OkHttp 恳求的中心履行过程如以下流程图所示:

深入浅出 OkHttp 源码解析及应用实践

图2-1 OkHttp恳求履行流程图

图中各部分的意义和效果如下:

  • OkHttpClient:是整个 OkHttp 的中心管理类,从面向目标的抽象表明上来看它代表了客户端本身,是恳求的调用工厂,用来发送恳求和读取呼应。在大多数状况下这个类应该是被共享的,由于每个 Client 目标持有自己的连接池和线程池。重复创立则会造成在空闲池上的资源糟蹋。Client目标能够经过默许的无参结构办法创立也能够经过 Builder 创立自定义的 Client 目标。Client 持有的线程池和连接池资源在空闲时能够主动开释无需客户端代码手动开释,在特别状况下也支撑手动开释。

  • Request:一个 Request 目标代表了一个 Http 恳求。它包含了恳求地址 url,恳求办法类型 method ,恳求头 headers,恳求体 body 等特点,该目标具有的特点普遍运用了 final 要害字来修饰,正如该类的阐明文档中所述,当这个类的 body 为空或许 body 本身是不可变目标时,这个类是一个不可变目标。

  • Response:一个 Response 目标代表了一个 Http 呼应。这个实例目标是一个不可变目标,只有 responseBody 是一个能够一次性运用的值,其他特点都是不可变的。

  • RealCall:一个 RealCall 目标代表了一个准备好履行的恳求调用。它只能被履行一次。一同担任了调度和职责链安排的两大重担。

  • Dispatcher:调度器。它决定了异步调用何时被履行,内部运用 ExecutorService 调度履行,支撑自定义 Executor。

  • EventListener:事情监听器。抽象类 EventListener 定义了在一个恳求生命周期中记载各种事情的办法,经过监听各种事情,能够用来捕获运用程序 HTTP 恳求的履行指标。然后监控 HTTP 调用的频率和功能。

  • Interceptor:阻拦器。对应了软件规划形式中的阻拦器形式,阻拦器可用于改动、增强软件的常规处理流程,该形式的中心特征是对软件体系的改动是通明的和主动的。OkHttp 将整个恳求的杂乱逻辑拆分红多个独立的阻拦器完结,经过职责链的规划形式将它们串联到一同,完结发送恳求获取呼应成果的过程。

2.3 OkHttp 全体架构

经过进一步阅览 OkHttp 源码,能够看到 OkHttp 是一个分层的结构。软件分层是杂乱体系规划的常用手段,经过分层能够将杂乱问题区分红规模更小的子问题,分而治之。一同分层的规划也有利于功能的封装和复用。OkHttp 的架构能够分为:运用接口层,协议层,连接层,缓存层,I/O层。不同的阻拦器为各个层次的处理供给调用进口,阻拦器经过职责链形式串联成阻拦器链,然后完结一个Http恳求的完好处理流程。如下图所示:

深入浅出 OkHttp 源码解析及应用实践

图 2-2 OkHttp架构图(图片来自网络)

2.4 OkHttp 阻拦器的品种和效果

OkHttp 的中心功能是经过阻拦器来完结的,各种阻拦器的效果分别为:

  • client.interceptors:由开发者设置的阻拦器,会在一切的阻拦器处理之前进行最早的阻拦处理,可用于增加一些公共参数,如自定义 header、自定义 log 等等。

  • RetryAndFollowUpInterceptor:首要担任进行重试和重定向的处理。

  • BridgeInterceptor:首要担任恳求和呼应的转化。把用户结构的 request 目标转化成发送到服务器 request目标,并把服务器回来的呼应转化为对用户友好的呼应。

  • CacheInterceptor:首要担任缓存的相关处理,将 Http 的恳求成果放到到缓存中,以便在下次进行相同的恳求时,直接从缓存中读取成果,提高呼应速度。

  • ConnectInterceptor:首要担任树立连接,树立 TCP 连接或许 TLS 连接。

  • client.networkInterceptors:由开发者设置的阻拦器,实质上和第一个阻拦器相似,但是由于位置不同,所以用处也不同。

  • CallServerInterceptor:首要担任网络数据的恳求和呼应,也便是实践的网络I/O操作。将恳求头与恳求体发送给服务器,以及解析服务器回来的 response。

除了结构供给的阻拦器外,OkHttp 支撑用户自定义阻拦器来对恳求做增强处理,自定义阻拦器能够分为两类,分别是运用程序阻拦器和网络阻拦器,他们发挥效果的层次结构如下图:

深入浅出 OkHttp 源码解析及应用实践

图 2-3 阻拦器(图片来自[OkHttp官网](https://square.github.io/okhttp/features/interceptors/))

不同的阻拦器有不同的适用场景,他们各自的优缺点如下:

运用程序阻拦器

  • 无需担心重定向和重试等中间呼应。

  • 总是被调用一次,即使 HTTP 呼应是从缓存中供给的。

  • 能够观察到运用程序的原始恳求。不关心 OkHttp 注入的标头。

  • 答应短路而不调用 Chain.proceed()办法。

  • 答应重试并屡次调用 Chain.proceed()办法。

  • 能够运用 withConnectTimeout、withReadTimeout、withWriteTimeout 调整呼叫超时。

网络阻拦器

  • 能够对重定向和重试等中间呼应进行操作。

  • 缓存呼应不会调用。

  • 能够观察到经过网络传输的原始数据。

  • 能够访问携带恳求的链接。

2.5 职责链形式串联阻拦器调用

OkHttp 内置了 5 个中心的阻拦器用来完结恳求生命周期中的要害处理,一同它也支撑增加自定义的阻拦器来增强和扩展 Http 客户端,这些阻拦器经过职责链形式串联起来,使得的恳求能够在不同阻拦器之间流通和处理。

2.5.1 职责链形式

职责链形式是一种行为规划形式,答应将恳求沿着处理者链发送。收到恳求后,每个处理者均可对恳求进行处理,或将其传递给链上的下一个处理者。

深入浅出 OkHttp 源码解析及应用实践

图 2-4 职责链(图片来自网络)

适用场景包括:

  • 当程序需求运用不同办法处理不同品种的恳求时

  • 当程序必须按次序履行多个处理者时

  • 当所需求的处理者及其次序必须在运转时进行改动时

长处

  • 能够操控恳求处理的次序

  • 可对建议操作和履行操作的类进行解耦。

  • 能够在不更改现有代码的状况下在程序中新增处理者。

2.5.2 阻拦器的串联

职责链的进口从第一个 RealInterceptorChain 目标的 proceed() 办法调用开端。这个办法的规划十分巧妙,在完好的 proceed() 办法里会做一些更为谨慎的校验,去掉这些校验后该办法的中心代码如下:

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec, RealConnection connection) throws IOException {
    if (index >= interceptors.size()) throw new AssertionError();
    // ……  
    // Call the next interceptor in the chain.
    RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec, connection, index + 1, request, call, eventListener, connectTimeout, readTimeout, writeTimeout);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);
    // ……
    return response;
  }

这段代码能够当作三个步骤:

  1. 索引判别。index 初始值为0,它指示了阻拦器目标在列表中的索引次序,每履行一次 proceed 办法该参数自增1,当索引值大于阻拦器列表的索引下标时反常退出。

  2. 创立下一个职责链目标。

  3. 依照索引次序获取一个阻拦器,并调用 intercept() 办法。

  • 职责链串联

单独看这个办法似乎并不能将一切阻拦器都串联起来,串联的要害在于 intercept() 办法,intercept() 办法是完结 interceptor 接口时必需要完结的办法,该办法持有下一个职责链 目标 chain,在阻拦器的完结类里只需求在 intercept() 办法里的恰当地方再次调用 chain.proceed() 办法,程序指令便会重新回到以上代码片段,所以就能够触发对于下一个阻拦器的查找和调用了,在这个过程中阻拦器目标在列表中的先后次序十分重要,由于阻拦器的调用次序便是其在列表中的索引次序。

  • 递归办法

从另一个视点来看,proceed() 办法能够当作是一个递归办法。递归办法的根本定义为“函数的定义中调用函数本身”,虽然 proceed() 办法没有直接调用本身,但是除了最终一个阻拦器以外,阻拦器链中的其他阻拦器都会在恰当的位置调用 chain.proceed() 办法,职责链目标和阻拦器目标合在一同则组成了一个自己调用自己的逻辑循环。依照笔者个人了解,这也是为什么源代码里 Chain 接口被规划成 Interceptor 接口的内部接口,在了解这段代码的时分要把它们两个接口当成一个全体来看,从这样的视点看的话,这样的接口规划是符合“高内聚”的准则的。

阻拦器 interceptor 和职责链 chain 的关系如下图:

深入浅出 OkHttp 源码解析及应用实践

图 2-5 Interceptor 和 Chain 的关系图

三、OkHttp 阻拦器在项目中的运用

在咱们的项目中,有一类恳求需求在恳求头 Header 中增加认证信息,运用阻拦器来完结能够极大地简化代码,提高代码可读性和可维护性。中心代码只需求完结符合事务需求的阻拦器如下:

  • 增加恳求头的阻拦器
public class EncryptInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request originRequest = chain.request();
        // 计算认证信息
        String authorization = this.encrypt(originRequest);
        // 增加恳求头
        Request request = originRequest.newBuilder()
                .addHeader("Authorization", authorization)
                .build();
        // 向职责链后边传递
        return chain.proceed(request);
    }
}

之后在创立 OkHttpClient 客户端的时分,运用 addInterceptor() 办法将咱们的阻拦器注册成运用程序阻拦器,即可完结主动地、无感地向恳求头中增加实时的认证信息的功能。

  • 注册运用程序阻拦器
OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(new EncryptInterceptor())
    .build();

四、回顾总结

OkHttp 在 Java 和 Android 国际中被广泛运用,经过运用 OkHttp 阻拦器能够解决一类问题——针对一类恳求一致修改恳求或呼应内容。深化了解 OkHttp 的规划和完结不只能够协助咱们学习优异开源软件的规划和编码经历,也有利于更好地运用软件特性以及对特别场景下问题的排查。本文测验从一个同步 GET 恳求的比如开端,首先经过源代码片段扼要剖析了一个恳求建议过程中触及的中心代码,接着用流程图的形式总结了恳求履行过程,然后用架构图展示了OkHttp的分层规划,介绍了各种阻拦器的用途、作业层次及优缺点,之后着重剖析了阻拦器的职责链形式规划——实质是一个递归调用,最终用一个简略的比如介绍了 OkHttp 阻拦器在实践生产场景中的运用。

参阅:

  1. OkHttp官方文档

  2. OkHttp源码解析系列文章