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

这篇具有很好参考价值的文章主要介绍了深入浅出 OkHttp 源码解析及应用实践。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

作者: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 请求的核心执行过程如以下流程图所示:

图 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请求的完整处理流程。如下图所示:

图 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 支持用户自定义拦截器来对请求做增强处理,自定义拦截器可以分为两类,分别是应用程序拦截器和网络拦截器,他们发挥作用的层次结构如下图:

图 2-3 拦截器(图片来自OkHttp官网)

 

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

 

应用程序拦截器

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

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

  • 可以观察到应用程序的原始请求。不关心 OkHttp 注入的标头。

  • 允许短路而不调用 Chain.proceed()方法。

  • 允许重试并多次调用 Chain.proceed()方法。

  • 可以使用 withConnectTimeout、withReadTimeout、withWriteTimeout 调整呼叫超时。

 

网络拦截器

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

  • 缓存响应不会调用。

  • 可以观察到通过网络传输的原始数据。

  • 可以访问携带请求的链接。

2.5 责任链模式串联拦截器调用

OkHttp 内置了 5 个核心的拦截器用来完成请求生命周期中的关键处理,同时它也支持添加自定义的拦截器来增强和扩展 Http 客户端,这些拦截器通过责任链模式串联起来,使得的请求可以在不同拦截器之间流转和处理。

2.5.1 责任链模式

责任链模式 是一种行为设计模式, 允许将请求沿着处理者链发送。 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下一个处理者。

图 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 的关系如下图:

图 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源码解析系列文章文章来源地址https://www.toymoban.com/news/detail-450161.html

到了这里,关于深入浅出 OkHttp 源码解析及应用实践的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 深入浅出:FFmpeg 音频解码与处理AVFrame全解析

    FFmpeg 是一个开源的音视频处理软件,它包含了一系列的库和程序,用于处理音频、视频和其他多媒体数据。FFmpeg 的名字来源于 “Fast Forward MPEG”,其中 MPEG 是一种常见的音视频编码标准。 FFmpeg 项目于 2000 年由 Fabrice Bellard 启动,他是 QEMU(一种开源的计算机模拟器和虚拟机

    2024年02月04日
    浏览(113)
  • 【深入浅出C#】章节10: 最佳实践和性能优化:内存管理和资源释放

    一、 内存管理基础 1.1 垃圾回收机制 垃圾回收概述 垃圾回收(Garbage Collection)是一种计算机科学和编程领域的重要概念,它主要用于自动管理计算机程序中的内存分配和释放。垃圾回收的目标是识别和回收不再被程序使用的内存,以便释放资源并防止内存泄漏,从而提高程

    2024年02月09日
    浏览(55)
  • 深入浅出解析LoRA完整核心基础知识 | 【算法兵器谱】

    Rocky Ding 公众号:WeThinkIn 【算法兵器谱】栏目专注分享AI行业中的前沿/经典/必备的模型论文,并对具备划时代意义的模型论文进行全方位系统的解析,比如Rocky之前出品的爆款文章Make YOLO Great Again系列。也欢迎大家提出宝贵的优化建议,一起交流学习💪 大家好,我是Rocky。

    2024年02月11日
    浏览(47)
  • 深入浅出阿里数据同步神器:Canal原理+配置+实战全网最全解析!

    canal 翻译为管道,主要用途是基于 MySQL 数据库的增量日志 Binlog 解析,提供增量数据订阅和消费。 早期阿里巴巴因为杭州和美国双机房部署,存在跨机房同步的业务需求,实现方式主要是基于业务 trigger 获取增量变更。从 2010 年开始,业务逐步尝试数据库日志解析获取增量变

    2024年02月10日
    浏览(41)
  • 深入浅出:单链表的实现和应用

      🌱博客主页:青竹雾色间. 😘博客制作不易欢迎各位👍点赞+⭐收藏+➕关注  ✨ 人生如寄,多忧何为  ✨ 目录 前言 单链表的基本概念 节点 头节点 尾节点 单链表的基本操作 创建单链表 头插法: 尾插法: 插入(增)操作  删除(删)操作: 查找(查)操作: 修改(改

    2024年02月08日
    浏览(73)
  • 深入浅出解析Stable Diffusion完整核心基础知识 | 【算法兵器谱】

    Rocky Ding 公众号:WeThinkIn 最新更新: Rocky也一直在更新Stable Diffusion系列的文章内容,包括最新发布的Stable Diffusion XL。Rocky都进行了全方位的深入浅出的解析, 码字真心不易,希望大家能给Rocky正在撰写更新的下面两篇文章多多点赞,万分感谢: 深入浅出完整解析Stable Diffus

    2024年02月10日
    浏览(43)
  • 【知识图谱】深入浅出讲解知识图谱(技术、构建、应用)

    本文收录于《深入浅出讲解自然语言处理》专栏,此专栏聚焦于自然语言处理领域的各大经典算法,将持续更新,欢迎大家订阅! 个人主页:有梦想的程序星空 个人介绍:小编是人工智能领域硕士,全栈工程师,深耕Flask后端开发、数据挖掘、NLP、Android开发、自动化等领域

    2023年04月08日
    浏览(64)
  • 【深入浅出】条件概率的链式法则:定义、公式与应用

    在概率论的研究中,条件概率是一种非常重要的概念。当多个随机事件发生时,我们有时需要考虑它们同时发生的概率。条件概率的链式法则就是一种用于计算多个随机事件同时发生的概率的方法。本文将会介绍条件概率的链式法则的定义、公式以及应用。 条件概率是指在已

    2024年02月08日
    浏览(46)
  • 深入浅出落地应用分析:AI数字人「微软小冰」

    hi,各位,今天要聊的是AI小冰,机缘巧合,投递了这家公司的产品,正好最近在看数字人相关的,就详细剖析下这款产品! 小冰,全称为北京红棉小冰科技有限公司,前身为微软(亚洲)互联网工程院人工智能小冰团队,是微软全球最大的人工智能独立产品研发团队。作为

    2024年03月20日
    浏览(38)
  • 深入浅出Rust内存安全:构建更安全、高效的系统应用

    在过去几年中,Rust编程语言以其独特的安全保障特性和高效的性能,成为了众多开发者和大型科技公司的新宠。尤其是其内存安全特性,成为了广泛讨论和赞扬的焦点。本文旨在深入探讨内存安全的概念、Rust在内存安全方面的独到之处,以及这些特性对系统开发的深远影响

    2024年02月19日
    浏览(51)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包