OKHttp 网络框架的原理在面试过程中经常被问道,笔者希望通过总结文字+流程图的方式来归纳OKHttp的原理。

1. OKhttp是什么?

OKHttp 是由Square公司开源的网络请求框架。Google在Android4.4以后开始将源码中的HttpURLConnection底层实现替换为OKHttp。

2. 为什么要使用OKHttp网络请求框架,或者该网络请求框架有什么优点?

1)支持Http1、Http2、WebSocket

这里顺便复习一下http1.0,  http1.1和http2.0的区别:

http1.0:每次请求都建立短连接,连接不可复用。

http1.1:引入了长连接,但是连接里的请求是排队等待的,其中一个请求超时,后续等待的请求就被阻塞。传输数据格式是文本。

http2.0: 多路复用。多个请求不是像1.1那样排队等待而是并行的,根据请求id来区分不同的请求;而且http2.0使用二进制格式进行传输;http2.0支持头部压缩;支持服务端主动push消息。

2)具有重试和重定向机制

3)支持GZIP压缩数据,减少数据流量

4)可以缓存响应的数据,减少短时间内重复的网络请求

5)底层TCP socket 连接复用,可以减少请求延时

等等,这些优点其实和OKHttp的五大拦截器(重试重定向拦截器、桥接拦截器、缓存拦截器、连接拦截器、请求服务拦截器)密切相关。

3. 如何使用OKHttp? 以同步Get和异步Get请求为例

//同步Get 请求
String url = "http://wwww.baidu.com";
OkHttpClient okHttpClient = new OkHttpClient();
final Request request = new Request.Builder().url(url).build();
final Call call = okHttpClient.newCall(request);
new Thread(new Runnable() {@Overridepublic void run() {try {Response response = call.execute();} catch (IOException e) {e.printStackTrace();}}
}).start();
//异步Get请求
String url = "http://wwww.baidu.com";
OkHttpClient okHttpClient = new OkHttpClient();
final Request request = new Request.Builder().url(url).get().build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {@Overridepublic void onFailure(Call call, IOException e) {Log.d(TAG, "okhttp onFailure: ");}@Overridepublic void onResponse(Call call, Response response) throws IOException {Log.d(TAG, "okhttp onResponse: " + response.body().string());}
});

使用OKHttp发起请求时,使用到三个关键的类OKHttpClient、Call、Request。

这里涉及到的设计模式:Request的创建是 Builder建造者设计模式、OKHttpClient的使用是门面设计模式

调用Call的同步接口execute或者异步接口enqueue后,统一交给分发器Dispatcher进行处理。

分发器把请求交给了拦截器链(责任链设计模式),整个流程如下图

在拦截器链中,传递的是Chain,可以通过Chain获取到Request,返回的是Response,一个U字型的请求响应链。

 4. 什么是分发器Dispatcher,有什么作用?

分发器作用是调配请求任务,内部有一个线程池、维护了三个队列:

//异步请求使用的线程池
private ExecutorService executorService;
//异步请求准备队列
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque();
//异步请求正在执行队列
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque();
//同步请求正在执行队列
private final Deque<RealCall> runningSyncCalls = new ArrayDeque();

分发器内部工作原理流程图(以异步请求为例):实际上就是一个生产——消费者模式, Dispatcher是生产者,线程池是消费者。

1)判断Call是放入ready队列还是Running队列的依据

如果当前正在执行队列的请求数大于64,则放入ready队列;

如果小于64,但是已经存在同一域名主机的请求5个,也还是放入 ready 队列;

2)从ready队列 把请求Call移动到执行队列的条件是什么?

每个请求执行完成就会调用client.dispatcher().finished(call)把当前的call从running队列移除,然后调用Dispatcher的promoteCalls()和1) 相同逻辑的判断,决定是否移动。

3)分发器中的线程池有什么特点?

核心线程数0; 最大线程数:Interger.MAXVALUE;keepAliveTime : 60

可以做到无等待、最大并发。

4)线程池的原理:

当把请求交给线程池时

a. 线程数量小于核心线程数量,新建核心线程处理新来的请求任务。

b. 线程数量大于等于核心线程数量,如果存在空闲线程,使用空闲线程处理新来的请求任务。

c.线程数量大于等于核心线程数量,并且不存在空闲线程,新的请求任务会被添加到等待队列

添加请求任务到等待队列成功:等待空闲线程。

添加请求任务到等待队列失败:

