OkHttp使用起来很简单,先创建OkHttpClient和Request对象,以Request来创建一个RealCall对象,利用它执行异步enqueue或者同步execute操作将请求发送出去,并监听请求失败或者成功的反馈Callback。
这里有三个主要的类需要说明一下:OkHttpClient、Request以及RealCall。
-
OkHttpClient: 相当于配置中⼼,可用于发送 HTTP 请求并读取其响应。它的配置有很多,例如connectTimeout:建⽴连接(TCP 或 TLS)的超时时间,readTimeout :发起请求到读到响应数据的超时时间,Dispatcher:调度器,⽤于调度后台发起的⽹络请求,等等。还有其他配置可查看源码。
-
Request: 一个主要设置网络请求Url、请求方法(GET、POST…)、请求头、请求body的请求类。
-
RealCall: RealCall是由newCall(Request)方法返回,是OkHttp执行请求最核心的一个类之一,用作连接OkHttp的应用程序层和网络层,也就是将OkHttpClient和Request结合起来,发起异步和同步请求。
从上面的使用步骤可以看到,OkHttp最后执行的是okHttpClient.newCall(request).enqueue
,也就是RealCall的enqueue方法,这是一个异步请求,同样的,也可以执行同步请求RealCall.execute()
。
RealCall的同步请求最后其实会调用RealCall.getResponseWithInterceptorChain()
,而RealCall的异步请求是使用线程池先将请求放置到后台处理,但是最后还是会调用RealCall.getResponseWithInterceptorChain()
来获取网络请求的返回值Response。从这里就基本能嗅到网络请求的核心其实与getResponseWithInterceptorChain()方法有关,那到底如何与服务器连接进行网络请求的?这个问题就先抛在这,后面再详细说。
我们先从异步请求enqueue开始,来看异步请求的主要结构。
类:Dispatcher
private fun promoteAndExecute(): Boolean {
…
val executableCalls = mutableListOf()
synchronized(this) {
val i = readyAsyncCalls.iterator()
while (i.hasNext()) {
val asyncCall = i.next()
if (runningAsyncCalls.size >= this.maxRequests) break // Max capacity.
if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue // Host max capacity.
i.remove()
asyncCall.callsPerHost.incrementAndGet()
executableCalls.add(asyncCall)
runningAsyncCalls.add(asyncCall)
}
isRunning = runningCallsCount() > 0
}
for (i in 0 until executableCalls.size) {
val asyncCall = executableCalls[i]
asyncCall.executeOn(executorService)
}
return isRunning
}
异步请求首先会将AsyncCall添加到双向队列readyAsyncCalls中(即准备执行但还没有执行的队列),做请求的准备动作。接着遍历准备执行队列readyAsyncCalls,寻找符合条件的请求,并将其加入到一个保存有效请求的列表executableCalls和正在执行队列runningAsyncCalls中,而这个筛选条件主要有两条:
-
if (runningAsyncCalls.size >= this.maxRequests) break
:并发执行的请求数要小于最大的请求数64。 -
if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue
:某个主机的并发请求数不能超过最大请求数5
也就是说,当我们的并发请求量超过64个或者某个主机的的请求数超过5,则超过的请求暂时不能执行,需要等一等才能再加入执行队列中。
将有效的请求筛选出后并保存,立即开始遍历请求,一一利用调度器Dispatcher里的ExecutorService进行Runnable任务,也就是遍历后加入到线程池中执行这些有效的网络请求。
类:RealCall.AsyncCall
override fun run() {
threadName(“OkHttp ${redactedUrl()}”) {
…
try {
val response = getResponseWithInterceptorChain()
signalledCallback = true
responseCallback.onResponse(this@RealCall, response)
} catch (e: IOException) {
…
responseCallback.onFailure(this@RealCall, e)
}
}
}
上面的代码就是在线程池中执行的请求任务,可以看到try-catch块中有一句 val response = getResponseWithInterceptorChain()
得到网络请求结果resonse ,将返回的response或者错误,通过callback告知给用户。这个callback也就是一开始OkHttp使用时所注册监听的callback。
另外,这个方法是不是很熟悉?因为在上面说明三个主要核心类时提到过,RealCall的同步请求或者异步请求,最后都会走到getResponseWithInterceptorChain()这一步。
网络请求结果response就是通过这个getResponseWithInterceptorChain()方法返回的,那网络请求结果到底是如何拿到的? 与服务器又是如何交互的呢? 我们就来剖析这个方法的内部结构。
=========================================================================
从上面OkHttp的结构分析知道,所有网络请求的细节都封装在getResponseWithInterceptorChain() 这个核心方法中。那我们就来研究一下它的具体实现。
类:RealCall
internal fun getResponseWithInterceptorChain(): Response {
// Build a full stack of interceptors.
val interceptors = mutableListOf()
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,
exchange = null,
request = originalRequest,
connectTimeoutMillis = client.connectTimeoutMillis,
readTimeoutMillis = client.readTimeoutMillis,
writeTimeoutMillis = client.writeTimeoutMillis
)
…
try {
val response = chain.proceed(originalRequest)
…
return response
}
…
}
getResponseWithInterceptorChain()的内部实现是通过一个责任链模式来完成,将网络请求的各个阶段封装到各个链条中(即各个拦截器Interceptor),配置好各个Interceptor后将其放在⼀个List⾥,然后作为参数,创建⼀个RealInterceptorChain对象,并调⽤ chain.proceed(request)来发起请求和获取响应。
在每一条拦截器中,会先做一些准备动作,例如对该请求进行是否可用的判断,或者将请求转换为服务器解析的格式,等等,接着就对请求执行chain.proceed(request)。上面提到getResponseWithInterceptorChain()的内部实现是一个责任链模式,而chain.proceed(request)的作用就是责任链模式的核心所在,将请求移交给下一个拦截器。
OkHttp中连自定义拦截器包括在内,一共有7种拦截器,在这里,网络请求的细节就封装在各个拦截器中,每个拦截器也都有自己的职责,只要把每个拦截器研究清楚,整个网络请求也就明了了。下面就来一一分析这些拦截器的职责。
1、用户自定义拦截器interceptors
用户自定义拦截器是在所有其他拦截器之前,开发者可根据业务需求进行网络拦截器的自定义,例如我们常常自定义Token处理拦截器,日志打印拦截器等。
2、RetryAndFollowUpInterceptor
RetryAndFollowUpInterceptor是一个请求失败和重定向时重试的拦截器。它的内部开启了一个请求循环,每次循环都会先做一个准备动作(call.enterNetworkInterceptorExchange(request, newExchangeFinder)),这个准备动作最主要的目的在于创建一个ExchangeFinder,为请求寻找可用的Tcl或者Tsl连接以及设置跟连接相关的一些参数,如连接编码解码器等。 ExchangeFinder在后面网络连接时,会详细说明。
准备工作做好后便开始了一个网络请求(response = realChain.proceed(request)),这句代码的目的是为了将请求传递给下一个拦截器。同时,会判断当前请求是否会出错以及是否需要重定向。如果出错或者需要重定向,那么就又开始新一轮的循环,直到没有出错和需要重定向为止。
这里出错和重定向的判断标准也简单说一下:
-
判断出错的标准: 利用try-catch块对请求进行异常捕获,这里会捕获RouteException和IOException,并且在出错后都会先判断当前请求是否能够进行重试的操作。
-
重定向标准: 这里判断是否需要重定向,是对Response的状态码Code进行审查,当状态码为3xx时,则表示需要重定向,而后创建一个新的request,进行重试操作。
3、BridgeInterceptor
BridgeInterceptor是用来连接应用程序代码和网络代码的一个拦截器。也就是说该拦截器会帮用户准备好服务器请求所需要的一些配置。可能定义太抽象,我们就先来看一下一个请求Url所对应的服务器请求头是怎么样的?
方法: GET
那它所对应的请求头如下:
GET /wxarticle/chapters/json HTTP/1.1
Host: wanandroid.com
Accept: application/json, text/plain, /
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
User-Agent: Mozilla/5.0 xxx
…
你可能会问,BridgeInterceptor拦截器和这个有什么关系?其实BridgeInterceptor的作用就是帮用户处理网络请求,它会帮助用户填写服务器请求所需要的配置信息,如上面所展示的User-Agent、Connection、Host、Accept-Encoding等。同时也会对请求的结果进行相应处理。
BridgeInterceptor的内部实现主要分为以下三步:
-
为用户网络请求设置Content-Type、Content-Length、Host、Connection、Cookie等参数,也就是将一般请求转换为适合服务器解析的格式,以适应服务器端;
-
通过 chain.proceed(requestBuilder.build())方法,将转换后的请求移交给下一个拦截器CacheInterceptor,并接收返回的结果Response;
-
对结果Response也进行gzip、Content-Type转换,以适应应用程序端。
所以说BridgeInterceptor是应用程序和服务器端的一个桥梁。
4、CacheInterceptor
CacheInterceptor是一个处理网络请求缓存的拦截器。它的内部处理和一些图片缓存的逻辑相似,首先会判断是否存在可用的缓存,如果存在,则直接返回缓存,反之,调用chain.proceed(networkRequest)方法将请求移交给下一个拦截器,有了结果后,将结果put到cache中。
5、ConnectInterceptor
ConnectInterceptor是建立连接去请求的拦截器。
internal fun initExchange(chain: RealInterceptorChain): Exchange {
…
val exchangeFinder = this.exchangeFinder!!
val codec = exchangeFinder.find(client, chain)
val result = Exchange(this, eventListener, exchangeFinder, codec)
this.interceptorScopedExchange = result
this.exchange = result
…
if (canceled) throw IOException(“Canceled”)
return result
}
从它的源码可以看到,它首先会通过ExchangeFinder查询到codec,这个ExchangeFinder是不是很熟悉?在上面RetryAndFollowUpInterceptor分析中,每次循环都会先做创建ExchangeFinder的准备工作。
而这个codec是什么?它是一个编码解码器,来确定是用Http1的方式还是以Http2的方式进行请求。
在找到合适的codec后,作为参数创建Exchange。Exchange内部涉及了很多网络连接的实现,这个后面再详细说,我们先看看是如何找到合适的codec?
如何找到可用连接?
找到合适的codec,就必须先找到一个可用的网络连接,再利用这个可用的连接创建一个新的codec。 为了找到可用的连接,内部使用了大概5种方式进行筛选。
第一种:从连接池中查找
if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {
val result = call.connection!!
return result
}
尝试在连接池中查找可用的连接,在遍历连接池中的连接时,就会判断每个连接是否可用,而判断连接是否可用的条件如下:
-
请求数要小于该连接最大能承受的请求数,Http2以下,最大请求数为1个,并且此连接上可创建新的交换;
-
该连接的主机和请求的主机一致;
如果从连接池中拿到了合格的连接connection,则直接返回。
如果没有拿到,那就进行第二种拿可用连接的方式。
第二种:传入Route,从连接池中查找
if (connectionPool.callAcquirePooledConnection(address, call, routes, false)) {
val result = call.connection!!
return result
}
第二种依然是从连接池中拿,但是这次不同的是,参数里传入了routes,这个routes是包含路由Route的一个List集合,而Route其实指的是连接的IP地址、TCP端口以及代理模式。
而这次从连接池中拿,主要是针对Http2,路由必须共用一个IP地址,此连接的服务器证书必须包含新主机且证书必须与主机匹配。
第三种:自己创建连接
如果前两次从连接池里都没有拿到可用连接,那么就自己创建连接。
val newConnection = RealConnection(connectionPool, route)
call.connectionToCancel = newConnection
try {
newConnection.connect(
connectTimeout,
readTimeout,
writeTimeout,
pingIntervalMillis,
connectionRetryEnabled,
call,
eventListener
)
}
创建连接其实是内部自己在进行socket,tls的连接,这里抛出一个问题在后面解答:TCP/TLS连接是如何实现的?
自己创建好连接后,又做了一次从连接池中查找的操作。
第四种:多路复用置为true,依然从连接池中查找
if (connectionPool.callAcquirePooledConnection(address, call, routes, true)) {
val result = call.connection!!
newConnection.socket().closeQuietly()
return result
}
这次从连接池中查找,requireMultiplexed置为了true,只查找支持多路复用的连接。并且在建立连接后,将新的连接保存到连接池中。
如何找到Http1和Http2的编/解码器?
上面已经分析出寻找可用且健康的连接的几种方式,那对于codec的创建则需要根据这些连接进行Http1和Http2的区分。如果http2Connection不为null,则创建Http2ExchangeCodec,反之创建Http1ExchangeCodec。
找到编解码器后,我们就回到ConnectInterceptor的一开始,利用编解码器codec创建了一个Exchange,而这个Exchange的内部其实是利用Http1解码器或者Http2解码器,分别进行请求头的编写writeRequestHeaders,或者创建Request Body,发送给服务器。
Exchange初始化成功后,就又将请求移交给了下一个拦截器CallServerInterceptor。
6、CallServerInterceptor
CallServerInterceptor是链中最后一个拦截器,主要用于向服务器发送内容,主要传输http的头部和body信息。
其内部利用上面创建的Exchange进行请求头编写,创建Request body,发送请求,得到结果后,对结果进行解析并回传。
7、NetworkInterceptor
networkInterceptor也是属于用户自定义的一种拦截器,它的位置在ConnectInterceptor之后,CallServerInterceptor之前。我们知道第一个拦截器便是用户自定义,那和这个有什么区别呢?
networkInterceptor前面已经存在有多个拦截器的使用,在请求到达该拦截器时,请求信息已经相当复杂了,其中就包括RetryAndFollowUpInterceptor重试拦截器,经过分析知道,每当重试一次,其后面的拦截器也都会被调用一次,这样就导致networkInterceptor也会被调用多次,而第一个自定义拦截器只会调用一次。当我们需要自定义拦截器时,如token、log,为了资源消耗这一点,一般都是使用第一个。
到这里为止,7种拦截器都分析完成。在分析ConnectInterceptor时抛出了一个问题:TCP/TLS连接是如何实现的?
================================================================================
fun connect(
connectTimeout: Int,
readTimeout: Int,
writeTimeout: Int,
pingIntervalMillis: Int,
connectionRetryEnabled: Boolean,
call: Call,
eventListener: EventListener
) {
…
while (true) {
try {
if (route.requiresTunnel()) {
connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener)
if (rawSocket == null) {
// We were unable to connect the tunnel but properly closed down our resources.
break
}
} else {
connectSocket(connectTimeout, readTimeout, call, eventListener)
}
establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener)
eventListener.connectEnd(call, route.socketAddress, route.proxy, protocol)
break
} catch (e: IOException) {
…
}
-
在connect的内部开启了一个while循环,可以看到第一步就是route.requiresTunnel()判断,这个requiresTunnel()方法表示该请求是否使用了Proxy.Type.HTTP代理且目标是Https连接;
-
如果是,则创建一个代理隧道连接Tunnel(connectTunnel)。创建这个隧道的目的在于利用Http来代理请求Https;
-
如果不是,则直接建立一个TCP连接(connectSocket);
-
建立请求协议。
代理隧道是如何创建的?它的内部会先通过Http代理创建一个TLS的请求,也就是在地址url上增加Host、Proxy-Connection、User-Agent首部。接着最多21次的尝试,利用connectSocket开启TCP连接且利用TLS请求创建一个代理隧道。
从这里可以看见,不管是否需要代理隧道,都会开始建立一个TCP连接(connectSocket),那又是如何建立TCP连接的?
private fun connectSocket(
connectTimeout: Int,
readTimeout: Int,
call: Call,
eventListener: EventListener
) {
val proxy = route.proxy
val address = route.address
val rawSocket = when (proxy.type()) {
Proxy.Type.DIRECT, Proxy.Type.HTTP -> address.socketFactory.createSocket()!!
else -> Socket(proxy)
}
this.rawSocket = rawSocket
eventListener.connectStart(call, route.socketAddress, proxy)
rawSocket.soTimeout = readTimeout
try {
Platform.get().connectSocket(rawSocket, route.socketAddress, connectTimeout)
} catch (e: ConnectException) {
throw ConnectException(“Failed to connect to ${route.socketAddress}”).apply {
initCause(e)
}
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
![](https://i-blog.csdnimg.cn/blog_migrate/f7d23037d0d9afa8b0ee8d559fd666ad.jpeg)
尾声
如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。
对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。
这里,笔者分享一份从架构哲学的层面来剖析的视频及资料分享给大家梳理了多年的架构经验,筹备近6个月最新录制的,相信这份视频能给你带来不一样的启发、收获。
PS:之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
架构篇
《Jetpack全家桶打造全新Google标准架构模式》
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
**
![](https://i-blog.csdnimg.cn/blog_migrate/f7d23037d0d9afa8b0ee8d559fd666ad.jpeg)
尾声
如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。
对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。
这里,笔者分享一份从架构哲学的层面来剖析的视频及资料分享给大家梳理了多年的架构经验,筹备近6个月最新录制的,相信这份视频能给你带来不一样的启发、收获。[外链图片转存中…(img-QYtFJGFj-1713257119228)]
PS:之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
[外链图片转存中…(img-YSIJubWL-1713257119229)]
架构篇
《Jetpack全家桶打造全新Google标准架构模式》
[外链图片转存中…(img-nTRiszyr-1713257119230)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!