OkHttp 基本使用源码分析

2024-08-30 01:18

本文主要是介绍OkHttp 基本使用源码分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本文介绍了 OkHttp 的基本使用以及源码分析,强烈建议配合源码进行阅读,否则会不知所云!!!

第一次写源码分析类文章,辛苦各位老铁指正

本文基于 OkHttp 3.11.0 版本进行分析,查看源码时请对应,或者直接下载文末的 Demo 进行查看

文章目录

    • OkHttp 的基本使用
      • 同步请求
      • 异步请求
    • OkHttp 的源码分析
      • 同步请求
      • 异步请求
    • OkHttp 的任务调度(Dispatcher)
    • OkHttp 拦截器
      • 官方定义
      • 基本流程
      • RetryAndFollowUpInterceptor(重试)
      • BridgeInterceptor(桥接)
      • CacheInterceptor(缓存)
      • ConnectInterceptor(连接)
      • CallServerInterceptor(请求)
    • Demo 地址

OkHttp 的基本使用

同步请求

  1. 创建 OkHttpClient 和 Request 对象
  2. 将 Request 封装成 Call 对象
  3. 调用 Call 的 execute() 发送同步请求
private fun synRequest() {val client = OkHttpClient.Builder().readTimeout(5, TimeUnit.SECONDS).build()//1val request = Request.Builder().url("https://www.baidu.com").get().build()//2GlobalScope.launch(Dispatchers.Main) {text.text = withContext(Dispatchers.IO) {val call = client.newCall(request)//3val response = call.execute()//4response.body()?.string()}}
}

注意事项:

  • 发送请求后,就会进入阻塞状态,直到收到响应

异步请求

  1. 创建 OkHttpClient和 Request 对象
  2. 将 Request封装成 Call 对象
  3. 调用 Call 的 enqueue 方法进行异步请求
private fun asyncRequest() {val client = OkHttpClient.Builder().readTimeout(5, TimeUnit.SECONDS).build()//1val request = Request.Builder().url("https://www.baidu.com").get().build()//2val call = client.newCall(request)//3call.enqueue(object : Callback {//4override fun onFailure(call: Call, e: IOException) {}override fun onResponse(call: Call, response: Response) {Log.d(TAG, "onResponse Thread: ${Thread.currentThread().name}")val result = response.body()?.string()GlobalScope.launch(Dispatchers.Main) {text.text = result}}})
}

注意事项:

  • onResponse 和 onFailure 都回调在子线程

OkHttp 的源码分析

总体流程:

总体流程

同步请求

同步请求相对简单,从 RealCall.execute 方法开始

流程图:

同步请求流程图

其中 getResponseWithInterceptorChain 方法中的内容将在后面讲述

异步请求

从 RealCall.enqueue 方法开始:

流程图:

异步请求流程图

  1. 判断当前 call 是否只执行了一次,否则抛出异常
  2. 创建一个 AsyncCall 对象,它其实是一个 Runable 对象
  3. 通过 client.dispatcher().enqueue() 传入 AsyncCall 对象执行异步请求,如果当前运行的异步任务队列(runningAsyncCalls)元素个数小于 maxRequests 并且当前请求的 Host 个数小于 maxRequestsPerHost 则直接放进运行对象并执行当前的任务,否则放进准备队列中(readyAsyncCalls)
  4. 如果第 3 步可以执行,则会调用 AsyncCall 的 execute 方法,之后调用 getResponseWithInterceptorChain() 获取 Response,执行 onResponse 或 onFailure。这也印证了上面提到的异步任务回调是在子线程中
  5. execute 方法末尾会必调 client.dispatcher().finished(this) 方法
  6. finished 方法中会做三件事:a. 从 runningAsyncCalls 中清除当前任务 b. 通过 promoteCalls 方法调整整个异步任务队列,主要工作是判断准备队列 readyAsyncCalls 中的任务是否可以执行 c. 重新计算正在执行的任务数量,为 0 则执行 idleCallback

默认 maxRequests=64, maxRequestsPerHost=5

OkHttp 的任务调度(Dispatcher)