如果线程数量小于最大线程池数量,则新建线程执行新的请求任务

如果线程数量等于最大线程池数量,拒绝该任务

OKhttp的线程池采用的是SyncChronousQueue队列——没有容量的等待队列,添加任务的时候会添加失败,因此会走到上述的步骤c 添加失败,又因为最大线程数是Interger.MAXVALUE,所以就会新建线程处理新的请求任务,这样就能做到无等待,最大并发。

    public synchronized ExecutorService executorService() {if (this.executorService == null) {this.executorService = new ThreadPoolExecutor(0, Interger.MAXVALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue(), Util.threadFactory("OkHttp Dispatcher", false));}return this.executorService;}

5.每一个拦截器的作用是什么?

1)重试重定向拦截器

请求超时可以重试;请求返回根据返回码如果是30X,可以进行重定向

2)桥接拦截器

对Request添加一些请求头,进行GZip压缩和解压缩

3)缓存拦截器

判断当前请求是否存在缓存以及是否可用利用缓存

4)连接拦截器

复用或者新建一个socket连接

5)请求服务拦截器

真正与服务器进行通信,向服务端发送数据和解析服务端的响应数据

6. 连接池复用原理

1)连接池的类位于okhttp3.ConnectionPool:

private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));private final int maxIdleConnections;private final Deque<RealConnection> connections = new ArrayDeque<>();final RouteDatabase routeDatabase = new RouteDatabase();boolean cleanupRunning;

线程池 : 使用的是 SyncChronousQueue队列

Deque<RealConnection>:双向队列

RouteDatabase: 记录连接失败的Route黑名单,当连接失败的时候就会把失败的线路加进去

RealConnection:  对Socket的物理连接的包装, 内部维护了List<Reference<StreamAllocation>>的引用。StreamAllocation的数量是socket被引用的计数,为0代表RealConnection空闲,不为0代表上层有引用,不关闭RealConnection。

2)ConnectionPool提供对Deque<RealConnection>进行操作的方法分别为put放入连接、get获取连接、connectionBecameIdle移除连接、evictAll移除所有连接。

Put方法核心:每次放入新的连接之前,先执行清理空闲连接的线程。

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

Get方法核心:遍历连接缓存列表connections,当Connection中socket的引用计数的次数小于限制大小并且请求的地址和此连接的地址完全匹配。则直接复用该Connection作为request的连接。

RealConnection get(Address address, StreamAllocation streamAllocation) {assert (Thread.holdsLock(this));for (RealConnection connection : connections) {if (connection.allocations.size() < connection.allocationLimit&& address.equals(connection.route().address)&& !connection.noNewStreams) {streamAllocation.acquire(connection);return connection;}}return null;}

清理线程的核心:不停调用Cleanup 清理并返回下次清理的间隔时间。继而进入wait 等待,时间到后释放锁,继续执行下一次Cleanup 清理。

private final Runnable cleanupRunnable = new Runnable() {@Override public void run() {while (true) {long waitNanos = cleanup(System.nanoTime());if (waitNanos == -1) return;if (waitNanos > 0) {long waitMillis = waitNanos / 1000000L;waitNanos -= (waitMillis * 1000000L);synchronized (ConnectionPool.this) {try {ConnectionPool.this.wait(waitMillis, (int) waitNanos);} catch (InterruptedException ignored) {}}}}}};

cleanup核心:

1)首先pruneAndGetAllocationCount()会根据连接中的引用计数来计算空闲连接数idleConnectionCount和活跃连接数inUseConnection。

2)如果空闲连接闲置时间超过keepAliveDurationNs,或者空闲连接数超过maxIdleConnections,则从Deque中移除此连接。

3)如果空闲连接个数大于0则返回此连接即将到期的时间,如果都是活跃连接并且大于0则返回默认的keepAlive时间5分钟,如果没有任何连接则跳出循环并返回-1。

