??今天我們來看看OkHttp
中另外一個大頭--Interceptor。Okhttp
真正的網絡請求都在這個攔截器鏈中執行的,所以分析OkHttp
的攔截器鏈是非常有必要的。
1. 概述
??在正式分析攔截器鏈之前,我們先來每一個攔截器有一個大概的認識,同時對攔截器鏈的調用過程有一個宏觀上的理解。
??在OkHttp
中,給我們分配了5種攔截器,它們分別是:RetryAndFollowUpInterceptor
、BridgeInterceptor
、CacheInterceptor
、ConnectInterceptor
、CallServerInterceptor
。這里先對每一種攔截器的作用做一個總結。
類名 | 作用 |
---|---|
RetryAndFollowUpInterceptor | 主要負責網絡請求的失敗重連 |
BridgeInterceptor | 主要是將用戶的Request轉換為能夠真正進行網絡請求的Request,負責添加一些相應頭;其次就是將服務器返回的Response解析成為用戶用夠真正使用的Response,負責GZip解壓之類的 |
CacheInterceptor | 主要負責網絡請求的Response緩存管理 |
ConnectInterceptor | 主要負責打開與服務器的連接,正式進行網絡請求 |
CallServerInterceptor | 主要負責往網絡數據流寫入數據,同時接收服務器返回的數據 |
??在這里,再對整個攔截器鏈做一個小小的總結。
??攔截器鏈的調用是從RealCall
的getResponseWithInterceptorChain
方法開始的,在這個方法里面,會創建一個RealInterceptorChain
對象,然后調用了RealInterceptorChain
的proceed
方法,進而,在RealInterceptorChain
的proceed
方法里面會調用第一個攔截器的intercept
方法。而在攔截器的intercept
方法里面,會再創建一個RealInterceptorChain
對象,然后調用proceed
方法,而此時,在proceed
方法里面,調用就是第二個攔截器的intercept
方法。如下圖:
2. RetryAndFollowUpInterceptor
??在getResponseWithInterceptorChain
方法里面,除了我們自定義的攔截器之外,第一個調用的就是RetryAndFollowUpInterceptor
,所以,我們來看看RetryAndFollowUpInterceptor
究竟為我們做了那些事情。
??RetryAndFollowUpInterceptor
主要是負責網絡請求的失敗重連。
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Call call = realChain.call();
EventListener eventListener = realChain.eventListener();
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;
int followUpCount = 0;
Response priorResponse = null;
while (true) {
//...代碼省略
}
}
??從上面的代碼中,我們可以看到,創建了StreamAllocation
對象,這個類主要負責網絡數據流的管理。在RetryAndFollowUpInterceptor
類里面只是對StreamAllocation
對象進行了初始化,并沒有使用它來進行網絡請求,真正使用它的地方是在ConnectInterceptor
里面。
??其次,就是在while
循環里面,這里就不詳細的展開了,只是對一個進行簡單的看看:
if (++followUpCount > MAX_FOLLOW_UPS) {
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
??這句話非常的簡單,就是如果當前重試的次數超過最大次數,就是結束當前的重連操作,并且拋出異常。
3. BridgeInterceptor
??在前面的總結表中,已經對BridgeInterceptor
做了一個小小的總結。
??BridgeInterceptor
在調用RealInterceptorChain
的 proceed
方法之前,會向RequestBuilder
里面添加很多的信息,包括Content-Type
、Content-Length
等等。這些都是網絡請求的必要信息,這里我們就不需要過多的關注。這個在后面講解ConnectionPool
時,會詳細的解釋
??但是,有一個參數需要的特別注意,那就是Connection
在不為null時,會設置為Keep-Alive
,這個參數表示,一些TCP連接是否保持連接,如果保持連接,就可以達到復用的效果。
??當調用proceed
返回結果時,此時還需要一步操作,那就是如果Response就是經過GZip壓縮的,就需要解壓。如下代碼:
if (transparentGzip
&& "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
&& HttpHeaders.hasBody(networkResponse)) {
GzipSource responseBody = new GzipSource(networkResponse.body().source());
Headers strippedHeaders = networkResponse.headers().newBuilder()
.removeAll("Content-Encoding")
.removeAll("Content-Length")
.build();
responseBuilder.headers(strippedHeaders);
String contentType = networkResponse.header("Content-Type");
responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
}
??當然這個服務端是否返回GZip壓縮的數據,還要取決于在添加Request的響應頭時,是否告訴服務端,客戶端支持Gzip壓縮;如果客戶端支持,那么服務端就會返回Gzip壓縮過的數據。
4. CacheInterceptor
??CacheInterceptor
的職責我們從這個類的名字就可以知道,它主要負責Response的緩存管理,包括哪些請求可以從緩存中取,哪些數據需要重新進行網絡請求;哪些數據可以緩存,哪些數據不能被緩存等等。這些操作都是由CacheInterceptor
來管理的。
(1).緩存原理
??在正式了解CacheInterceptor
之前,我們還是先來看看OkHttp怎么使用Cache
,同時了解OkHttp的緩存原理。
private final OkHttpClient mOkHttpClient = new OkHttpClient.Builder()
.readTimeout(50, TimeUnit.SECONDS)
.cache(new Cache(new File("pby123"),1000))
.build();
??如果我們需要將某些Response
緩存,可以直接在創建OkHttpClient
的對象時,調用cache
方法來進行配置。
??CacheInterceptor
在使用緩存時,是通過調用Cache
的get
方法和put
方法來進行數據的獲取和緩存。我們分別來看看。
(2).put方法
??在Cache
類里面,我們需要重點關注的是put
方法和get
方法,因為CacheInterceptor
是通過調用這個方法來進行數據緩存的相關操作的。我們先來看看put
方法:
@Nullable CacheRequest put(Response response) {
String requestMethod = response.request().method();
if (HttpMethod.invalidatesCache(response.request().method())) {
try {
remove(response.request());
} catch (IOException ignored) {
// The cache cannot be written.
}
return null;
}
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;
}
if (HttpHeaders.hasVaryAll(response)) {
return null;
}
Entry entry = new Entry(response);
DiskLruCache.Editor editor = null;
try {
editor = cache.edit(key(response.request().url()));
if (editor == null) {
return null;
}
entry.writeTo(editor);
return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}
??我將put
方法分為兩個大步驟。
??首先是判斷當前的Response是否支持緩存,比如當前的請求方式是Post,就不支持緩存:
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
支持緩存,那么就可以進行緩存的操作,這就是第二步。首先是,將當前的Response
使用Entry
包裝起來,然后將創建DiskLruCache.Editor
的對象,最后就是使用DiskLruCache.Editor
對象來對數據進行寫入。
??總體上來說,還是比較簡單的。不過在在這里我們發現,OkHttp是DiskLruCache
來實現緩存的操作,這一點需要特別的注意。
(3).get方法
??說了put
方法,get
方法就更加的簡單了。put
方法是緩存數據,那么get
方法就是獲取緩存數據。我們還是簡單的看看:
@Nullable Response get(Request request) {
String key = key(request.url());
DiskLruCache.Snapshot snapshot;
Entry entry;
try {
snapshot = cache.get(key);
if (snapshot == null) {
return null;
}
} catch (IOException e) {
// Give up because the cache cannot be read.
return null;
}
try {
entry = new Entry(snapshot.getSource(ENTRY_METADATA));
} catch (IOException e) {
Util.closeQuietly(snapshot);
return null;
}
Response response = entry.response(snapshot);
if (!entry.matches(request, response)) {
Util.closeQuietly(response.body());
return null;
}
return response;
}
??get
方法總體來說比較,這里就不做過多的解釋了。
??OkHttp的緩存原理也沒有傳說中那么高大上,就是一些普通的操作。在簡單的了解緩存原理之后,我們來看看CacheInterceptor
。
(4). CacheInterceptor
??CacheInterceptor
的intercept
方法比較長,這里就不完全貼出來,這里以分段的形式解釋。
??我覺得,可以將CacheInterceptor
的intercept
過程分為三個步驟。
1.從緩存中獲取數據,如果緩存數據有效的,之前返回,不進行網絡請求。
2.調用RealInterceptorChain
的proceed
方法,進行網絡請求,獲取數據。
3.如果請求的數據支持緩存的話,那么緩存起來。
??我們一個一個的來看看。首先來看獲取緩存數據這部分。
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
if (cache != null) {
cache.trackResponse(strategy);
}
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
// If we're forbidden from using the network and the cache is insufficient, fail.
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}
// If we don't need the network, we're done.
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
??在整個過程中,思路還是比較清晰的。首先是調用Cache
的get
方法獲取相應的緩存數據;然后就是創建一個緩存策略對象,這個緩存策略對象在后面有很大的作用,后面對它單獨的講解,這里就先對它有一個了解吧。
??接下就有四個判斷,需要重點解釋一下。
if (cache != null) {
cache.trackResponse(strategy);
}
??上面代碼表示的意思是,如果當前的Cache
不為空,那么就追蹤一下當前的緩存數據,主要是更新一下,獲取緩存的次數,以及緩存命中率。
synchronized void trackResponse(CacheStrategy cacheStrategy) {
requestCount++;
if (cacheStrategy.networkRequest != null) {
// If this is a conditional request, we'll increment hitCount if/when it hits.
networkCount++;
} else if (cacheStrategy.cacheResponse != null) {
// This response uses the cache and not the network. That's a cache hit.
hitCount++;
}
}
??然后就是第二個判斷。
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
??上面代碼的意思表示,如果從緩存里面獲取的數據不為空,但是緩存策略里面的Response
為空,就表示當前從緩存中獲取數據無效的,所以調用closeQuietly
方法關閉緩存相應的數據。
??我們再來看看第三個判斷。
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}
??如果緩存策略里面的Request
和Response
都為空,表示當前網絡不可用,并且沒有緩存數據,所以就是返回504。
??最后我們來看看判斷。
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
??這里就非常簡單了,如果當前的網絡請求不可用,并且緩存數據不為空,那么就返回緩存的數據。
??第二個過程就是調用RealInterceptorChain
的proceed
方法,這里就不詳細的解釋。我們直接來看看第三個過程。
// If we have a cache response too, then we're doing a conditional get.
if (cacheResponse != null) {
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
??緩存數據的過程也是非常的簡單的。
??首先返回的數據如果是HTTP_NOT_MODIFIED
并且緩存數據不為空,那么更新一下緩存數據,否則的話,就關閉緩存的數據。
??其次,就是返回的數據支持緩存,那么就是調用cacheWritingResponse
方法來緩存。
??如上就是整個RealInterceptorChain
的緩存過程。接下來,我們來分析一下整個過程中貫穿的緩存策略類CacheStrategy
。
(5).CacheStrategy
??我們直接來看CacheStrategy.Factory
的get
方法。看看這個方法到底做了什么。
??不過在看get
方法之前,我們必須得有認知,那就是CacheStrategy
里面的networkRequest
和cacheResponse
到底代表著什么。
- networkRequest:表示一個網絡請求,如果這個對象不為空的話,那么在
CacheStrategy
里面肯定會進行網絡請求,至于最后是選擇緩存數據還是請求回來的數據,得看具體的情況。- cacheResponse:表示緩存的
Response
,如果當前的網絡不可用,也就是networkRequest
為空,那么會直接返回緩存的數據;如果networkRequest
不為空,那么就得跟請求回來的數據比較,具體的比較,可以參考上面的第三個過程。
??現在,我們來正式看看get
方法。
public CacheStrategy get() {
CacheStrategy candidate = getCandidate();
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
// We're forbidden from using the network and the cache is insufficient.
return new CacheStrategy(null, null);
}
return candidate;
}
??其實get
方法也沒有做什么,核心操作還是在getCandidate
里面。由于getCandidate
方法過于長,這里直接貼出代碼,在代碼中解釋。
private CacheStrategy getCandidate() {
// 沒有緩存,返回一個request和一個為null的cacheResponse
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// 如果當前是Https的請求,并且沒有握手
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
//如果當前的cacheResponse不支持緩存
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
CacheControl requestCaching = request.cacheControl();
// 如果當前沒有緩存或者請求有條件
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
CacheControl responseCaching = cacheResponse.cacheControl();
// 如果當前的Response不可以被改變
if (responseCaching.immutable()) {
return new CacheStrategy(null, cacheResponse);
}
// 省略代碼
}
??其實,到最后來看,緩存策略CacheStrategy
并不是那么的可怕,還是比較通俗易懂的。
5. ConnectInterceptor
??現在,我們來看看ConnectInterceptor
這個類。我們先來看看ConnectInterceptor
的proceed
方法:
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();
// We need the network to satisfy this request. Possibly for validating a conditional GET.
boolean doExtensiveHealthChecks = !request.method().equals("GET");
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
??整個過程還是簡單的,不過有幾個地方還是需要我們注意的。
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
??還記得在RetryAndFollowUpInterceptor
里面,在那里創建了StreamAllocation
對象,但是沒有用。當時,我也說了,StreamAllocation
對象真正的使用實在ConnectInterceptor
里面,這也正證實了當時的描述。
??然而這一句有什么用呢?通過調用StreamAllocation
的newStream
方法,返回了一個HttpCodec
對象。這個HttpCodec
對象的作用是對網絡請求進行編碼和對網絡請求回來的數據進行解碼,這些操作都是HttpCodec
類給我們分裝好的。
??然后就是調用StreamAllocation
的connection
方法獲取RealConnection
對象。
??最后就調用RealInterceptorChain
的proceed
方法。這一步是常規操作。
??看到這里來,是不是感覺心里面有一點失落?感覺ConnectInterceptor
在intercept
方法里面并沒有做什么事。
??其實真正的操作并沒有在intercept
方法里面,而是在StreamAllocation
的newStream
方法里面。我們來看看newStream
方法。
public HttpCodec newStream(
OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
int connectTimeout = chain.connectTimeoutMillis();
int readTimeout = chain.readTimeoutMillis();
int writeTimeout = chain.writeTimeoutMillis();
int pingIntervalMillis = client.pingIntervalMillis();
boolean connectionRetryEnabled = client.retryOnConnectionFailure();
try {
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
synchronized (connectionPool) {
codec = resultCodec;
return resultCodec;
}
} catch (IOException e) {
throw new RouteException(e);
}
}
??其實newStream
方法也是非常的簡單,重點還是在findHealthyConnection
方法,我們來看看。
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
boolean doExtensiveHealthChecks) throws IOException {
while (true) {
RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
pingIntervalMillis, connectionRetryEnabled);
// If this is a brand new connection, we can skip the extensive health checks.
synchronized (connectionPool) {
if (candidate.successCount == 0) {
return candidate;
}
}
// Do a (potentially slow) check to confirm that the pooled connection is still good. If it
// isn't, take it out of the pool and start again.
if (!candidate.isHealthy(doExtensiveHealthChecks)) {
noNewStreams();
continue;
}
return candidate;
}
}
?? findHealthyConnection
方法的作用是找到一個健康的連接,在findHealthyConnection
方法里面,我們可以看到,不斷調用findConnection
方法來找到一個連接,然后判斷當前這個連接是否是健康,如果是健康的,那么就返回,否則就重新尋找。
?? 這里的健康是一個宏觀的概念,那什么表示不健康呢?如果一個連接沒有關閉或者相關的流沒有關閉都表示該連接是不健康的。
??我們再來看看findConnection
方法,由于findConnection
方法過長,這里就不全部貼出來了。簡單的解釋一下這個方法整個執行流程。
??findConnection
方法主要分為兩步:
- 找到一個可以使用的
Connection
。- 調用
Connection
的connect
方法進行連接。
??先來看看第一步。
// Attempt to use an already-allocated connection. We need to be careful here because our
// already-allocated connection may have been restricted from creating new streams.
releasedConnection = this.connection;
toClose = releaseIfNoNewStreams();
if (this.connection != null) {
// We had an already-allocated connection and it's good.
result = this.connection;
releasedConnection = null;
}
if (!reportedAcquired) {
// If the connection was never reported acquired, don't report it as released!
releasedConnection = null;
}
if (result == null) {
// Attempt to get a connection from the pool.
Internal.instance.get(connectionPool, address, this, null);
if (connection != null) {
foundPooledConnection = true;
result = connection;
} else {
selectedRoute = route;
}
}
}
??尋找一個可以使用的Connection
過程看上去還是比較簡單的。總體上來說,先嘗試復用當前StreamAllocation
的connection
,如果可以復用的話,直接拿來用;否則的話,就去連接池 connectionPool
里面去取得一個可以使用的連接。
??第二步操作就是調用Connection
的connect
方法。這連接方法里面就涉及到非常多的東西,包括連接方式(socket連接還是隧道連接)等等。這里不進行展開了。
??最后在findConnection
方法里面,我們可以又把獲得的連接放回了連接池中去了:
synchronized (connectionPool) {
reportedAcquired = true;
// Pool the connection.
Internal.instance.put(connectionPool, result);
// If another multiplexed connection to the same address was created concurrently, then
// release this connection and acquire that one.
if (result.isMultiplexed()) {
socket = Internal.instance.deduplicate(connectionPool, address, this);
result = connection;
}
}
6. CallServerInterceptor
??CallServerInterceptor
是OkHttp
中的5大攔截器最后一個攔截器。CallServerInterceptor
攔截器的作用在上面做了一個簡單的介紹,在這里將結合代碼來說明CallServerInterceptor
到底做了那些事情。
??CallServerInterceptor
的執行過程,我們可以分為兩步
- 往網絡請求中寫入數據,包括寫入頭部數據和body數據。
- 接收服務器返回的數據。
??具體的細節這里就不在詳細的解釋了,都是一些常規操作。
7. 總結
??Interceptor
是Okhttp
里面比較核心的東西,同時也是比較復雜的東西,這里對攔截器做一個簡單的總結。
- 在
OKhttp
里面,5大攔截器構成一個鏈,從RealCall
的getResponseWithInterceptorChain
方法開始,5大攔截器開始執行。- 5大攔截是通過
RealInterceptorChain
連接起來的,它們之間調用過程一定熟悉。- 5大攔截器,其中每個攔截的作用都需要熟悉。