Dispatcher 用于维护同步和异步的请求状态,并维护一个线程池来执行请求。同步请求相对简单,主要说异步相关的内容,下面代码是 Dispatcher 类中几个非常重要的字段:

  /** 线程池,通过懒加载创建. */private @Nullable ExecutorService executorService;/** 准备异步任务队列 */private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();/** 正在运行的异步任务队列,包括未结束却已取消的任务 */private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();/** 正在运行的同步任务队列,包括未结束却已取消的任务 */private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

executorService 通过懒加载方式创建:

  • corePoolSize 为 0 即核心线程池数量为 0,表示空闲一段时间之后将线程全部销毁
  • maximumPoolSize 线程池最大数量设置为 int 最大值
  • keepAliveTime 当前线程数大于核心线程数时多余空闲线程存活的时间,即 60s
 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;}

异步请求为什么需要两个队列?

可以理解成生产者和消费者模型

  • dispatcher 生产者
  • executorService 消费者池
  • readyAsyncCalls 缓存
  • runningAsyncCalls 正在运行的任务

readyAsyncCalls 队列中的线程调用时机在哪?

从上一小节的流程图就可以看出 AsyncCall 的 execute 方法调用结束后必然调用 client.dispatcher().finished(this),最终会走到 Dispatcher.promoteCalls 方法中,判断 runningAsyncCalls 的数量是否符合要求,若符合则直接添加到 runningAsyncCalls 队列中并通过线程池执行任务

  private void promoteCalls() {if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {AsyncCall call = i.next();if (runningCallsForHost(call) < maxRequestsPerHost) {i.remove();runningAsyncCalls.add(call);executorService().execute(call);}if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.}}

流程图:

readyAsyncCalls 调用时机

OkHttp 拦截器

官方定义

拦截器是 OkHttp 中提供一种强大机制,它可以实现网络监听、请求以及响应重写、请求失败重试等功能

官方定义拦截器

基本流程

流程图

拦截器流程图

  1. 创建一系列拦截器,并将其放入一个拦截器 list 中
  2. 创建一个拦截器链 RealInterceptorChain,传入拦截器集合,RealInterceptorChain 内部包含当前拦截器访问的 index 用于控制访问 list 中第几个拦截器
  3. 首次传入 index=0,并执行拦截器链的 proceed 方法
  4. 执行当前拦截器 interceptor.intercept
  5. 当前拦截器执行 RealInterceptorChain.proceed 返回 response
  6. index+1,并重复第2步
  7. 对response进行处理,返回给上一个拦截器

OkHttp 内部的拦截器调用关系

OkHttp 内部的拦截器调用关系

RetryAndFollowUpInterceptor(重试)

该拦截器用于处理 OkHttp 的重试逻辑

  1. 创建 StreamAllocation 对象,传递给下一个拦截器(后面会提到)
  2. 内部有一个 While(true) 循环,用于触发重试逻辑
  3. 循环内调用 RealInterceptorChain.proceed(…) 进行网络请求(即调用下一个拦截器) 得到 response
  4. 循环内有 followUpCount 变量用于控制最大重试次数,最大 20 次
  5. 根据异常结果或响应码判断是否进行重新请求
  6. 将 response 返回给上一个拦截器

BridgeInterceptor(桥接)

负责将用户构建的一个 Request 请求转化为能够进行网络访问的请求,主要是请求头 Header 的构建

另外如果是 gzip 资源还会处理 gzip 的解压等

CacheInterceptor(缓存)

在说 CacheInterceptor 拦截器前需要先看下 OkHttp 的缓存的基本配置和使用:

