共计 8234 个字符,预计需要花费 21 分钟才能阅读完成。
前言
OkHttp
能够说是 Android
开发中最常见的网络申请框架,OkHttp
使用方便,扩展性强,功能强大,OKHttp
源码与原理也是面试中的常客
然而 OKHttp
的源码内容比拟多,想要学习它的源码往往千头万绪,一时抓不住重点.
本文从几个问题登程梳理 OKHttp
相干知识点,以便疾速构建 OKHttp
常识体系,如果对你有用,欢送点赞~
本文次要包含以下内容
OKHttp
申请的整体流程是怎么的?OKHttp
散发器是怎么工作的?OKHttp
拦截器是如何工作的?- 利用拦截器和网络拦截器有什么区别?
OKHttp
如何复用TCP
连贯?OKHttp
闲暇连贯如何革除?OKHttp
有哪些长处?OKHttp
框架中用到了哪些设计模式?
1. OKHttp
申请整体流程介绍
首先来看一个最简略的 Http
申请是如何发送的。
val okHttpClient = OkHttpClient()
val request: Request = Request.Builder()
.url("https://www.google.com/")
.build()
okHttpClient.newCall(request).enqueue(object :Callback{override fun onFailure(call: Call, e: IOException) { }
override fun onResponse(call: Call, response: Response) {}})
这段代码看起来比较简单,OkHttp
申请过程中起码只须要接触 OkHttpClient
、Request
、Call
、Response
,然而框架外部会进行大量的逻辑解决。
所有网络申请的逻辑大部分集中在拦截器中,然而在进入拦截器之前还须要依附散发器来调配申请工作。
对于散发器与拦截器,咱们在这里先简略介绍下,后续会有更加具体的解说
- 散发器: 外部保护队列与线程池,实现申请调配;
- 拦截器: 五大默认拦截器实现整个申请过程。
整个网络申请过程大抵如上所示
- 通过建造者模式构建
OKHttpClient
与Request
OKHttpClient
通过newCall
发动一个新的申请- 通过散发器保护申请队列与线程池,实现申请调配
- 通过五大默认拦截器实现申请重试,缓存解决,建设连贯等一系列操作
- 失去网络申请后果
2. OKHttp
散发器是怎么工作的?
散发器的次要作用是保护申请队列与线程池, 比方咱们有 100 个异步申请,必定不能把它们同时申请,而是应该把它们排队分个类,分为正在申请中的列表和正在期待的列表,等申请实现后,即可从期待中的列表中取出期待的申请,从而实现所有的申请
而这里同步申请各异步申请又略有不同
同步申请
synchronized void executed(RealCall call) {runningSyncCalls.add(call);
}
因为同步申请不须要线程池,也不存在任何限度。所以散发器仅做一下记录。后续依照退出队列的程序同步申请即可
异步申请
synchronized void enqueue(AsyncCall call) {
// 申请数最大不超过 64, 同一 Host 申请不能超过 5 个
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {runningAsyncCalls.add(call);
executorService().execute(call);
} else {readyAsyncCalls.add(call);
}
}
当正在执行的工作未超过最大限度 64,同时同一 Host
的申请不超过 5 个,则会增加到正在执行队列,同时提交给线程池。否则先退出期待队列。
每个工作实现后,都会调用散发器的 finished
办法, 这外面会取出期待队列中的工作继续执行
3. OKHttp
拦截器是怎么工作的?
通过下面散发器的工作散发,上面就要利用拦截器开始一系列配置了
# RealCall
override fun execute(): Response {
try {client.dispatcher.executed(this)
return getResponseWithInterceptorChain()} finally {client.dispatcher.finished(this)
}
}
咱们再来看下 RealCall
的execute
办法,能够看出,最初返回了getResponseWithInterceptorChain
, 责任链的构建与解决其实就是在这个办法外面
internal fun getResponseWithInterceptorChain(): Response {
// Build a full stack of interceptors.
val interceptors = mutableListOf<Interceptor>()
interceptors += client.interceptors
interceptors += RetryAndFollowUpInterceptor(client)
interceptors += BridgeInterceptor(client.cookieJar)
interceptors += CacheInterceptor(client.cache)
interceptors += ConnectInterceptor
if (!forWebSocket) {interceptors += client.networkInterceptors}
interceptors += CallServerInterceptor(forWebSocket)
val chain = RealInterceptorChain(call = this,interceptors = interceptors,index = 0)
val response = chain.proceed(originalRequest)
}
如上所示,构建了一个 OkHttp
拦截器的责任链
责任链,顾名思义,就是用来解决相干事务责任的一条执行链,执行链上有多个节点,每个节点都有机会(条件匹配)解决申请事务,如果某个节点解决完了就能够依据理论业务需要传递给下一个节点持续解决或者返回处理完毕。
如上所示责任链增加的程序及作用如下表所示:
拦截器 | 作用 |
---|---|
利用拦截器 | 拿到的是原始申请,能够增加一些自定义 header、通用参数、参数加密、网关接入等等。 |
RetryAndFollowUpInterceptor | 处理错误重试和重定向 |
BridgeInterceptor | 应用层和网络层的桥接拦截器,次要工作是为申请增加 cookie、增加固定的 header,比方 Host、Content-Length、Content-Type、User-Agent 等等,而后保留响应后果的 cookie,如果响应应用 gzip 压缩过,则还须要进行解压。 |
CacheInterceptor | 缓存拦截器,如果命中缓存则不会发动网络申请。 |
ConnectInterceptor | 连贯拦截器,外部会保护一个连接池,负责连贯复用、创立连贯(三次握手等等)、开释连贯以及创立连贯上的 socket 流。 |
networkInterceptors(网络拦截器) | 用户自定义拦截器,通常用于监控网络层的数据传输。 |
CallServerInterceptor | 申请拦截器,在前置筹备工作实现后,真正发动了网络申请。 |
咱们的网络申请就是这样通过责任链一级一级的递推上来,最终会执行到 CallServerInterceptor
的intercept
办法,此办法会将网络响应的后果封装成一个 Response
对象并 return
。之后沿着责任链一级一级的回溯,最终就回到getResponseWithInterceptorChain
办法的返回, 如下图所示:
4. 利用拦截器和网络拦截器有什么区别?
从整个责任链路来看,利用拦截器是最先执行的拦截器,也就是用户本人设置 request
属性后的原始申请,而网络拦截器位于 ConnectInterceptor
和CallServerInterceptor
之间,此时网络链路曾经筹备好,只期待发送申请数据。它们次要有以下区别
- 首先,利用拦截器在
RetryAndFollowUpInterceptor
和CacheInterceptor
之前,所以一旦产生谬误重试或者网络重定向,网络拦截器可能执行屡次,因为相当于进行了二次申请,然而利用拦截器永远只会触发一次。另外如果在CacheInterceptor
中命中了缓存就不须要走网络申请了,因而会存在短路网络拦截器的状况。 - 其次,除了
CallServerInterceptor
之外,每个拦截器都应该至多调用一次realChain.proceed
办法。实际上在利用拦截器这层能够屡次调用proceed
办法(本地异样重试)或者不调用proceed
办法(中断),然而网络拦截器这层连贯曾经筹备好,可且仅可调用一次proceed
办法。 - 最初,从应用场景看,利用拦截器因为只会调用一次,通常用于统计客户端的网络申请发动状况;而网络拦截器一次调用代表了肯定会发动一次网络通信,因而通常可用于统计网络链路上传输的数据。
5. OKHttp
如何复用 TCP
连贯?
ConnectInterceptor
的次要工作就是负责建设 TCP
连贯,建设 TCP
连贯须要经验三次握手四次挥手等操作,如果每个 HTTP
申请都要新建一个 TCP
耗费资源比拟多
而 Http1.1
曾经反对 keep-alive
, 即多个Http
申请复用一个 TCP
连贯,OKHttp
也做了相应的优化,上面咱们来看下 OKHttp
是怎么复用 TCP
连贯的
ConnectInterceptor
中查找连贯的代码会最终会调用到 ExchangeFinder.findConnection
办法,具体如下:
# ExchangeFinder
// 为承载新的数据流 寻找 连贯。寻找程序是 已调配的连贯、连接池、新建连贯
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {synchronized (connectionPool) {
// 1. 尝试应用 已给数据流调配的连贯.(例如重定向申请时,能够复用上次申请的连贯)releasedConnection = transmitter.connection;
result = transmitter.connection;
if (result == null) {
// 2. 没有已调配的可用连贯,就尝试从连接池获取。(连接池稍后具体解说)if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {result = transmitter.connection;}
}
}
synchronized (connectionPool) {if (newRouteSelection) {
//3. 当初有了 IP 地址,再次尝试从连接池获取。可能会因为连贯合并而匹配。(这里传入了 routes,下面的传的 null)routes = routeSelection.getAll();
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, false)) {
foundPooledConnection = true;
result = transmitter.connection;
}
}
// 4. 第二次没胜利,就把新建的连贯,进行 TCP + TLS 握手,与服务端建设连贯. 是阻塞操作
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
synchronized (connectionPool) {
// 5. 最初一次尝试从连接池获取,留神最初一个参数为 true,即要求 多路复用(http2.0)// 意思是,如果本次是 http2.0,那么为了保障 多路复用性,(因为下面的握手操作不是线程平安)会再次确认连接池中此时是否已有同样连贯
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
// 如果获取到,就敞开咱们创立里的连贯,返回获取的连贯
result = transmitter.connection;
} else {
// 最初一次尝试也没有的话,就把刚刚新建的连贯存入连接池
connectionPool.put(result);
}
}
return result;
}
下面精简了局部代码,能够看出,连贯拦截器应用了 5 种办法查找连贯
- 首先会尝试应用 已给申请调配的连贯。(已调配连贯的状况例如重定向时的再次申请,阐明上次曾经有了连贯)
- 若没有 已调配的可用连贯,就尝试从连接池中 匹配获取。因为此时没有路由信息,所以匹配条件:
address
统一——host
、port
、代理等统一,且匹配的连贯能够承受新的申请。 - 若从连接池没有获取到,则传入
routes
再次尝试获取,这次要是针对Http2.0
的一个操作,Http2.0
能够复用square.com
与square.ca
的连贯 - 若第二次也没有获取到,就创立
RealConnection
实例,进行TCP + TLS
握手,与服务端建设连贯。 - 此时为了确保
Http2.0
连贯的多路复用性,会第三次从连接池匹配。因为新建设的连贯的握手过程是非线程平安的,所以此时可能连接池新存入了雷同的连贯。 - 第三次若匹配到,就应用已有连贯,开释刚刚新建的连贯;若未匹配到,则把新连贯存入连接池并返回。
以上就是连贯拦截器尝试复用连贯的操作,流程图如下:
6. OKHttp
闲暇连贯如何革除?
下面说到咱们会建设一个 TCP
连接池,但如果没有工作了,闲暇的连贯也应该及时革除,OKHttp
是如何做到的呢?
# RealConnectionPool
private val cleanupQueue: TaskQueue = taskRunner.newQueue()
private val cleanupTask = object : Task("$okHttpName ConnectionPool") {override fun runOnce(): Long = cleanup(System.nanoTime())
}
long cleanup(long now) {
int inUseConnectionCount = 0;// 正在应用的连接数
int idleConnectionCount = 0;// 闲暇连接数
RealConnection longestIdleConnection = null;// 闲暇工夫最长的连贯
long longestIdleDurationNs = Long.MIN_VALUE;// 最长的闲暇工夫
// 遍历连贯:找到待清理的连贯, 找到下一次要清理的工夫(还未到最大闲暇工夫)synchronized (this) {for (Iterator<RealConnection> i = connections.iterator(); i.hasNext();) {RealConnection connection = i.next();
// 若连贯正在应用,continue,正在应用连接数 +1
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
// 闲暇连接数 +1
idleConnectionCount++;
// 赋值最长的闲暇工夫和对应连贯
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
// 若最长的闲暇工夫大于 5 分钟 或 闲暇数 大于 5,就移除并敞开这个连贯
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// else,就返回 还剩多久达到 5 分钟,而后 wait 这个工夫再来清理
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
// 连贯没有闲暇的,就 5 分钟后再尝试清理.
return keepAliveDurationNs;
} else {
// 没有连贯,不清理
cleanupRunning = false;
return -1;
}
}
// 敞开移除的连贯
closeQuietly(longestIdleConnection.socket());
// 敞开移除后 立即 进行下一次的 尝试清理
return 0;
}
思路还是很清晰的:
- 在将连贯退出连接池时就会启动定时工作
- 有闲暇连贯的话,如果最长的闲暇工夫大于 5 分钟 或 闲暇数 大于 5,就移除敞开这个最长闲暇连贯;如果 闲暇数 不大于 5 且 最长的闲暇工夫不大于 5 分钟,就返回到 5 分钟的剩余时间,而后期待这个工夫再来清理。
- 没有闲暇连贯就等 5 分钟后再尝试清理。
- 没有连贯不清理。
流程如下图所示:
7. OKHttp
有哪些长处?
- 应用简略,在设计时应用了外观模式,将整个零碎的复杂性给暗藏起来,将子系统接口通过一个客户端
OkHttpClient
对立裸露进去。 - 扩展性强,能够通过自定义利用拦截器与网络拦截器,实现用户各种自定义的需要
- 功能强大,反对
Spdy
、Http1.X
、Http2
、以及WebSocket
等多种协定 - 通过连接池复用底层
TCP
(Socket
),缩小申请延时 - 无缝的反对
GZIP
缩小数据流量 - 反对数据缓存, 缩小反复的网络申请
- 反对申请失败主动重试主机的其余
ip
,主动重定向
8. OKHttp
框架中用到了哪些设计模式?
- 构建者模式:
OkHttpClient
与Request
的构建都用到了构建者模式 - 外观模式:
OkHttp
应用了外观模式, 将整个零碎的复杂性给暗藏起来,将子系统接口通过一个客户端OkHttpClient
对立裸露进去。 - 责任链模式:
OKHttp
的外围就是责任链模式,通过 5 个默认拦截器形成的责任链实现申请的配置 - 享元模式: 享元模式的外围即池中复用,
OKHttp
复用TCP
连贯时用到了连接池,同时在异步申请中也用到了线程池
总结
本文次要梳理了 OKHttp
原理相干知识点,并答复了以下问题:
OKHttp
申请的整体流程是怎么的?OKHttp
散发器是怎么工作的?OKHttp
拦截器是如何工作的?- 利用拦截器和网络拦截器有什么区别?
OKHttp
如何复用TCP
连贯?OKHttp
闲暇连贯如何革除?OKHttp
有哪些长处?OKHttp
框架中用到了哪些设计模式?
如果对您有所帮忙,欢送点赞,谢谢~
相干视频:
【2021 最新版】Android studio 装置教程 +Android(安卓)零基础教程视频(适宜 Android 0 根底,Android 初学入门)含音视频_哔哩哔哩_bilibili
Android OkHttp 原理解读——带你深刻把握 OkHttp 散发器与拦截器开发_哔哩哔哩_bilibili
【Android 进阶教程】——基于 Okhttp 的可用网络框架原理解析_哔哩哔哩_bilibili
本文转自 https://juejin.cn/post/7020027832977850381,如有侵权,请分割删除。