long cleanup(long now) {int inUseConnectionCount = 0;int idleConnectionCount = 0;RealConnection longestIdleConnection = null;long longestIdleDurationNs = Long.MIN_VALUE;// Find either a connection to evict, or the time that the next eviction is due.synchronized (this) {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;}idleConnectionCount++;//寻找闲置时间越大的连接long idleDurationNs = now - connection.idleAtNanos;if (idleDurationNs > longestIdleDurationNs) {longestIdleDurationNs = idleDurationNs;longestIdleConnection = connection;}}//如果空闲连接闲置时间超过keepAliveDurationNs,或者空闲连接数超过maxIdleConnections //则从Deque中移除此 连接。if (longestIdleDurationNs >= this.keepAliveDurationNs|| idleConnectionCount > this.maxIdleConnections) {// We've found a connection to evict. Remove it from the list, then close it below (outside// of the synchronized block).connections.remove(longestIdleConnection);} else if (idleConnectionCount > 0) {//如果空闲连接个数大于0则返回此连接即将到期的时间// A connection will be ready to evict soon.return keepAliveDurationNs - longestIdleDurationNs;} else if (inUseConnectionCount > 0) {//如果都是活跃连接并且大于0则返回默认的keepAlive时间5分钟// All connections are in use. It'll be at least the keep alive duration 'til we run again.return keepAliveDurationNs;} else {//如果没有任何连接则跳出循环并返回-1// No connections, idle or in use.cleanupRunning = false;return -1;}}closeQuietly(longestIdleConnection.socket());// Cleanup again immediately.return 0;}

总结连接池复用的原理:

使用Deque<RealConnection>双向队列来存储连接,通过put、get、connectionBecameIdle和evictAll几个操作来对Deque进行操作。

通过连接Connection中socket的引用计数对象StreamAllocation来判断是否是空闲连接还是活跃连接。

根据算法(空闲连接闲置时间超过keepAliveDurationNs,或者空闲连接数超过maxIdleConnections,则从Deque中移除此连接)进行回收。

复用时,遍历连接缓存列表connections,当Connection中socket的引用计数的次数小于限制大小并且请求的地址和此连接的地址完全匹配。则直接复用该Connection作为request的连接。

7. 面试问题

7.1 OKHttp网络请求做了什么优化?

1)连接池复用,减少了网络请求延时。连接池复用原理如上。

2)无缝支持GZip压缩来减少数据流量。可以在Request的请求头添加("Accept-Encoding", "gzip")告诉服务端数据是经过gzip压缩的。

3)缓存数据减少重复的网络请求。

OKHttp里有一个CacheStrategy对象用于判断是使用缓存还是发起网络请求,该对象内部有两个成员变量networkRequest和cacheResponse。当networkRequest不为空,则发起网络请求;如果networkRequest为空,cacheRespose不为空,则使用缓存;两者都不存在则返回504,请求失败。

4)支持重试和重定向。注意:并不是失败都重试,一般是发生 路由异常或者IO异常并且满足OKHttpClient的重试配置才能重试。

7.2 OKHttp使用了哪些设计模式

Builder(创建Request)、责任链(拦截器)、工厂(如创建Call对象时);门面设计模式(Dispatcher)等。

7.3 回答一下OKHttp是如何进行异步网络请求的

1)Builder设计模式创建OkhttpClient对象和Request对象

2)通过OkhttpClient对象和Request对象创建Call,内部使用了工厂方法设计模式。

3)Call.enqueue把事情交给Dispatcher,Dispatcher相当于生产者,消费者就是线程池。

4)Call根据条件被放入ready队列还是执行队列(正在执行的任务不大于64并且同一个域名请求的连接不超过5个就被放入执行队列)

5)Call执行时经过五大拦截器(责任链设计模式)把请求发送到服务端。服务端返回后需要手动切换到主线程进行Ui更新(如何没有使用Retrofit)

7.4 个人对网络这块做过的优化

1)HttpDns  ip直连。Okhttp提供了dns接口设置自己的HttpDns,可以解决Local DNS域名解析耗时长、域名劫持、跨网访问以及域名缓存等问题。

2)使用webP格式的图片代替jpeg和png格式的图片,节省流量。

3)网络请求合并请求,如业务埋点累计一定的数目后再进行统一上报,而不是每条埋点每次触发就上报。

4)大文件采用分块上传下载,断点续传,降低传输失败率。

5)业务逻辑需要有缓存机制,优先使用本地的内存缓存和磁盘缓存进行UI显示。

6)大量数据按需加载,使用增量更新机制。如需要分页的模块。