private fun cacheRequest() {val cacheFile = File(externalCacheDir, "okHttpCacheFile")val client = OkHttpClient.Builder().cache(Cache(cacheFile, 1024 * 1024 * 10))//指定缓存目录 缓存最大容量(10M).readTimeout(5, TimeUnit.SECONDS).build()val request = Request.Builder().url("https://www.xxx.com").cacheControl(CacheControl.Builder()//设置max-age为5分钟之后,这5分钟之内不管有没有网, 都读缓存.maxAge(5, TimeUnit.MINUTES)// max-stale设置为5天,意思是,网络未连接的情况下设置缓存时间为5天.maxStale(5, TimeUnit.DAYS).build()).get().build()val call = client.newCall(request)call.enqueue(object : Callback {override fun onFailure(call: Call, e: IOException) {GlobalScope.launch(Dispatchers.Main) {responseText.text = "失败:${e.message}"}}override fun onResponse(call: Call, response: Response) {Log.d(TAG, "onResponse Thread: ${Thread.currentThread().name}")val result = response.body()?.string()GlobalScope.launch(Dispatchers.Main) {responseText.text = resultprintLog(response.cacheResponse()?.toString() ?: "cacheResponse 为空")}}})
}

需要注意的是 Cache 的生效与否也取决于服务端是否支持

基本流程

需要注意以下几点

  • OkHttp 缓存采用 DiskLruCache 进行存储,即使用 LRU 算法
  • 从源码看 OkHttp 并不支持非 Get 请求的缓存

从 Cache.put 方法可以看到:

if (!requestMethod.equals("GET")) {// Don't cache non-GET responses. We're technically allowed to cache// HEAD requests and some POST requests, but the complexity of doing// so is high and the benefit is low.return null;
}
  • 缓存的内容不光只缓存 response 的 body 的内容,会将 response 的 header 也会存起来

ConnectInterceptor(连接)

基本流程

RealConnection:用于实际网络传输的对象

HttpCodec: 用于编码 request,解码 response

连接池(ConnectionPool)

主要作用是在一定的范围内复用连接(Connection),同时进行有效的清理回收 Connection

连接池结构图:

连接池结构图

OkHttp 的每次请求都会产生一个 StreamAllocation 对象,会将其弱引用添加到 RealConnection.allocations 集合中( (RealConnection.findConnection 中负责添加),这个 allocations 集合主要是为了判断每一个 Connection 是否超过了最大连接数以及后面提到回收算法所使用

ConnectionPool 内部维护一个 ArrayDeque 队列 (connections) 用于存放 RealConnection,ConnectionPool 的 get 方法就负责遍历这个 connections 队列,从中取出符合资格的 connection (即调用 RealConnection.isEligible 进行判断),获得成功后赋值给 streamAllocation

ConnectionPool 的 put 方法相对简单,通过调用 connections.add(connection) 将 connection 添加到连接池。特别的在 put 方法中会执行 cleanupRunnable,可以看下面的源码:

void put(RealConnection connection) {assert (Thread.holdsLock(this));if (!cleanupRunning) {cleanupRunning = true;executor.execute(cleanupRunnable );}connections.add(connection);
}

ConnectionPool 的自动回收

其中 cleanupRunnable 就负责对连接池中的 Connection 的进行自动回收。

特点如下:

  • 内部有一个死循环实现自动回收
  • 利用了类似 Java GC 的标记清除算法进行回收
  • 监控每个 Connection 的 StreamAllocation 的数量,当其为 0 时进行回收

通过 OkHttp 连接池的回收,就可以保持多个“健康”的 keep-alive 连接

private final Runnable cleanupRunnable = new Runnable() {@Override public void run() {while (true) {...long waitNanos = cleanup(System.nanoTime());...try {ConnectionPool.this.wait(waitMillis, (int) waitNanos);} catch (InterruptedException ignored) {}...}}
};

OkHttp 是如何标记可回收连接的呢?

在 ConnectionPool.cleanup() 方法中可以看到,会遍历每一个连接,如果 pruneAndGetAllocationCount 方法返回大于 0 则无需回收,否则之后的代码就会标记该 connection

long cleanup(long now) {...for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {RealConnection connection = i.next();// If the connection is in use, keep searching.if (pruneAndGetAllocationCount(connection, now) > 0) {inUseConnectionCount++;continue;}}...
}

再来看这个 pruneAndGetAllocationCount 方法,很关键一步就是判断 connection.allocations 中的弱引用对象是否为空,如果不为空则继续遍历,为空则会 remove,最后返回集合剩余大小

private int pruneAndGetAllocationCount(RealConnection connection, long now) {List<Reference<StreamAllocation>> references = connection.allocations;for (int i = 0; i < references.size(); ) {Reference<StreamAllocation> reference = references.get(i);if (reference.get() != null) {i++;continue;}...references.remove(i);...}...return references.size();
}

CallServerInterceptor(请求)

该拦截器负责发起真正的网络请求以及接受网络响应

简单的总结下:

  • 调用 httpCodec.writeRequestHeaders(request) 向 Socket 中写入 Header 信息
  • 调用 request.body().writeTo(bufferedRequestBody) 向 Socket 中写入请求 Body 信息
  • 调用 httpCodec.finishRequest() 完成请求体的写入
  • 调用 httpCodec.readResponseHeaders(false) 读取网络响应的 Header 信息
  • 调用 httpCodec.openResponseBody(response) 读取网络响应的 Body 信息
  • 返回 response,给上一个拦截器

Demo 地址

https://github.com/changer0/FrameworkLearning

以上就是本节内容,欢迎大家关注👇👇👇

长按关注

这篇关于OkHttp 基本使用源码分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1119385

相关文章

中文分词jieba库的使用与实景应用(一)

知识星球:https://articles.zsxq.com/id_fxvgc803qmr2.html 目录 一.定义: 精确模式(默认模式): 全模式: 搜索引擎模式: paddle 模式(基于深度学习的分词模式): 二 自定义词典 三.文本解析   调整词出现的频率 四. 关键词提取 A. 基于TF-IDF算法的关键词提取 B. 基于TextRank算法的关键词提取

使用SecondaryNameNode恢复NameNode的数据

1)需求: NameNode进程挂了并且存储的数据也丢失了,如何恢复NameNode 此种方式恢复的数据可能存在小部分数据的丢失。 2)故障模拟 (1)kill -9 NameNode进程 [lytfly@hadoop102 current]$ kill -9 19886 (2)删除NameNode存储的数据(/opt/module/hadoop-3.1.4/data/tmp/dfs/na

Hadoop数据压缩使用介绍

一、压缩原则 (1)运算密集型的Job,少用压缩 (2)IO密集型的Job,多用压缩 二、压缩算法比较 三、压缩位置选择 四、压缩参数配置 1)为了支持多种压缩/解压缩算法,Hadoop引入了编码/解码器 2)要在Hadoop中启用压缩,可以配置如下参数

