Android中okhttp原理详解

217 阅读14分钟

#目录

  • 1、okhttp工作的大致流程
  • 2、okhttp中的连接
  • 3、Dispatcher和线程池
  • 4、OkHttp中的设计模式
  • 5、OkHttp的优势
  • 6、参考连接

1、okhttp工作的大致流程

val client = OkHttpClient()

val request = Request.Builder() .url("api.example.com/data") .build()

client.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { e.printStackTrace() }

override fun onResponse(call: Call, response: Response) {
    response.use {
        if (!it.isSuccessful) throw IOException("Unexpected code $it")

        val responseData = it.body?.string()
        println(responseData)
    }
}

})

1.1、整体流程

  • (1)、当我们通过OkhttpClient创建一个Call,并发起同步或异步请求时;
  • (2)、okhttp会通过Dispatcher对我们所有的RealCall(Call的具体实现类)进行统一管理,并通过execute()及enqueue()方法对同步或异步请求进行处理;
  • (3)、execute()及enqueue()这两个方法会最终调用RealCall中的getResponseWithInterceptorChain()方法,从拦截器链中获取返回结果;
  • (4)、拦截器链中,依次通过RetryAndFollowUpInterceptor(重定向拦截器)、BridgeInterceptor(桥接拦截器)、CacheInterceptor(缓存拦截器)、ConnectInterceptor(连接拦截器)、CallServerInterceptor(网络拦截器)对请求依次处理,与服务的建立连接后,获取返回数据,再经过上述拦截器依次处理后,最后将结果返回给调用方。 提供两张图便于理解和记忆: okhttp整体流程1

okhttp整体流程2 这张图只画出了请求流程,没有数据返回流程,后期会处理。

1.2、各大拦截器的原理解析

1.2.1、RetryAndFollowUpInterceptor:负责重定向

构建一个StreamAllocation对象,然后调用下一个拦截器获取结果,从返回结果中获取重定向的request,如果重定向的request不为空的话,并且不超过重定向最大次数的话就进行重定向,否则返回结果。注意:这里是通过一个while(true)的循环完成下一轮的重定向请求。

(1)、StreamAllocation为什么在第一个拦截器中就进行创建?
便于取消请求以及出错释放资源。

(2)、StreamAllocation的作用是什么?        StreamAllocation负责统筹管理Connection、Stream、Call三个实体类,具体就是为一个Call(Realcall),寻找( findConnection() )一个Connection(RealConnection),获取一个Stream(HttpCode)。

1.2.2、BridgeInterceptor

负责将原始Requset转换给发送给服务端的Request以及将Response转化成对调用方友好的Response。

具体就是对request添加Content-Type、Content-Length、cookie、Connection、Host、Accept-Encoding等请求头以及对返回结果进行解压、保持cookie等。

1.2.3、CacheInterceptor

CacheInterceptor:负责读取缓存以及更新缓存。 在请求阶段:

  1. 读取候选缓存cacheCandidate;
  2. 根据originOequest和cacheresponse创建缓存策略CacheStrategy;
  3. 根据缓存策略,来决定是否使用网络或者使用缓存或者返回错误。 具体的的缓存策略就是http的缓存策略,详见下图: 在结果返回阶段: 负责将网络结果进行缓存(使用于DiskLruCache)。

okhttp&http缓存策略

强制缓存:当客户端第一次请求数据是,服务端返回了缓存的过期时间(Expires与Cache-Control),没有过期就可以继续使用缓存,否则则不适用,无需再向服务端询问。

对比缓存:当客户端第一次请求数据时,服务端会将缓存标识(Etag/If-None-Match与Last-Modified/If-Modified-Since)与数据一起返回给客户端,客户端将两者都备份到缓存中 ,再次请求数据时,客户端将上次备份的缓存

标识发送给服务端,服务端根据缓存标识进行判断,如果返回304,则表示缓存可用,如果返回200,标识缓存不可用,使用最新返回的数据。

