什么, 还有这么简单的 OkHttp 源码分析?
//查询有没有退货到北京的车 val existingCall = findExistingCallWithHost(call.host)//复用退货到北京的计数器 callsPerHost,用于统计发往北京快递车数量 if (existingCall != null) call.reuseCallsPerHostFrom(existingCall)}}//准备发送快递 promoteAndExecute()}复制代码
首先是放进准备队列中,也就是把快递放进了打包的车中。然后先看看有没有到北京的快递车,有的话直接用它的计数器,用来统计现在有几辆发往北京的快递车了,也就是检查同一个主机已经有几个请求了。然后准备发送
Dispatcher.promoteAndExecute
private fun promoteAndExecute(): Boolean {this.assertThreadDoesntHoldLock()//收集所有要发出去的快递,也就是要执行的请求 val executableCalls = mutableListOf<AsyncCall>()val isRunning: Booleansynchronized(this) {val i = readyAsyncCalls.iterator()//得到迭代器对象用于遍历 while (i.hasNext()) {//遍历 val asyncCall = i.next()//得到下一个快递//很重要的判断,如果 64 个快递车都出发了,也就没快递车了,那么将无法请求 if (runningAsyncCalls.size >= this.maxRequests) break//发往北京的 5 辆快递车也都走了,那么继续下一个快递 if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue // Host max capacity.//上面都通过了,说明有车,并且发往北京的快递车也还有,那么就离开打包车 i.remove()//把去往北京的快递车数量+1asyncCall.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}复制代码
上面的代码很长,但其实只做了这么三件事
先判断还有没有快递车了,没有车的话什么快递都寄不出去了,跳出循环
在判断同一目的地的车辆还有没有了,目的地如果是北京,那就是判断去北京的车还有没有了,没有的话就看下一个快递。如果还有车的话,就把它装进即将出发的车里,然后计数+1,表示又占用了一辆去北京的车
再次循环,将所有要寄出去的快递寄出去
最终啊快递出发还是回到了 AsyncCall 的 executeOn 方法,我们先看看 AsyncCall 对象是什么
internal inner class AsyncCall(private val responseCallback: Callback) : Runnable {}复制代码
其它的我们都不需要关注,我们只需要关注两点,第一它接受了回调,也就是你的电话号。第二,它继承了 Runnable,这是什么?这不就是线程么,创建 Thread,实现 Runnable 接口。没错就是这样喵
上面说到,最终发送是调用了asyncCall.executeOn(executorService)
方法,那我们具体看一下这个方法
asyncCall.executeOn
fun executeOn(executorService: ExecutorService) {client.dispatcher.assertThreadDoesntHoldLock()//暂时定义发送没成功 var success = falsetry {//使用线程池来执行自身 executorService.execute(this)//发送成功了 success = true} catch (e: RejectedExecutionException) {val ioException = InterruptedIOException("executor rejected")ioException.initCause(e)noMoreExchanges(ioException)//失败回调 responseCallback.onFailure(this@RealCall, ioException)} finally {if (!success) {//结束 client.dispatcher.finished(this)}}}复制代码
别看代码很长,其实就是做了一件事,这个 AsyncCall 把自己加入到了线程池,然后由线程池来开启线程执行自己的 run 方法。
小伙伴会问了,什么是线程
池?
线程池可有讲究了,咱们本篇研究的是 OkHttp,这里就不讨论线程池了。以后的文章中会专门研究线程的种种,包括线程池。这里咱们只研究一点,就是这是一个什么样的线程池
还记得刚才 Dispatcher 类中定义了单例并且线程安全的线程池么
@get:Synchronized@get:JvmName("executorService") val executorService: ExecutorServiceget() {if (executorServiceOrNull == null) {//定义了一个缓存线程池 executorServiceOrNull = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS,SynchronousQueue(), threadFactory("$okHttpName Dispatcher", false))}return executorServiceOrNull!!}复制代码
这里就是定义了一个缓存的线程池,什么是缓存线程池?就是具有缓存功能的,如果一个线程工作完了并且 60s 之内又有请求过来,就复用刚才那个线程,这提高了性能。
然后交由线程池之后,线程池会开启线程执行 asyncCall 的 run 方法
asyncCall.run
override fun run() {threadName("OkHttp ${redactedUrl()}") {//定义响应标志位,用于表示请求是否成功 var signalledCallback = falsetimeout.enter()try {//寄快递过程中~~~得到了换回来的货物 val response = getResponseWithInterceptorChain()//走到这步没出错,代表寄快递成功了
signalledCallback = true//打电话给你表示货到了
responseCallback.onResponse(this@RealCall, response)} catch (e: IOException) {//如果寄快递过程没出错,但是 try/catch 还是报异常了只能说 onResponse 中出错了,这就是用户的锅 if (signalledCallback) {Platform.get().log("Callback failure for ${toLoggableString()}", Platform.INFO, e)} else {//失败回调
responseCallback.onFailure(this@RealCall, e)}} catch (t: Throwable) {cancel()//取消请求//okhttp 自己的锅,发送过程中出现了错误
if (!signalledCallback) {val canceledException = IOException("canceled due to $t")canceledException.addSuppressed(t)//失败回调
responseCallback.onFailure(this@RealCall, canceledException)}throw t} finally {//结束
client.dispatcher.finished(this)}}}}复制代码
走到这一步不容易,核心流程算是走完了。最终是通过 getResponseWithInterceptorChain 方法发出去快递并且得到换回来的货。
看懵了?不要紧,我们来梳理一下流程
我要放大招了!
[图片上传中...(image-5673b4-1602425800911-5)]
流程如下:
进入快递点,对应创建 okhttpClient 对象
填写快递单,对应创建 Request 对象
将快递单交给快递小哥,对应创建 RealCall 对象并将 Request 对象交给它
让小哥寄快递并告诉自己的手机号,对应调用 Call 的 enqueue 方法,并传递回调
小哥把快递交给了调度员,并且把电话号告诉了开车的老司机,然后调度员把快递放到了打包车中,对应 dispatcher 的 enqueue 方法和
调度员询问还有没有车了,以及有没有北京的车了两个判断。对应 dispatcher 的 promoteAndExecute 方法
答案是有车的话,就把老司机叫过来,给他分配一辆车,让他开车去把快递送出去。对应于线程池分配一个线程去执行 AsyncCall 的 run 方法
老司机开车一路上会遇到安检,收费站等等地方,对应于拦截器链。
老司机不管是送到了以后拿到了要换的货还是没送到,老司机都会回来,并且照着快递小哥给的电话给你打电话告诉你结果。对应于回调方法的触发
这边是主线流程,是不是相当的简单呢?和我们寄快递没有一点点差别,所以说啊,代码来源于生活,又抽象与生活。
相信有小伙伴肯定不满足于如此简单的主线流程,那么我们就进入 OkHttp 的支线剧情,看看拦截器链具体做了什么
前方高能!!
前方高能!!
前方高能!!
如果你只是想大概关注一下,可以直接拉到后面看结论,前面将要关门放狗,啊不,放源码了!
拦截器链
拦截器嘛,顾名思义就是拦截你的。
比如安检就是拦截你的,老司机开车进出北京市,那肯定要过安检的,毕竟安全为重。上路以后还得过收费站,中间在经过休息区等等。所以拦截器链的作用就是对请求做各种各样的操作的。
拦截器链采用的是责任链模式。看到了吗,一个设计优秀的框架必然会使用很多的经典的设计模式来保证代码的健壮性。所以我们也应该学习这种设计模式,不过今天就不学了,可以关注我,随后我会发布设计模式专栏。
在上面的代码介绍中,我们最终是走到了 getResponseWithInterceptorChain 方法,通过这个方法得到了 reponse 响应,也就是我要换的货。
我们就从这个方法开始看 ,究竟什么是责任链,什么是拦截器
@Throws(IOException::class)internal fun getResponseWithInterceptorChain(): Response {// 构建完整的拦截器栈。val interceptors = mutableListOf<Interceptor>()interceptors += client.interceptors //用户自定义的拦截器//添加默认拦截器 interceptors += RetryAndFollowUpInterceptor(client)interceptors += BridgeInterceptor(client.cookieJar)interceptors += CacheInterceptor(client.cache)interceptors += ConnectInterceptorif (!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)var calledNoMoreExchanges = falsetry {//责任链的精髓所在,这一行代码就开启责任链的传递,然后责任链中的所有拦截器层层调用,然后层层返回 val response = chain.proceed(originalRequest)//这一行代码是责任链全部执行完毕且不会出错才会执行到这里 if (isCanceled()) {response.closeQuietly()throw IOException("Canceled")}return response} catch (e: IOException) {calledNoMoreExchanges = truethrow noMoreExchanges(e) as Throwable} finally {if (!calledNoMoreExchanges) {noMoreExchanges(null)}}}复制代码
如果自己不定义拦截器的话,OkHttp 默认的是五个拦截器。
可能有的小伙伴不太明白责任链到底是怎么运作的
很简单,当调用了 chain.proceed 方法以后即进入下一个拦截器的方法,下一个拦截器依然调用 chain.proceed 方法,重复不断的进入下一个拦截器。这就是请求层层被拦截,响应传回来的时候会被最后一个拦截器拦截,然后在层层的传回来。
如图所示
[图片上传中...(image-e540b3-1602425800911-4)]
这里暂不讨论用户自定义的拦截器,我们看下默认的五个拦截器分别是做什么的
[图片上传中...(image-f2ee49-1602425800911-3)]
这是五个拦截器的作用。我们一个个来看
RetryAndFollowUpInterceptor
总的来说 RetryAndFollowUpInterceptor 就是负责重试和请求重定向的一个拦截器,它还额外做了一个工作就是创建了一个 ExchangeFinder 对象,这个对象就是用来管理连接池为后来的连接做准备的。
class RetryAndFollowUpInterceptor(private val client: OkHttpClient) : Interceptor {
companion object {//最大重试次数或者重定向次数 private const val MAX_FOLLOW_UPS = 20}
@Throws(IOException::class)override fun intercept(chain: Interceptor.Chain): Response {...while (true) {//创建了 ExchangeFinder 对象,这个对象用于管理连接池以及 call.enterNetworkInterceptorExchange(request, newExchangeFinder)var response: Responsevar closeActiveExchange = truetry {...try {//让下一个拦截器来处理 response = realChain.proceed(request)newExchangeFinder = true} catch (e: RouteException) {...} catch (e: IOException) {...}...//后面的拦截器执行完了,拿到 Response,解析看下是否需要重试或重定向,需要则返回新的 Requestval followUp = followUpRequest(response, exchange)//新的 Request 为空,直接返回 responseif (followUp == null) {if (exchange != null && exchange.isDuplex) {call.timeoutEarlyExit()}closeActiveExchange = falsereturn response}val followUpBody = followUp.body//如果 RequestBody 有值且只许被调用一次,直接返回 response
if (followUpBody != null && followUpBody.isOneShot()) {closeActiveExchange = falsereturn response}response.body?.closeQuietly()//超过重试最大次数抛出异常 if (++followUpCount > MAX_FOLLOW_UPS) {throw ProtocolException("Too many follow-up requests: $followUpCount")}//将新的请求赋值给 request,继续循环 request = followUppriorResponse = response} finally {call.exitNetworkInterceptorExchange(closeActiveExchange)}}}}复制代码
BridgeInterceptor
桥接,负责把应用请求转换成网络请求,把网络响应转换成应用响应,就是添加各种响应头信息的。负责把用户构造的请求转换为发送给服务器的请求,把服务器返回的响应转换为对用户友好的响应。 在 Request 阶段配置用户信息,并添加一些请求头。在 Response 阶段,进行 gzip 解压。
有以下几点需要注意:
开发者没有添加 Accept-Encoding 时,自动添加 Accept-Encoding: gzip
自动添加 Accept-Encoding,会对 request,response 进行自动解压
手动添加 Accept-Encoding,不负责解压缩
自动解压时移除 Content-Length,所以上层 Java 代码想要 contentLength 时为-1
自动解压时移除 Content-Encoding
@Throws(IOException::class)override fun intercept(chain: Interceptor.Chain): Response {...val body = userRequest.bodyif (body != null) {//添加各种请求头信息 requestBuilder.header("Content-Type", contentType.toString())requestBuilder.header("Content-Length", contentLength.toString())requestBuilder.header("Host", userRequest.url.toHostHeader())requestBuilder.header("Connection", "Keep-Alive")requestBuilder.header("Accept-Encoding", "gzip")requestBuilder.header("Cookie", cookieHeader(cookies))//让下一个拦截器来做操作 val networkResponse = chain.proceed(requestBuilder.build())cookieJar.receiveHeaders(userRequest.url, networkResponse.headers)
val responseBuilder = networkResponse.newBuilder().request(userRequest)//这里有个坑:如果你在请求的时候主动添加了"Accept-Encoding: gzip" ,transparentGzip=false,那你就要自己解压 if (transparentGzip &&"gzip".equals(networkResponse.header("Content-Encoding"), ignoreCase = true) &&networkResponse.promisesBody()) {val responseBody = networkResponse.bodyif (responseBody != null) {val gzipSource = GzipSource(responseBody.source())val strippedHeaders = networkResponse.headers.newBuilder().removeAll("Content-Encoding").removeAll("Content-Length").build()responseBuilder.headers(strippedHeaders)val contentType = networkResponse.header("Content-Type")responseBuilder.body(RealResponseBody(contentType, -1L, gzipSource.buffer()))}}return responseBuilder.build()}复制代码
CacheInterceptor
我们知道为了节省流量和提高响应速度,Okhttp 是有自己的一套缓存机制的,CacheInterceptor 就是用来负责读取缓存以及更新缓存的。它内部的实现是使用的 OKIO 是进行读取和写入的。
@Throws(IOException::class)override fun intercept(chain: Interceptor.Chain): Response {//读取候选缓存 val cacheCandidate = cache?.get(chain.request())//定义了缓存的策略 val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()//根据策略,不使用网络,又没有缓存的直接报错,并返回错误码 504。if (networkRequest == null && cacheResponse == null) {return ...}// 根据策略,不使用网络,有缓存的直接返回。if (networkRequest == null) {return ...}var networkResponse: Response? = nulltry {//让下一个拦截器进行工作
networkResponse = chain.proceed(networkRequest)} finally {...
}// 接收到网络结果,如果响应 code 式 304,则使用缓存,返回缓存结果 if (cacheResponse != null) {if (networkResponse?.code == HTTP_NOT_MODIFIED) {...return response.also {listener.cacheHit(call, it)}} else {cacheResponse.body?.closeQuietly()}}//7. 读取网络结果。val response = networkResponse!!.newBuilder().cacheResponse(stripBody(cacheResponse)).networkResponse(stripBody(networkResponse)).build()//8. 对数据进行缓存。if (cache != null) {//省略判断,这里使用了 OKio 来进行缓存的写入 return cacheWritingResponse(cacheRequest, response)}//返回响应结果
return response}复制代码
整个方法的流程如下所示:
读取候选缓存,
创建缓存策略,强制缓存、对比缓存等
根据策略,不使用网络,又没有缓存的直接报错,并返回错误码 504。
根据策略,不使用网络,有缓存的直接返回。
前面两个都没有返回,继续执行下一个 Interceptor,即 ConnectInterceptor。
接收到网络结果,如果响应 code 式 304,则使用缓存,返回缓存结果。
读取网络结果。
对数据进行缓存。
返回网络读取的结果。
关于缓存方面,一会放在后面专门讲缓存
ConnectInterceptor
顾名思义,这个拦截器就是用来建立连接的。
还记得在之前的 RetryAndFollowUpInterceptor 中定义的 ExchangeFinder 对象吗,它里面包含了一个连接池,用于在连接池中取得连接对象。
object ConnectInterceptor : Interceptor {@Throws(IOException::class)override fun intercept(chain: Interceptor.Chain): Response {val realChain = chain as RealInterceptorChain//开始连接工作 val exchange = realChain.call.initExchange(chain)//将 exchange 对象传递到下一个拦截器中 val connectedChain = realChain.copy(exchange = exchange)//进行下一个拦截器的工作
return connectedChain.proceed(realChain.request)}}复制代码
这个拦截器里面的代码很少,但是工作作了不少。它真正的建立了连接。并且将 exchange 对象传递给下一个拦截器,这个对象的作用是什么呢?
exchange 对象主要就是用来发送 HTTP 请求和接受响应的。
不妨我们看看连接是如何建立的?如果你并不想关注连接是如何建立的,可以跳过这一部分,直接看结果
RealCall.initExchange
internal fun initExchange(chain: RealInterceptorChain): Exchange {...val exchangeFinder = this.exchangeFinder!! //在 RetryAndFollowUpInterceptor 定义的对象,里面有个连接池 val codec = exchangeFinder.find(client, chain)//查找连接,返回的是 ExchangeCodec 对象 val result = Exchange(this, eventListener, exchangeFinder, codec)//创建 Exchange 用来发送和接受请求 this.interceptorScopedExchange = resultthis.exchange = result...return result}复制代码
ExchangeFinder.find
fun find(client: OkHttpClient,chain: RealInterceptorChain): ExchangeCodec {try {//查找可用的连接
val resultConnection = findHealthyConnection(...)//创建新的 ExchangeCodec 对象 return resultConnection.newCodec(client, chain)} catch (e: RouteException) {...}}复制代码
ExchangeFinder.findHealthyConnection
@Throws(IOException::class)private fun findHealthyConnection(connectTimeout: Int,readTimeout: Int,writeTimeout: Int,pingIntervalMillis: Int,connectionRetryEnabled: Boolean,doExtensiveHealthChecks: Boolean):RealConnection {while (true) {//查找连接
val candidate = findConnection(...)//确认连接是正常的 if (candidate.isHealthy(doExtensiveHealthChecks)) {return candidate}//如果不正常则从连接池取出 candidate.noNewExchanges()...}}复制代码
ExchangeFinder.findConnection
@Throws(IOException::class)private fun findConnection(connectTimeout: Int,readTimeout: Int,writeTimeout: Int,pingIntervalMillis: Int,connectionRetryEnabled: Boolean): RealConnection {if (call.isCanceled()) throw IOException("Canceled")
// 尝试重用连接 val callConnection = call.connection // This may be mutated by releaseConnectionNoEvents()!if (callConnection != null) {...}// 尝试从池中获取连接 if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {...}...// 创建一个新的连接 val newConnection = RealConnection(connectionPool, route)call.connectionToCancel = newConnectiontry {//连接服务器
newConnection.connect(connectTimeout,readTimeout,writeTimeout,pingIntervalMillis,connectionRetryEnabled,call,eventListener)} finally {call.connectionToCancel = null}...synchronized(newConnection) {//添加到连接池中
connectionPool.put(newConnection)call.acquireConnectionNoEvents(newConnection)}return newConnection}复制代码
工作很简单,就是先尝试重用连接,如果没法重用就从连接池中取一个连接,如果没取到就创建新的连接。
可以看到,链接对象最终是 RealConnection。,并且调用了 RealConnection.connect 方法来进行连接。并且经过调用,最后到了 RealConnection.connectSocket 方法。
RealConnection.connectSocket
@Throws(IOException::class)private fun connectSocket(connectTimeout: Int,readTimeout: Int,call: Call,eventListener: EventListener) {...//创建 Socket 对象 val rawSocket = when (proxy.type()) {Proxy.Type.DIRECT, Proxy.Type.HTTP -> address.socketFactory.createSocket()!!else -> Socket(proxy)}this.rawSocket = rawSockettry {//真正进行 Socket 连接
Platform.get().connectSocket(rawSocket, route.socketAddress, connectTimeout)} catch (e: ConnectException) {throw ConnectException("Failed to connect to ${route.socketAddress}").apply {initCause(e)}}try {//okio 中的接口,用来输入,类似于 InputStreamsource = rawSocket.source().buffer()//okio 中的接口 ,用来输出,类似于 OutputStreamsink = rawSocket.sink().buffer()} catch (npe: NullPointerException) {if (npe.message == NPE_THROW_WITH_NULL) {throw IOException(npe)}}}复制代码
这个方法的内容也很简单,我做了部分删减。主要就是创建了 Socket 对象,并且使用 Socket 对象建立了连接,然后使用 OKio 中的接口获得输入/输出流。
ConnectInterceptor 小结
ConnectInterceptor 是一个特别重要的拦截器,在这个拦截器中真正的建立了连接,并且获得了输入输出流,为将来的输入输出进行了准备。
总的来说就是做了这么几个工作:
首先查找是否有可用的连接,没有的话就尝试是否有能重用的连接,没有的话就去连接池中找,连接池中也没有就创建新的连接对象 RealConnection
然后调用连接对象 RealConnection 的 connect 方法,最终创建了 Socket 对象用于真正的连接,然后使用了 OkIO 的的输入输出流,为输入输出做准备
最终返回 Exchange 对象,它是负责发送请求和接受响应的,而真正具体干活的是 ExchangeCodec 对象。
将 Exchange 对象放到拦截链中,让下一个拦截器进行真正的请求和响应
CallServerInterceptor
class CallServerInterceptor(private val forWebSocket: Boolean) : Interceptor {
@Throws(IOException::class)override fun intercept(chain: Interceptor.Chain): Response {val exchange = realChain.exchange!!val request = realChain.requestval requestBody = request.body
//写入请求头,如果是 GET 那么请求已经结束 exchange.writeRequestHeaders(request)var responseBuilder: Response.Builder? = nullif (HttpMethod.permitsRequestBody(request.method) && requestBody != null) {//如果熟悉 HTTP 的小伙伴应该知道 POST 请求会发送两个包,先发送请求头,获得相应为 100 后再发送请求体。if ("100-continue".equals(request.header("Expect"), ignoreCase = true)) {exchange.flushRequest()responseBuilder = exchange.readResponseHeaders(expectContinue = true)exchange.responseHeadersStart()}//写入请求体 requestBody.writeTo(bufferedRequestBody)//读取响应头
responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!var response = responseBuilder.request(request).handshake(exchange.connection.handshake()).sentRequestAtMillis(sentRequestMillis).receivedResponseAtMillis(System.currentTimeMillis()).build()var code = response.coderesponse = if (forWebSocket && code == 101) {...} else {//读取响应体
response.newBuilder().body(exchange.openResponseBody(response)).build()}...return response}}复制代码
为了便于观看,我删除了一些判断逻辑,但是不影响整体流程
流程大致为
先写入请求头,如果是 GET 请求的话就已经请求完毕,POST 请求的话是先发送请求头再发送请求体,会发送两个 TCP 包
然后读取响应头,接着判断过后,读取响应体。
最终将响应的结果返回,这个结果会层层的向上传递,经过上面所有的拦截器。
最终走到了我们自定义的回调处。
拦截器总结
拦截器采用了责任链模式,层层向下请求,请求后的结果层层向上传递
首先经过了 RetryAndFollowUpInterceptor 拦截器,这个拦截器负责重试和重定向,最大重试次数为 20 次。并且在这个对象中创建了 ExchangeFinder 对象,用于管理连接池等,为随后的链接做准备
经过 BridgeInterceptor 拦截器,这个拦截器主要就是帮我们添加一些请求头的和压缩/解压缩的。在这个拦截器中表明了,如果用户自定义了 gzip 请求头,需要自行解压缩,OkHttp 则不再负责解压缩
CacheInterceptor 是负责缓存的,并且内部使用的是 OKio 进行的缓存,缓存策略下面会有讲。
ConnectInterceptor 拦截器是负责建立连接的,最终是通过 RealConnection 对象建立的 Socket 连接,并且获得了输入输出流为下一步读写做准备。RealConnection 对象的获取是优先复用的,没有复用的就从连接池里取,连接池也没的话在创建新的,并加入连接池
CallServerInterceptor 拦截器就是最终的拦截器了,它将负责数据真正的读取和写入。
OkHttp 缓存策略
首先学习一下 HTTP 缓存相关的理论知识,是实现 Okhttp 机制的基础。
HTTP 的缓存机制也是依赖于请求和响应 header 里的参数类实现的。缓存分为两种,一种是强制缓存,一种是对比缓存
强制缓存:
[图片上传中...(image-f65ba5-1602425800909-2)]
客户端先看有没有缓存,有缓存直接拿缓存,如果没缓存的话就请求服务器然后将结果缓存,以备下次请求。
强制缓存使用的的两个标识:
Expires:Expires 的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据。到期时间是服务端生成的,客户端和服务端的时间可能有误差。
Cache-Control:Expires 有个时间校验的问题,所有 HTTP1.1 采用 Cache-Control 替代 Expires。
Cache-Control 的取值有以下几种:
private: 客户端可以缓存。
public: 客户端和代理服务器都可缓存。
max-age=xxx: 缓存的内容将在 xxx 秒后失效
no-cache: 需要使用对比缓存来验证缓存数据。
no-store: 所有内容都不会缓存,强制缓存,对比缓存都不会触发。
对比缓存:
[图片上传中...(image-6caf66-1602425800909-1)]
对比缓存需要服务端参与判断是否继续使用缓存,当客户端第一次请求数据时,服务端会将缓存标识(Last-Modified/If-Modified-Since 与 Etag/If-None-Match)与数据一起返回给客户端,客户端将两者都备份到缓存中 ,再次请求数据时,客户端将上次备份的缓存 标识发送给服务端,服务端根据缓存标识进行判断,如果返回 304,则表示通知客户端可以继续使用缓存。
Last-Modified/If-Modified-Since
Last-Modified 表示资源上次修改的时间。
当客户端发送第一次请求时,服务端返回资源上次修改的时间:
Last-Modified: Tue, 12 Jan 2016 09:31:27 GMT 复制代码
客户端再次发送,会在 header 里携带 If-Modified-Since。将上次服务端返回的资源时间上传给服务端。
If-Modified-Since: Tue, 12 Jan 2016 09:31:27 GMT 复制代码
服务端接收到客户端发来的资源修改时间,与自己当前的资源修改时间进行对比,如果自己的资源修改时间大于客户端发来的资源修改时间,则说明资源做过修改, 则返回 200 表示需要重新请求资源,否则返回 304 表示资源没有被修改,可以继续使用缓存。
上面是一种时间戳标记资源是否修改的方法,还有一种资源标识码 ETag 的方式来标记是否修改,如果标识码发生改变,则说明资源已经被修改,ETag 优先级高于 Last-Modified。
Etag/If-None-Match
ETag 是资源文件的一种标识码,当客户端发送第一次请求时,服务端会返回当前资源的标识码:
ETag: "5694c7ef-24dc"复制代码
客户端再次发送,会在 header 里携带上次服务端返回的资源标识码:
If-None-Match:"5694c7ef-24dc"复制代码
服务端接收到客户端发来的资源标识码,则会与自己当前的资源吗进行比较,如果不同,则说明资源已经被修改,则返回 200,如果相同则说明资源没有被修改,返回 304,客户端可以继续使用缓存。
这是流程图:
[图片上传中...(image-8790e7-1602425800909-0)]
Okhttp 的缓存策略就是根据上述流程图实现的,具体的实现类是 CacheStrategy,CacheStrategy 的构造函数里有两个参数
class CacheStrategy internal constructor(val networkRequest: Request?,val cacheResponse: Response?)复制代码
这两个参数参数的含义如下:
networkRequest:网络请求。
cacheResponse:缓存响应,基于 DiskLruCache 实现的文件缓存,key 是请求中 url 的 md5,value 是文件中查询到的缓存
CacheStrategy 根据之前缓存结果与当前将要发生的 request 的 Header 计算缓存策略。规则如下
具体流程如下所示:
读取候选缓存。
根据候选缓存创建缓存策略。
根据缓存策略,如果不进行网络请求,而且没有缓存数据时,报错返回错误码 504。
根据缓存策略,如果不进行网络请求,缓存数据可用,则直接返回缓存数据。
缓存无效,则继续执行网络请求。
通过服务端校验后,缓存数据可以使用(返回 304),则直接返回缓存数据,并且更新缓存。
读取网络结果,构造 response,对数据进行缓存。
整个流程就是这样,另外说一点,Okhttp 的缓存是根据服务器 header 自动的完成的,整个流程也是根据 RFC 文档写死的,客户端不必要进行手动控制。
Okhttp 的磁盘缓存机制是基于 DiskLruCache 做的,即最近最少使用算法来进行缓存的,使用 okio 作为输入输出。
OkHttp 连接池
因为 HTTP 是基于 TCP,TCP 连接时需要经过三次握手,为了加快网络访问速度,我们可以 Reuqst 的 header 中将 Connection 设置为 keepalive 来复用连接。
Okhttp 支持 5 个并发 KeepAlive,默认链路生命为 5 分钟(链路空闲后,保持存活的时间),连接池有 ConectionPool 实现,对连接进行回收和管理。
连接池真正的实现类是 RealConnectionPool。其中用来保存连接的是这样的对象:
评论