Makefile简明使用教程

文章目录 规则makefile文件的基本语法:加在命令前的特殊符号:.PHONY伪目标: Makefilev1 直观写法v2 加上中间过程v3 伪目标v4 变量 make 选项-f-n-C Make 是一种流行的构建工具,常用于将源代码转换成可执行文件或者其他形式的输出文件(如库文件、文档等)。Make 可以自动化地执行编译、链接等一系列操作。 规则 makefile文件

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

使用opencv优化图片(画面变清晰)

文章目录 需求影响照片清晰度的因素 实现降噪测试代码 锐化空间锐化Unsharp Masking频率域锐化对比测试 对比度增强常用算法对比测试 需求 对图像进行优化,使其看起来更清晰,同时保持尺寸不变,通常涉及到图像处理技术如锐化、降噪、对比度增强等 影响照片清晰度的因素 影响照片清晰度的因素有很多,主要可以从以下几个方面来分析 1. 拍摄设备 相机传感器:相机传

基本知识点

1、c++的输入加上ios::sync_with_stdio(false);  等价于 c的输入,读取速度会加快(但是在字符串的题里面和容易出现问题) 2、lower_bound()和upper_bound() iterator lower_bound( const key_type &key ): 返回一个迭代器,指向键值>= key的第一个元素。 iterator upper_bou

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

pdfmake生成pdf的使用

实际项目中有时会有根据填写的表单数据或者其他格式的数据,将数据自动填充到pdf文件中根据固定模板生成pdf文件的需求 文章目录 利用pdfmake生成pdf文件1.下载安装pdfmake第三方包2.封装生成pdf文件的共用配置3.生成pdf文件的文件模板内容4.调用方法生成pdf 利用pdfmake生成pdf文件 1.下载安装pdfmake第三方包 npm i pdfma

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]