OKHttp原理学习总结相关推荐

  1. Retrofit原理学习总结

    Retrofit 网络框架的原理在面试过程中经常被问道,笔者希望通过总结文字+流程图的方式来归纳Retrofit的原理.这篇也是OKHttp原理学习总结_我不勤奋v的博客-CSDN博客 的兄弟篇. 什 ...

  2. Android:安卓学习笔记之OkHttp原理的简单理解和使用

    Android OkHttp使用原理的简单理解和使用 OkHttp 0.前言 1.请求与响应流程 1.1 请求的封装 1.2 请求的发送 1.3 请求的调度 1.4 请求的处理 2.拦截器 2.1 R ...

  3. 征服面试官:OkHttp 原理篇 掌握这篇面试题汇总,吊打面试官!

    前言 如今面试中高级开发工程师岗位,OKhttp 原理是必问环节,只会使用已经无法满足 Android 开发市场的需求,优秀的第三方框架源码剖析不仅能深度理解框架,也能对自己学习带来很大的帮助. 本篇 ...

  4. OkHttp原理解析(二)

    前言 上一篇我们学习了OKHttp的请求执行流程,知道了最终请求流程都会交给getResponseWithInterceptorChain方法来执行,接下来我们就详细分析执行getResponseWi ...

  5. OkHttp 原理解析

    一.前言: HTTP是现代应用常用的一种交换数据和媒体的网络方式,高效地使用HTTP能让资源加载更快,节省带宽.OkHttp是一个高效的HTTP客户端,它有以下默认特性: 支持HTTP/2,允许所有同 ...

  6. Mybatis底层原理学习(二):从源码角度分析一次查询操作过程

    在阅读这篇文章之前,建议先阅读一下我之前写的两篇文章,对理解这篇文章很有帮助,特别是Mybatis新手: 写给mybatis小白的入门指南 mybatis底层原理学习(一):SqlSessionFac ...

  7. Docker镜像原理学习理解

    Docker镜像原理学习理解 一.Docker镜像的组成 1.Docker镜像图层 2.union file system 3.镜像层-bootfs 4.镜像层-rootfs 5.镜像层-依赖环境 6 ...

  8. Redis主从复制原理学习

    Redis主从复制原理学习总结 - 运维笔记 和Mysql主从复制的原因一样,Redis虽然读取写入的速度都特别快,但是也会产生读压力特别大的情况.为了分担读压力,Redis支持主从复制,Redis的 ...

  9. java锁原理_Java锁原理学习

    Java锁原理学习 为了学习Java锁的原理,参照ReentrantLock实现了自己的可重入锁,代码如下: 先上AQS的相关方法: // AQS = AbstractQueuedSynchroniz ...

最新文章

  1. Linux学习笔记重新梳理20180702 之 yum软件包管理器
  2. MySQL 高可用架构 之 MHA (Centos 7.5 MySQL 5.7.18 MHA 0.58)
  3. 双层玻璃窗的功效模型matlab,数学建模实例双层玻璃的功效
  4. 20172331 《Java程序设计》第3周学习总结
  5. [Jsoi2010]连通数
  6. eclipse不能调试某个文件的解决办法
  7. 试题库管理系统--数据库设计
  8. CDA LEVELⅠ2021新版模拟题一(附答案)
  9. java基于springboot的酒店预约管理平台系统
  10. sniffer Pro4.7.5最完整安装教程
  11. 常见路由器默认用户名和密码
  12. 经济学论文素材之汇率波动的外汇风险
  13. BeyondCompare密钥过期怎么办?不用再找新的密钥,一招帮你搞定!
  14. 闰年和平年的区别python_连续四年中一定有一个闰年吗
  15. fiddler手机下载证书提示No root certificate was found. Have you enabled HTTPS traff 解决方法 及手机配置代理后无网络问题
  16. 架构——20——Jenkins+Gitlab实现持续集成——3
  17. 王炜:城市虚拟交通系统与交通发展决策支持模式研究
  18. 不同excel根据某列相同字段值进行关联
  19. ArcGIS教程:最小值和最大值条形图
  20. JavaEE - 集合 - Map集合

热门文章

  1. 微信h5实现复制内容到剪贴板,两种方法
  2. 游程检验与秩和检验的Python实现
  3. 【运动规划算法项目实战】如何实现Dubins曲线和Reeds-Shepp曲线(附ROS C++代码)
  4. RT-Thread Studio移植LAN8720A驱动
  5. 云服务器中的网络及云平台上的服务器集群
  6. Python爬虫:基于Scrapy的爬取失踪人口数据小爬虫
  7. Windows10 1803版本以上找回控制面板语言设置的方法
  8. 问题 L: 零基础学C/C++23——AA制
  9. copilot插件使用介绍
  10. 2015RMBP 初次使用上手