ETag是用资源标识码标识资源是否被修改,Last-Modified是用时间戳标识资源是否被修改。ETag优先级高于Last-Modified。

1.2.4、ConnectInterceptor:负责与服务器建立连接

使用StreamAllocation.newStream来和服务端建立连接,并返回输入输出流(HttpCodec),实际上是通过StreamAllocation中的findConnection寻找一个可用的Connection,然后调用Connection的connect方法,使用socket与服务端建立连接。

复用的核心条件(以 HTTP/1.1 为主)

以下是连接可以复用的 关键条件

条件描述
1️⃣ 相同的 Host(域名) 和 Port(端口)比如都是 api.example.com:443
2️⃣ 使用相同的协议HTTP/1.1 或 HTTP/2 不能混用
3️⃣ 连接未关闭(keep-alive)服务器和客户端都需支持 keep-alive
4️⃣ 没有被其他请求占用一个连接不能同时处理两个 HTTP/1 请求
5️⃣ 无代理或相同的代理使用代理时代理地址必须一致
6️⃣ TLS 连接复用时,SSL 配置要完全一致包括 SNI、证书校验等
HTTP/2 中的连接复用
  • 同一连接中可以并发处理多个请求(多路复用)
  • 只要 host + port 相同且协商出 HTTP/2 协议,就可以复用连接
  • 非常适合高并发场景,如多个图片加载、接口并发调用

以下是 TLS 配置一致性 的核心点(即连接复用时需要一致的方面):

配置项说明
SSLContext / SSLSocketFactory用来创建 TLS 连接的工厂,必须是同一个
HostnameVerifier域名校验器,比如是否接受非标准证书,必须相同
CertificatePinner证书绑定(公钥 pinning)策略一致
TrustManager / TrustStore自定义证书信任策略必须一样
TLS 版本 / Cipher Suites支持的协议、加密算法列表需兼容,特别是 ALPN
SNI(Server Name Indication)例如同一个 IP 不同子域名,是否开启 SNI 影响是否能复用

TLS 层是加密安全的连接通道,它和连接的安全配置绑定,如果配置不一致,可能存在“复用了不可信连接”的风险。

1.2.5、CallServerInterceptor:负责从服务器读取响应的数据

主要的工作就是把请求的Request写入到服务端,然后从服务端读取Response。

  • (1)、写入请求头
  • (2)、写入请求体
  • (3)、读取响应头
  • (4)、读取响应体

2、连接池原理

由于HTTP是基于TCP,TCP连接时需要经过三次握手,为了加快网络访问速度,我们可以Reuqst的header中将Connection设置为keepalive来复用连接。

Okhttp支持5个并发KeepAlive,默认链路生命为5分钟(链路空闲后,保持存活的时间),连接池有ConectionPool实现,对连接进行回收和管理。

2.1、连接池的清理

连接池清理1

ConectionPool在内部使用一个异步线程来清理连接。

当连接池中有连接时:清理任务由cleanup()方法完成,首先执行清理,并返回下次需要清理的间隔时间,调用调用wait() 方法释放锁。等时间到了以后,再次进行清理,并返回下一次需要清理的时间间隔,再次进入wait,以此循环往复。

当连接池中没有连接时:cleanup()返回-1,跳出循环,下次有连接加进来时,再次开启线程进行循环清理。

之所以连接池线程可以跳出循环,是因为,他是子线程,

而looper选择一直阻塞是因为他是主线程,如果跳出,程序执行结束。

连接池原理2

  • 1、首先统计空闲连接数量;
  • 2、然后通过for循环查找最长空闲时间的连接以及对应空闲时长;
  • 3、然后判断这个最长空闲时间的连接是否超出最大空闲连接数或者或者超过最大空闲时间,满足其一则清除最长空闲的连接。如果不满足清理条件,则返回一个对应等待时间。
  • 这个对应等待的时间又分二种情况:
  • 1 有空闲连接:则返回:keepAliveDurationNs-longestIdleDurationNs;
  • 2 没有空闲的连接,则返回:keepAliveDurationNs
  • 注意:清除一个空闲连接后,会返回0,再次立即开始清理。

如何统计空闲连接呢? 统计空闲连接 StreamAllocation创建或者复用一个Connection后,会将自己添加到Connection的connection.allocations列表中,数据读取完毕之后,会将自己从Connection的connection.allocations中移除,所以判读一个Connection是否是空闲连接可以采用引用计数法,判断connection.allocations列表中是否有StreamAllocation,如果没有就是空闲连接,否则不是。

3、OkHttp中Dispatcher和线程池

3.1、OkHttp中线程池

public synchronized ExecutorService executorService() {
    if (executorService == null) {
      executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
          new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
    }
    return executorService;
  }

OkHttp中的线程池是一个 newCachedThreadPool。 所以在 OkHttp 中线程池只是一个辅助作用,仅仅是用来做线程缓存,便于复用的。 真正控制请求并发数量和执行时机是通过调度器 Dispatcher 完成的。

3.2、OkHttp中Dispatcher

RealCall.execute

RealCall.execute

先将RealCall加入Dispatcher的runningSyncCalls队列,然后调用getResponseWithInterceptorChain获取Response,最后调用Dispatcher的finished方法,将自身从runningSyncCalls移除,然后进行轮询readyAsyncCalls队列,取出ready的异步任务在满足条件的情况下进行执行。

RealCall.enqueue

RealCall.enqueue

如果当前正在执行的RealCall的数量小于最大并发数maxRequest(64),并且该call对应的Host上的call小于同一host上的最大并发数maxRequestsPerHos(5),则将该call加入runningAsyncCalls,并将这个call放到线程池中进行执行,否则加入readyAsyncCall排队等待。

注意:

同步请求和异步请求执行完成之后,都会调用dispatcher的finished方法,将自身从对应的队列中移除,然后进行轮询readyAsyncCalls队列,取出ready的异步任务在满足条件下放到线程池中执行。 轮询readyAsyncCalls

Dispatcher.中的并发数量及三个队列的作用
  • maxRequests = 64 // 最大并发请求数为64
  • maxRequestsPerHost = 5 //每个主机最大请求数为5
  • ExecutorService executorService //消费者池(也就是线程池)
  • Deque readyAsyncCalls: // 异步的缓存,正在准备被消费的(用数组实现,可自动扩容,无大小限制)
  • Deque runningAsyncCalls //正在运行的 异步的任务集合,仅仅是用来引用正在运行的任务以判断并发量,注意它并不是消费者缓存
  • Deque runningSyncCalls //正在运行的,同步的任务集合。仅仅是用来引用正在运行的同步任务以判断并发量

4、OkHttp中的设计模式

  • 责任链模式:拦截器链
  • 单例模式:线程池
  • 观察者模式:各种回调监听
  • 策略模式:缓存策略
  • Builder模式:OkHttpClient的构建过程
  • 外观模式:OkHttpClient封装了很对类对象
  • 工厂模式:Socket的生产

5、OkHttp的优势

5.1、功能方面:

功能全面,满足了网络请求的大部分需求。

5.2、网络优化方面:

  • (1)内置连接池,支持连接复用
  • (2)支持gzip压缩响应体
  • (3)通过缓存避免重复的请求
  • (4)支持http2,对一台机器的所有请求共享同一个socket

5.3、扩展性方面:

拦截器模式使得我们很容易使得我们很容易添加一个自定义拦截器对请求和返回结果进行处理。

http2

头部压缩

HTTP/2 请求头是通过一种名为 HPACK 的算法进行压缩的。HPACK 是 HTTP/2 专用的头部压缩格式,它可以大幅减少冗余头信息,提高传输效率

一句话总结:

HTTP/2 使用 HPACK 算法对请求/响应头部进行压缩,通过“静态表 + 动态表 + 哈夫曼编码”组合实现高效传输。

HPACK 的核心原理

1. 静态表(Static Table)

HPACK 预定义了一张常见 Header 的列表(编号从 1 开始),比如:

编号Header
1:authority
2:method: GET
3:method: POST
......

这些常见头可以直接用编号代替,减少字节量。

2. 动态表(Dynamic Table)

  • 每当客户端或服务器发送一个新的头字段时,可以将其加入动态表。
  • 后续如果再次使用相同头字段,只需引用该条目的索引。

3. 哈夫曼编码(Huffman Coding)

HPACK 使用一套自定义的哈夫曼表对字符串(如头的键值)进行压缩。


🔧 一个请求头压缩的例子

假设请求如下:

http
复制编辑
GET /index.html HTTP/2
Host: example.com
User-Agent: Chrome/120
Accept: */*

第一次发送(没有动态表):

  • :method: GET → 静态表索引 2
  • Host: example.com → 不在静态表,作为新条目加入动态表
  • User-AgentAccept 也是新头,加入动态表并使用哈夫曼编码压缩字符串

第二次发送(与前一次相同):

  • :method: GET → 发送索引 2
  • Host → 发送动态表索引 62(例如)
  • User-Agent → 发送动态表索引 63
  • 所有字段都变成发送几个字节的“索引编号”

这就比 HTTP/1.1 的重复发送文本高效太多了。

HPACK 中的 静态表(Static Table)动态表(Dynamic Table) 是分别存储在 HTTP/2 通信双方的内存中(客户端和服务器各自维护一份)

举个例子:

  • 客户端发送一个头:User-Agent: Chrome
  • 编码器将它加入动态表(在本地内存中)
  • 服务端接收后,也将这个头添加到自己的解码器的动态表
  • 下一次再发送这个字段,客户端只需要发送“动态表索引”即可

所以,动态表是“每个连接内状态”的一部分,存在内存中,会因流量动态变化。

HPACK 使用哈夫曼编码来压缩 Header 中的“字符串部分”——即 Header 的名字(name)和内容(value)都可以被编码。

  1. HPACK 动态表是按顺序构建的,所有新增字段顺序一致
  2. HTTP/2 是基于“连接”的协议,一个连接内,动态表状态始终同步

哈夫曼编码通过“高频字符短编码,低频字符长编码”来减少整体比特数,从而实现压缩。

多路复用

一句话概括

HTTP/2 的多路复用允许在一个 TCP 连接中并发处理多个请求和响应,避免了 HTTP/1.x 中“队头阻塞”问题,大幅提升性能。


🔧 多路复用原理详解

1. 单连接,多流(Stream)

  • HTTP/2 在 一个 TCP 连接 上通过 流(Stream) 来并发传输多个请求和响应。
  • 每个流都有一个唯一的 Stream ID(奇数为客户端发起,偶数为服务端发起)。

2. 帧(Frame)机制

  • 所有数据都被拆成 二进制帧(Frame) 传输。

  • 每个帧都包含:

    • 帧类型(如 HEADERS, DATA, WINDOW_UPDATE 等)
    • 所属的流 ID
    • 载荷数据

比如:一个请求的 HEADERS 帧、多个 DATA 帧可以交错传输,但都带着 Stream ID,接收方可以重新组装。


3. 交错发送,不阻塞

  • 多个流的帧可以**交错(interleave)**在同一个连接中发送,不再是串行阻塞。
  • 所有流都独立,不会互相影响。

✅ 示例:

HTTP/1.1 中如果第一个请求卡住,后面的全都等着(队头阻塞)
HTTP/2 中多个请求可以同时发,同时收,互不阻塞


4. 流优先级与权重(可选)

  • 客户端可以为每个流设置优先级,告诉服务器谁更重要。
  • 用于资源协调(如优先加载首屏 HTML)。

5. 配合头部压缩(HPACK)

  • 多路复用带来的并发性能提升 + HPACK 带来的体积优化 = 更快的网页加载体验

6、参考链接