文章目錄
- 一 請求與響應流程
- 1.1 請求的封裝
- 1.2 請求的發送
- 1.3 請求的排程
- 二 攔截器
- 2.1 RetryAndFollowUpInterceptor
- 2.2 BridgeInterceptor
- 2.3 CacheInterceptor
- 2.4 ConnectInterceptor
- 2.5 CallServerInterceptor
- 三 連接配接機制
- 3.1 建立連接配接
- 3.2 連接配接池
- 四 緩存機制
- 4.1 緩存政策
- 4.2 緩存管理
更多Android開源架構源碼分析文章請參見 Android open framwork analysis 。
在Android刀耕火種的哪個年代,我們做網絡請求通常會選用HttpURLConnection或者Apache HTTP Client,這兩者均支援HTTPS、流的上傳和下載下傳、配置逾時和連接配接池等特性,但随着業務場景的負責化以及 對流量消耗的優化需求,Okhttp應運而生,自誕生起,口碑就一直很好。
但是,大家都說好,好在哪裡?既然這麼好,它的設計理念和實作思路有哪些值得我們學習的地方?
今天就帶着這些問題,一探究竟。
An HTTP+HTTP/ client for Android and Java applications.
官方網站: https://github.com/square/okhttp
源碼版本:3.9.1
在正式分析源碼之前,我們先來看個簡單的小例子,從例子入手,逐漸分析Okhttp的實作。
:point_right: 舉例
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.build();
Request request = new Request.Builder()
.url(url)
.build();
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
}
});
在上面的例子中,我們建構了一個用戶端OkHttpClient和一個請求Request,然後調用newCall()方法将請求發送了出去。從這個小例子中,我們可以發現 OkHttpClient相當于是個上下文或者說是大管家,它接到我們給的任務以後,将具體的工作分發到各個子系統中去完成。
Okhttp的子系統層級結構圖如下所示:
:point_right: 點選圖檔檢視大圖
- 網絡配置層:利用Builder模式配置各種參數,例如:逾時時間、攔截器等,這些參數都會由Okhttp分發給各個需要的子系統。
- 重定向層:負責重定向。
- Header拼接層:負責把使用者構造的請求轉換為發送給伺服器的請求,把伺服器傳回的響應轉換為對使用者友好的響應。
- HTTP緩存層:負責讀取緩存以及更新緩存。
- 連接配接層:連接配接層是一個比較複雜的層級,它實作了網絡協定、内部的攔截器、安全性認證,連接配接與連接配接池等功能,但這一層還沒有發起真正的連接配接,它隻是做了連接配接器一些參數的處理。
- 資料響應層:負責從伺服器讀取響應的資料。
在整個Okhttp的系統中,我們還要了解以下幾個關鍵角色:
- OkHttpClient:通信的用戶端,用來統一管理發起請求與解析響應。
- Call:Call是一個接口,它是HTTP請求的抽象描述,具體實作類是RealCall,它由CallFactory建立。
- Request:請求,封裝請求的具體資訊,例如:url、header等。
- RequestBody:請求體,用來送出流、表單等請求資訊。
- Response:HTTP請求的響應,擷取響應資訊,例如:響應header等。
- ResponseBody:HTTP請求的響應體,被讀取一次以後就會關閉,是以我們重複調用responseBody.string()擷取請求結果是會報錯的。
- Interceptor:Interceptor是請求攔截器,負責攔截并處理請求,它将網絡請求、緩存、透明壓縮等功能都統一起來,每個功能都是一個Interceptor,所有的Interceptor最 終連接配接成一個Interceptor.Chain。典型的責任鍊模式實作。
- StreamAllocation:用來控制Connections與Streas的資源配置設定與釋放。
- RouteSelector:選擇路線與自動重連。
- RouteDatabase:記錄連接配接失敗的Route黑名單。
我們首先來分析連接配接的請求與響應流程,這樣我們就可以對整個Okhttp系統有一個整體的認識。
一 請求與響應流程
Okhttp的整個請求與響應的流程就是Dispatcher不斷從Request Queue裡取出請求(Call),根據是否已經存存緩存,從記憶體緩存或者伺服器擷取請求的資料,請求分為同步和異步兩種,同步請求通過
調用Call.exectute()方法直接傳回目前請求的Response,異步請求調用Call.enqueue()方法将請求(AsyncCall)添加到請求隊列中去,并通過回調(Callback)擷取伺服器傳回的結果。
一圖勝千言,我們來看一下整個的流程圖,如下所示:
:point_right: 點選圖檔檢視大圖
讀者仔細看一下這個流程圖,是不是很像計算機網絡的OSI七層模型,Okhttp正式采用這種思路,利用攔截器Interceptor将整套架構縱向分層,簡化了設計邏輯,提升了架構擴充性。
通過上面的流程圖,我們可以知道在整個請求與響應流程中,以下幾點是我們需要重點關注的:
- Dispatcher是如何進行請求排程的?
- 各個攔截器是如何實作的?
- 連接配接與連接配接池是如何建立和維護的?
帶着以上問題,我們去源碼中一探究竟。
我們先來看一下具體的函數調用鍊,請求與響應的序列圖如下所示:
:point_right: 點選圖檔檢視大圖
上述序列圖可以幫助我們了解整個請求與響應流程的具體細節,我們首先來看一下一個請求和如何被封裝并發出的。
1.1 請求的封裝
請求是由Okhttp發出,真正的請求都被封裝了在了接口Call的實作類RealCall中,如下所示:
Call接口如下所示:
public interface Call extends Cloneable {
//傳回目前請求
Request request();
//同步請求方法,此方法會阻塞目前線程知道請求結果放回
Response execute() throws IOException;
//異步請求方法,此方法會将請求添加到隊列中,然後等待請求傳回
void enqueue(Callback responseCallback);
//取消請求
void cancel();
//請求是否在執行,當execute()或者enqueue(Callback responseCallback)執行後該方法傳回true
boolean isExecuted();
//請求是否被取消
boolean isCanceled();
//建立一個新的一模一樣的請求
Call clone();
interface Factory {
Call newCall(Request request);
}
}
RealCall的構造方法如下所示:
final class RealCall implements Call {
private RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
//我們建構的OkHttpClient,用來傳遞參數
this.client = client;
this.originalRequest = originalRequest;
//是不是WebSocket請求,WebSocket是用來建立長連接配接的,後面我們會說。
this.forWebSocket = forWebSocket;
//建構RetryAndFollowUpInterceptor攔截器
this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);
}
}
RealCall實作了Call接口,它封裝了請求的調用,這個構造函數的邏輯也很簡單:指派外部傳入的OkHttpClient、Request與forWebSocket,并
建立了重試與重定向攔截器RetryAndFollowUpInterceptor。
1.2 請求的發送
RealCall将請求分為兩種:
- 同步請求
- 異步請求
異步請求隻是比同步請求多了個Callback,分别調用的方法如下所示:
異步請求
final class RealCall implements Call {
@Override public void enqueue(Callback responseCallback) {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
}
同步請求
final class RealCall implements Call {
@Override public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
try {
client.dispatcher().executed(this);
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} finally {
client.dispatcher().finished(this);
}
}
}
從上面實作可以看出,不管是同步請求還是異步請求都是Dispatcher在處理:
- 同步請求:直接執行,并傳回請求結果
- 異步請求:構造一個AsyncCall,并将自己加入處理隊列中。
AsyncCall本質上是一個Runable,Dispatcher會排程ExecutorService來執行這些Runable。
final class AsyncCall extends NamedRunnable {
private final Callback responseCallback;
AsyncCall(Callback responseCallback) {
super("OkHttp %s", redactedUrl());
this.responseCallback = responseCallback;
}
String host() {
return originalRequest.url().host();
}
Request request() {
return originalRequest;
}
RealCall get() {
return RealCall.this;
}
@Override protected void execute() {
boolean signalledCallback = false;
try {
Response response = getResponseWithInterceptorChain();
if (retryAndFollowUpInterceptor.isCanceled()) {
signalledCallback = true;
responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
} else {
signalledCallback = true;
responseCallback.onResponse(RealCall.this, response);
}
} catch (IOException e) {
if (signalledCallback) {
// Do not signal the callback twice!
Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
} else {
responseCallback.onFailure(RealCall.this, e);
}
} finally {
client.dispatcher().finished(this);
}
}
}
從上面代碼可以看出,不管是同步請求還是異步請求最後都會通過getResponseWithInterceptorChain()擷取Response,隻不過異步請求多了個線程排程,異步 執行的過程。
我們先來來看看Dispatcher裡的實作。
1.3 請求的排程
public final class Dispatcher {
private int maxRequests = ;
private int maxRequestsPerHost = ;
/** Ready async calls in the order they'll be run. */
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
/** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
/** Running synchronous calls. Includes canceled calls that haven't finished yet. */
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
/** Used by {@code Call#execute} to signal it is in-flight. */
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);
}
}
}
Dispatcher是一個任務排程器,它内部維護了三個雙端隊列:
- readyAsyncCalls:準備運作的異步請求
- runningAsyncCalls:正在運作的異步請求
- runningSyncCalls:正在運作的同步請求
記得異步請求與同步騎牛,并利用ExecutorService來排程執行AsyncCall。
同步請求就直接把請求添加到正在運作的同步請求隊列runningSyncCalls中,異步請求會做個判斷:
如果正在運作的異步請求不超過64,而且同一個host下的異步請求不得超過5個則将請求添加到正在運作的同步請求隊列中runningAsyncCalls并開始 執行請求,否則就添加到readyAsyncCalls繼續等待。
講完Dispatcher裡的實作,我們繼續來看getResponseWithInterceptorChain()的實作,這個方法才是真正發起請求并處理請求的地方。
1.4 請求的處理
final class RealCall implements Call {
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
//這裡可以看出,我們自定義的Interceptor會被優先執行
interceptors.addAll(client.interceptors());
//添加重試和重定向爛機器
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(
interceptors, null, null, null, , originalRequest);
return chain.proceed(originalRequest);
}
}
短短幾行代碼,完成了對請求的所有處理過程,Interceptor将網絡請求、緩存、透明壓縮等功能統一了起來,它的實作采用責任鍊模式,各司其職, 每個功能都是一個Interceptor,上一級處理完成以後傳遞給下一級,它們最後連接配接成了一個Interceptor.Chain。它們的功能如下:
- RetryAndFollowUpInterceptor:負責重定向。
- BridgeInterceptor:負責把使用者構造的請求轉換為發送給伺服器的請求,把伺服器傳回的響應轉換為對使用者友好的響應。
- CacheInterceptor:負責讀取緩存以及更新緩存。
- ConnectInterceptor:負責與伺服器建立連接配接。
- CallServerInterceptor:負責從伺服器讀取響應的資料。
位置決定功能,位置靠前的先執行,最後一個則複制與伺服器通訊,請求從RetryAndFollowUpInterceptor開始層層傳遞到CallServerInterceptor,每一層 都對請求做相應的處理,處理的結構再從CallServerInterceptor層層傳回給RetryAndFollowUpInterceptor,最紅請求的發起者獲得了伺服器傳回的結果。
以上便是Okhttp整個請求與響應的具體流程,可以發現攔截器才是Okhttp核心功能所在,我們來逐一分析每個攔截器的實作。
二 攔截器
從上面的流程可以看出,各個環節都是由相應的攔截器進行處理,所有的攔截器(包括我們自定義的)都實作了Interceptor接口,如下所示:
public interface Interceptor {
Response intercept(Chain chain) throws IOException;
interface Chain {
Request request();
Response proceed(Request request) throws IOException;
//傳回Request執行後傳回的連接配接
@Nullable Connection connection();
}
}
Okhttp内置的攔截器如下所示:
- RetryAndFollowUpInterceptor:負責失敗重試以及重定向。
- BridgeInterceptor:負責把使用者構造的請求轉換為發送給伺服器的請求,把伺服器傳回的響應轉換為對使用者友好的響應。
- CacheInterceptor:負責讀取緩存以及更新緩存。
- ConnectInterceptor:負責與伺服器建立連接配接。
- CallServerInterceptor:負責從伺服器讀取響應的資料。
我們繼續來看看RealInterceptorChain裡是怎麼一級級處理的。
public final class RealInterceptorChain implements Interceptor.Chain {
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
if (index >= interceptors.size()) throw new AssertionError();
calls++;
// If we already have a stream, confirm that the incoming request will use it.
if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
throw new IllegalStateException("network interceptor " + interceptors.get(index - )
+ " must retain the same host and port");
}
// If we already have a stream, confirm that this is the only call to chain.proceed().
if (this.httpCodec != null && calls > ) {
throw new IllegalStateException("network interceptor " + interceptors.get(index - )
+ " must call proceed() exactly once");
}
// Call the next interceptor in the chain.
RealInterceptorChain next = new RealInterceptorChain(
interceptors, streamAllocation, httpCodec, connection, index + , request);
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);
// Confirm that the next interceptor made its required call to chain.proceed().
if (httpCodec != null && index + < interceptors.size() && next.calls != ) {
throw new IllegalStateException("network interceptor " + interceptor
+ " must call proceed() exactly once");
}
// Confirm that the intercepted response isn't null.
if (response == null) {
throw new NullPointerException("interceptor " + interceptor + " returned null");
}
return response;
}
}
這個方法比較有意思,在調用proceed方法之後,會繼續建構一個新的RealInterceptorChain對象,調用下一個interceptor來繼續請求,直到所有interceptor都處理完畢,将 得到的response傳回。
每個攔截器的方法都遵循這樣的規則:
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
//1 Request階段,該攔截器在Request階段負責做的事情
//2 調用RealInterceptorChain.proceed(),其實是在遞歸調用下一個攔截器的intercept()方法
response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
//3 Response階段,完成了該攔截器在Response階段負責做的事情,然後傳回到上一層的攔截器。
return response;
}
}
從上面的描述可知,Request是按照interpretor的順序正向處理,而Response是逆向處理的。這參考了OSI七層模型的原理。上面我們也提到過。CallServerInterceptor相當于最底層的實體層, 請求從上到逐層包裝下發,響應從下到上再逐層包裝傳回。很漂亮的設計。
interceptor的執行順序:RetryAndFollowUpInterceptor -> BridgeInterceptor -> CacheInterceptor -> ConnectInterceptor -> CallServerInterceptor。
2.1 RetryAndFollowUpInterceptor
RetryAndFollowUpInterceptor負責失敗重試以及重定向。
public final class RetryAndFollowUpInterceptor implements Interceptor {
private static final int MAX_FOLLOW_UPS = ;
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
//1. 建構一個StreamAllocation對象,StreamAllocation相當于是個管理類,維護了
//Connections、Streams和Calls之間的管理,該類初始化一個Socket連接配接對象,擷取輸入/輸出流對象。
streamAllocation = new StreamAllocation(
client.connectionPool(), createAddress(request.url()), callStackTrace);
//重定向次數
int followUpCount = ;
Response priorResponse = null;
while (true) {
if (canceled) {
streamAllocation.release();
throw new IOException("Canceled");
}
Response response = null;
boolean releaseConnection = true;
try {
//2. 繼續執行下一個Interceptor,即BridgeInterceptor
response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
//3. 抛出異常,則檢測連接配接是否還可以繼續。
if (!recover(e.getLastConnectException(), false, request)) {
throw e.getLastConnectException();
}
releaseConnection = false;
continue;
} catch (IOException e) {
// 和服務端建立連接配接失敗
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, requestSendStarted, request)) throw e;
releaseConnection = false;
continue;
} finally {
//檢測到其他未知異常,則釋放連接配接和資源
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}
//建構響應體,這個響應體的body為空。
if (priorResponse != null) {
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder()
.body(null)
.build())
.build();
}
//4。根據響應碼處理請求,傳回Request不為空時則進行重定向處理。
Request followUp = followUpRequest(response);
if (followUp == null) {
if (!forWebSocket) {
streamAllocation.release();
}
return response;
}
closeQuietly(response.body());
//重定向的次數不能超過20次
if (++followUpCount > MAX_FOLLOW_UPS) {
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
if (followUp.body() instanceof UnrepeatableRequestBody) {
streamAllocation.release();
throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
}
if (!sameConnection(response, followUp.url())) {
streamAllocation.release();
streamAllocation = new StreamAllocation(
client.connectionPool(), createAddress(followUp.url()), callStackTrace);
} else if (streamAllocation.codec() != null) {
throw new IllegalStateException("Closing the body of " + response
+ " didn't close its backing stream. Bad interceptor?");
}
request = followUp;
priorResponse = response;
}
}
}
我們先來說說StreamAllocation這個類的作用,這個類協調了三個實體類的關系:
- Connections:連接配接到遠端伺服器的實體套接字,這個套接字連接配接可能比較慢,是以它有一套取消機制。
- Streams:定義了邏輯上的HTTP請求/響應對,每個連接配接都定義了它們可以攜帶的最大并發流,HTTP/1.x每次隻可以攜帶一個,HTTP/2每次可以攜帶多個。
- Calls:定義了流的邏輯序列,這個序列通常是一個初始請求以及它的重定向請求,對于同一個連接配接,我們通常将所有流都放在一個調用中,以此來統一它們的行為。
我們再來看看整個方法的流程:
- 建構一個StreamAllocation對象,StreamAllocation相當于是個管理類,維護了Connections、Streams和Calls之間的管理,該類初始化一個Socket連接配接對象,擷取輸入/輸出流對象。
- 繼續執行下一個Interceptor,即BridgeInterceptor
- 抛出異常,則檢測連接配接是否還可以繼續,以下情況不會重試:
- 用戶端配置出錯不再重試
- 出錯後,request body不能再次發送
- 發生以下Exception也無法恢複連接配接:
- ProtocolException:協定異常
- InterruptedIOException:中斷異常
- SSLHandshakeException:SSL握手異常
- SSLPeerUnverifiedException:SSL握手未授權異常
- 沒有更多線路可以選擇 4。根據響應碼處理請求,傳回Request不為空時則進行重定向處理,重定向的次數不能超過20次。
最後是根據響應碼來處理請求頭,由followUpRequest()方法完成,具體如下所示:
public final class RetryAndFollowUpInterceptor implements Interceptor {
private Request followUpRequest(Response userResponse) throws IOException {
if (userResponse == null) throw new IllegalStateException();
Connection connection = streamAllocation.connection();
Route route = connection != null
? connection.route()
: null;
int responseCode = userResponse.code();
final String method = userResponse.request().method();
switch (responseCode) {
//407,代理認證
case HTTP_PROXY_AUTH:
Proxy selectedProxy = route != null
? route.proxy()
: client.proxy();
if (selectedProxy.type() != Proxy.Type.HTTP) {
throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
}
return client.proxyAuthenticator().authenticate(route, userResponse);
//401,未經認證
case HTTP_UNAUTHORIZED:
return client.authenticator().authenticate(route, userResponse);
//307,308
case HTTP_PERM_REDIRECT:
case HTTP_TEMP_REDIRECT:
// "If the 307 or 308 status code is received in response to a request other than GET
// or HEAD, the user agent MUST NOT automatically redirect the request"
if (!method.equals("GET") && !method.equals("HEAD")) {
return null;
}
// fall-through
//300,301,302,303
case HTTP_MULT_CHOICE:
case HTTP_MOVED_PERM:
case HTTP_MOVED_TEMP:
case HTTP_SEE_OTHER:
//用戶端在配置中是否允許重定向
if (!client.followRedirects()) return null;
String location = userResponse.header("Location");
if (location == null) return null;
HttpUrl url = userResponse.request().url().resolve(location);
// url為null,不允許重定向
if (url == null) return null;
//查詢是否存在http與https之間的重定向
boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
if (!sameScheme && !client.followSslRedirects()) return null;
// Most redirects don't include a request body.
Request.Builder requestBuilder = userResponse.request().newBuilder();
if (HttpMethod.permitsRequestBody(method)) {
final boolean maintainBody = HttpMethod.redirectsWithBody(method);
if (HttpMethod.redirectsToGet(method)) {
requestBuilder.method("GET", null);
} else {
RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
requestBuilder.method(method, requestBody);
}
if (!maintainBody) {
requestBuilder.removeHeader("Transfer-Encoding");
requestBuilder.removeHeader("Content-Length");
requestBuilder.removeHeader("Content-Type");
}
}
// When redirecting across hosts, drop all authentication headers. This
// is potentially annoying to the application layer since they have no
// way to retain them.
if (!sameConnection(userResponse, url)) {
requestBuilder.removeHeader("Authorization");
}
return requestBuilder.url(url).build();
//408,逾時
case HTTP_CLIENT_TIMEOUT:
// 408's are rare in practice, but some servers like HAProxy use this response code. The
// spec says that we may repeat the request without modifications. Modern browsers also
// repeat the request (even non-idempotent ones.)
if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
return null;
}
return userResponse.request();
default:
return null;
}
}
}
重定向會涉及到一些網絡程式設計的知識,這裡如果沒有完成了解,你隻要知道RetryAndFollowUpInterceptor的作用就是處理了一些連接配接異常以及重定向就可以了。我們接着來看看下一個BridgeInterceptor。
2.2 BridgeInterceptor
BridgeInterceptor就跟它的名字那樣,它是一個連接配接橋,它負責把使用者構造的請求轉換為發送給伺服器的請求,把伺服器傳回的響應轉換為對使用者友好的響應。
轉換的過程就是添加一些服務端需要的header資訊。
public final class BridgeInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
Request.Builder requestBuilder = userRequest.newBuilder();
RequestBody body = userRequest.body();
if (body != null) {
//1 進行Header的包裝
MediaType contentType = body.contentType();
if (contentType != null) {
requestBuilder.header("Content-Type", contentType.toString());
}
long contentLength = body.contentLength();
if (contentLength != -) {
requestBuilder.header("Content-Length", Long.toString(contentLength));
requestBuilder.removeHeader("Transfer-Encoding");
} else {
requestBuilder.header("Transfer-Encoding", "chunked");
requestBuilder.removeHeader("Content-Length");
}
}
if (userRequest.header("Host") == null) {
requestBuilder.header("Host", hostHeader(userRequest.url(), false));
}
if (userRequest.header("Connection") == null) {
requestBuilder.header("Connection", "Keep-Alive");
}
//這裡有個坑:如果你在請求的時候主動添加了"Accept-Encoding: gzip" ,transparentGzip=false,那你就要自己解壓,如果
// 你沒有吹解壓,或導緻response.string()亂碼。
// If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
// the transfer stream.
boolean transparentGzip = false;
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
transparentGzip = true;
requestBuilder.header("Accept-Encoding", "gzip");
}
//建立OkhttpClient配置的cookieJar
List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
if (!cookies.isEmpty()) {
requestBuilder.header("Cookie", cookieHeader(cookies));
}
if (userRequest.header("User-Agent") == null) {
requestBuilder.header("User-Agent", Version.userAgent());
}
Response networkResponse = chain.proceed(requestBuilder.build());
//解析伺服器傳回的Header,如果沒有這事cookie,則不進行解析
HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
Response.Builder responseBuilder = networkResponse.newBuilder()
.request(userRequest);
//判斷伺服器是否支援gzip壓縮,如果支援,則将壓縮送出給Okio庫來處理
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);
responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));
}
return responseBuilder.build();
}
}
就跟它的名字描述的那樣,它是一個橋梁,負責把使用者構造的請求轉換為發送給伺服器的請求,把伺服器傳回的響應轉換為對使用者友好的響應。 在Request階段配置使用者資訊,并添加一些請求頭。在Response階段,進行gzip解壓。
這個方法主要是針對Header做了一些處理,這裡主要提一下"Accept-Encoding", "gzip",關于它有以下幾點需要注意:
- 開發者沒有添加Accept-Encoding時,自動添加Accept-Encoding: gzip
- 自動添加Accept-Encoding,會對request,response進行自動解壓
- 手動添加Accept-Encoding,不負責解壓縮
- 自動解壓時移除Content-Length,是以上層Java代碼想要contentLength時為-1
- 自動解壓時移除 Content-Encoding
- 自動解壓時,如果是分塊傳輸編碼,Transfer-Encoding: chunked不受影響。
BridgeInterceptor主要就是針對Header做了一些處理,我們接着來看CacheInterceptor。
2.3 CacheInterceptor
我們知道為了節省流量和提高響應速度,Okhttp是有自己的一套緩存機制的,CacheInterceptor就是用來負責讀取緩存以及更新緩存的。
public final class CacheInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
//1. 讀取候選緩存,具體如何讀取的我們下面會講。
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
//2. 建立緩存政策,強制緩存、對比緩存等,關于緩存政策我們下面也會講。
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());
}
//3. 根據政策,不使用網絡,又沒有緩存的直接報錯,并傳回錯誤碼504。
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code()
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}
//4. 根據政策,不使用網絡,有緩存的直接傳回。
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
Response networkResponse = null;
try {
//5. 前面兩個都沒有傳回,繼續執行下一個Interceptor,即ConnectInterceptor。
networkResponse = chain.proceed(networkRequest);
} finally {
//如果發生IO異常,則釋放掉緩存
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
//6. 接收到網絡結果,如果響應code式304,則使用緩存,傳回緩存結果。
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();
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
//7. 讀取網絡結果。
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
//8. 對資料進行緩存。
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.
}
}
}
//9. 傳回網絡讀取的結果。
return response;
}
}
整個方法的流程如下所示:
- 讀取候選緩存,具體如何讀取的我們下面會講。
- 建立緩存政策,強制緩存、對比緩存等,關于緩存政策我們下面也會講。
- 根據政策,不使用網絡,又沒有緩存的直接報錯,并傳回錯誤碼504。
- 根據政策,不使用網絡,有緩存的直接傳回。
- 前面兩個都沒有傳回,繼續執行下一個Interceptor,即ConnectInterceptor。
- 接收到網絡結果,如果響應code式304,則使用緩存,傳回緩存結果。
- 讀取網絡結果。
- 對資料進行緩存。
- 傳回網絡讀取的結果。
我們再接着來看ConnectInterceptor。
2.4 ConnectInterceptor
在RetryAndFollowUpInterceptor裡初始化了一個StreamAllocation對象,我們說在這個StreamAllocation對象裡初始化了一個Socket對象用來做連接配接,但是并沒有 真正的連接配接,等到處理完hader和緩存資訊之後,才調用ConnectInterceptor來進行真正的連接配接
public final class ConnectInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();
boolean doExtensiveHealthChecks = !request.method().equals("GET");
//建立輸出流
HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
//建立連接配接
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
}
ConnectInterceptor在Request階段建立連接配接,處理方式也很簡單,建立了兩個對象:
- HttpCodec:用來編碼HTTP requests和解碼HTTP responses
- RealConnection:連接配接對象,負責發起與伺服器的連接配接。
這裡事實上包含了連接配接、連接配接池等一整套的Okhttp的連接配接機制,我們放在下面單獨講,先來繼續看最後一個Interceptor:CallServerInterceptor。
2.5 CallServerInterceptor
CallServerInterceptor負責從伺服器讀取響應的資料。
public final class CallServerInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
//這些對象在前面的Interceptor都已經建立完畢
RealInterceptorChain realChain = (RealInterceptorChain) chain;
HttpCodec httpCodec = realChain.httpStream();
StreamAllocation streamAllocation = realChain.streamAllocation();
RealConnection connection = (RealConnection) realChain.connection();
Request request = realChain.request();
long sentRequestMillis = System.currentTimeMillis();
//1. 寫入請求頭
httpCodec.writeRequestHeaders(request);
Response.Builder responseBuilder = null;
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
// If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
// Continue" response before transmitting the request body. If we don't get that, return what
// we did get (such as a 4xx response) without ever transmitting the request body.
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
httpCodec.flushRequest();
responseBuilder = httpCodec.readResponseHeaders(true);
}
//2 寫入請求體
if (responseBuilder == null) {
// Write the request body if the "Expect: 100-continue" expectation was met.
Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
} else if (!connection.isMultiplexed()) {
// If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection from
// being reused. Otherwise we're still obligated to transmit the request body to leave the
// connection in a consistent state.
streamAllocation.noNewStreams();
}
}
httpCodec.finishRequest();
//3 讀取響應頭
if (responseBuilder == null) {
responseBuilder = httpCodec.readResponseHeaders(false);
}
Response response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
//4 讀取響應體
int code = response.code();
if (forWebSocket && code == ) {
// Connection is upgrading, but we need to ensure interceptors see a non-null response body.
response = response.newBuilder()
.body(Util.EMPTY_RESPONSE)
.build();
} else {
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))
.build();
}
if ("close".equalsIgnoreCase(response.request().header("Connection"))
|| "close".equalsIgnoreCase(response.header("Connection"))) {
streamAllocation.noNewStreams();
}
if ((code == || code == ) && response.body().contentLength() > ) {
throw new ProtocolException(
"HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
}
return response;
}
}
我們通過ConnectInterceptor已經連接配接到伺服器了,接下來我們就是寫入請求資料以及讀出傳回資料了。整個流程:
- 寫入請求頭
- 寫入請求體
- 讀取響應頭
- 讀取響應體
這篇文章就到這裡,後續的文章我們會來分析Okhttp的緩存機制、連接配接機制、編輯嗎機制等實作。
三 連接配接機制
連接配接的建立是在StreamAllocation對象統籌下完成的,我們前面也說過它早在RetryAndFollowUpInterceptor就被建立了,StreamAllocation對象 主要用來管理兩個關鍵角色:
- RealConnection:真正建立連接配接的對象,利用Socket建立連接配接。
- ConnectionPool:連接配接池,用來管理和複用連接配接。
在裡初始化了一個StreamAllocation對象,我們說在這個StreamAllocation對象裡初始化了一個Socket對象用來做連接配接,但是并沒有
3.1 建立連接配接
我們在前面的ConnectInterceptor分析中已經說過,onnectInterceptor用來完成連接配接。而真正的連接配接在RealConnect中實作,連接配接由連接配接池ConnectPool來管理,連接配接池最多保 持5個位址的連接配接keep-alive,每個keep-alive時長為5分鐘,并有異步線程清理無效的連接配接。
主要由以下兩個方法完成:
- HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
- RealConnection connection = streamAllocation.connection();
我們來具體的看一看。
StreamAllocation.newStream()最終調動findConnect()方法來建立連接配接。
public final class StreamAllocation {
/**
* Returns a connection to host a new stream. This prefers the existing connection if it exists,
* then the pool, finally building a new connection.
*/
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
boolean connectionRetryEnabled) throws IOException {
Route selectedRoute;
synchronized (connectionPool) {
if (released) throw new IllegalStateException("released");
if (codec != null) throw new IllegalStateException("codec != null");
if (canceled) throw new IOException("Canceled");
//1 檢視是否有完好的連接配接
RealConnection allocatedConnection = this.connection;
if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
return allocatedConnection;
}
//2 連接配接池中是否用可用的連接配接,有則使用
Internal.instance.get(connectionPool, address, this, null);
if (connection != null) {
return connection;
}
selectedRoute = route;
}
//線程的選擇,多IP操作
if (selectedRoute == null) {
selectedRoute = routeSelector.next();
}
//3 如果沒有可用連接配接,則自己建立一個
RealConnection result;
synchronized (connectionPool) {
if (canceled) throw new IOException("Canceled");
// Now that we have an IP address, make another attempt at getting a connection from the pool.
// This could match due to connection coalescing.
Internal.instance.get(connectionPool, address, this, selectedRoute);
if (connection != null) {
route = selectedRoute;
return connection;
}
// Create a connection and assign it to this allocation immediately. This makes it possible
// for an asynchronous cancel() to interrupt the handshake we're about to do.
route = selectedRoute;
refusedStreamCount = ;
result = new RealConnection(connectionPool, selectedRoute);
acquire(result);
}
//4 開始TCP以及TLS握手操作
result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);
routeDatabase().connected(result.route());
//5 将新建立的連接配接,放在連接配接池中
Socket socket = null;
synchronized (connectionPool) {
// 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;
}
}
closeQuietly(socket);
return result;
}
}
整個流程如下:
- 查找是否有完整的連接配接可用:
- Socket沒有關閉
- 輸入流沒有關閉
- 輸出流沒有關閉
- Http2連接配接沒有關閉
- 連接配接池中是否有可用的連接配接,如果有則可用。
- 如果沒有可用連接配接,則自己建立一個。
- 開始TCP連接配接以及TLS握手操作。
- 将新建立的連接配接加入連接配接池。
上述方法完成後會建立一個RealConnection對象,然後調用該方法的connect()方法建立連接配接,我們再來看看RealConnection.connect()方法的實作。
public final class RealConnection extends Http2Connection.Listener implements Connection {
public void connect(
int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled) {
if (protocol != null) throw new IllegalStateException("already connected");
//線路選擇
RouteException routeException = null;
List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
if (route.address().sslSocketFactory() == null) {
if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
throw new RouteException(new UnknownServiceException(
"CLEARTEXT communication not enabled for client"));
}
String host = route.address().url().host();
if (!Platform.get().isCleartextTrafficPermitted(host)) {
throw new RouteException(new UnknownServiceException(
"CLEARTEXT communication to " + host + " not permitted by network security policy"));
}
}
//開始連接配接
while (true) {
try {
//如果是通道模式,則建立通道連接配接
if (route.requiresTunnel()) {
connectTunnel(connectTimeout, readTimeout, writeTimeout);
}
//否則進行Socket連接配接,一般都是屬于這種情況
else {
connectSocket(connectTimeout, readTimeout);
}
//建立https連接配接
establishProtocol(connectionSpecSelector);
break;
} catch (IOException e) {
closeQuietly(socket);
closeQuietly(rawSocket);
socket = null;
rawSocket = null;
source = null;
sink = null;
handshake = null;
protocol = null;
http2Connection = null;
if (routeException == null) {
routeException = new RouteException(e);
} else {
routeException.addConnectException(e);
}
if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
throw routeException;
}
}
}
if (http2Connection != null) {
synchronized (connectionPool) {
allocationLimit = http2Connection.maxConcurrentStreams();
}
}
}
/** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
private void connectSocket(int connectTimeout, int readTimeout) throws IOException {
Proxy proxy = route.proxy();
Address address = route.address();
//根據代理類型的不同處理Socket
rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
? address.socketFactory().createSocket()
: new Socket(proxy);
rawSocket.setSoTimeout(readTimeout);
try {
//建立Socket連接配接
Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
} catch (ConnectException e) {
ConnectException ce = new ConnectException("Failed to connect to " + route.socketAddress());
ce.initCause(e);
throw ce;
}
// The following try/catch block is a pseudo hacky way to get around a crash on Android 7.0
// More details:
// https://github.com/square/okhttp/issues/3245
// https://android-review.googlesource.com/#/c/271775/
try {
//擷取輸入/輸出流
source = Okio.buffer(Okio.source(rawSocket));
sink = Okio.buffer(Okio.sink(rawSocket));
} catch (NullPointerException npe) {
if (NPE_THROW_WITH_NULL.equals(npe.getMessage())) {
throw new IOException(npe);
}
}
}
}
最終調用Java裡的套接字Socket裡的connect()方法。
3.2 連接配接池
我們知道在負責的網絡環境下,頻繁的進行建立Sokcet連接配接(TCP三向交握)和斷開Socket(TCP四次分手)是非常消耗網絡資源和浪費時間的,HTTP中的keepalive連接配接對于 降低延遲和提升速度有非常重要的作用。
複用連接配接就需要對連接配接進行管理,這裡就引入了連接配接池的概念。
Okhttp支援5個并發KeepAlive,預設鍊路生命為5分鐘(鍊路空閑後,保持存活的時間),連接配接池有ConectionPool實作,對連接配接進行回收和管理。
ConectionPool在内部維護了一個線程池,來清理連接配接,如下所示:
public final class ConnectionPool {
private static final Executor executor = new ThreadPoolExecutor( /* corePoolSize */,
Integer.MAX_VALUE /* maximumPoolSize */, /* keepAliveTime */, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));
//清理連接配接,線上程池executor裡調用。
private final Runnable cleanupRunnable = new Runnable() {
@Override public void run() {
while (true) {
//執行清理,并傳回下次需要清理的時間。
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -) return;
if (waitNanos > ) {
long waitMillis = waitNanos / ;
waitNanos -= (waitMillis * );
synchronized (ConnectionPool.this) {
try {
//在timeout時間内釋放鎖
ConnectionPool.this.wait(waitMillis, (int) waitNanos);
} catch (InterruptedException ignored) {
}
}
}
}
}
};
}
`
ConectionPool在内部維護了一個線程池,來清理連,清理任務由cleanup()方法完成,它是一個阻塞操作,首先執行清理,并傳回下次需要清理的間隔時間,調用調用wait()
方法釋放鎖。等時間到了以後,再次進行清理,并傳回下一次需要清理的時間,循環往複。
我們來看一看cleanup()方法的具體實作。
undefined
整個方法的流程如下所示:
- 查詢此連接配接内部的StreanAllocation的引用數量。
- 标記空閑連接配接。
- 如果空閑連接配接超過5個或者keepalive時間大于5分鐘,則将該連接配接清理掉。
- 傳回此連接配接的到期時間,供下次進行清理。
- 全部都是活躍連接配接,5分鐘時候再進行清理。
- 沒有任何連接配接,跳出循環。
- 關閉連接配接,傳回時間0,立即再次進行清理。
在RealConnection裡有個StreamAllocation虛引用清單,每建立一個StreamAllocation,就會把它添加進該清單中,如果留關閉以後就将StreamAllocation 對象從該清單中移除,正是利用利用這種引用計數的方式判定一個連接配接是否為空閑連接配接,
undefined
查找引用計數由pruneAndGetAllocationCount()方法實作,具體實作如下所示:
undefined
四 緩存機制
4.1 緩存政策
在分析Okhttp的緩存機制之前,我們先來回顧一下HTTP與緩存相關的理論知識,這是實作Okhttp機制的基礎。
HTTP的緩存機制也是依賴于請求和響應header裡的參數類實作的,最終響應式從緩存中去,還是從服務端重新拉取,HTTP的緩存機制的流程如下所示:
:point_right: 點選圖檔檢視大圖
HTTP的緩存可以分為兩種:
- 強制緩存:需要服務端參與判斷是否繼續使用緩存,當用戶端第一次請求資料是,服務端傳回了緩存的過期時間(Expires與Cache-Control),沒有過期就可以繼續使用緩存,否則則不适用,無需再向服務端詢問。
- 對比緩存:需要服務端參與判斷是否繼續使用緩存,當用戶端第一次請求資料時,服務端會将緩存辨別(Last-Modified/If-Modified-Since與Etag/If-None-Match)與資料一起傳回給用戶端,用戶端将兩者都備份到緩存中 ,再次請求資料時,用戶端将上次備份的緩存 辨別發送給服務端,服務端根據緩存辨別進行判斷,如果傳回304,則表示通知用戶端可以繼續使用緩存。
強制緩存優先于對比緩存。
上面提到強制緩存使用的的兩個辨別:
- Expires:Expires的值為服務端傳回的到期時間,即下一次請求時,請求時間小于服務端傳回的到期時間,直接使用緩存資料。到期時間是服務端生成的,用戶端和服務端的時間可能有誤差。
- Cache-Control:Expires有個時間校驗的問題,所有HTTP1.1采用Cache-Control替代Expires。
Cache-Control的取值有以下幾種:
- private: 用戶端可以緩存。
- public: 用戶端和代理伺服器都可緩存。
- max-age=xxx: 緩存的内容将在 xxx 秒後失效
- no-cache: 需要使用對比緩存來驗證緩存資料。
- no-store: 所有内容都不會緩存,強制緩存,對比緩存都不會觸發。
我們再來看看對比緩存的兩個辨別:
Last-Modified/If-Modified-Since
Last-Modified 表示資源上次修改的時間。
當用戶端發送第一次請求時,服務端傳回資源上次修改的時間:
undefined 用戶端再次發送,會在header裡攜帶If-Modified-Since。将上次服務端傳回的資源時間上傳給服務端。
undefined 服務端接收到用戶端發來的資源修改時間,與自己目前的資源修改時間進行對比,如果自己的資源修改時間大于用戶端發來的資源修改時間,則說明資源做過修改, 則傳回200表示需要重新請求資源,否則傳回304表示資源沒有被修改,可以繼續使用緩存。
上面是一種時間戳标記資源是否修改的方法,還有一種資源辨別碼ETag的方式來标記是否修改,如果辨別碼發生改變,則說明資源已經被修改,ETag優先級高于Last-Modified。
Etag/If-None-Match
ETag是資源檔案的一種辨別碼,當用戶端發送第一次請求時,服務端會傳回目前資源的辨別碼:
undefined 用戶端再次發送,會在header裡攜帶上次服務端傳回的資源辨別碼:
undefined 服務端接收到用戶端發來的資源辨別碼,則會與自己目前的資源嗎進行比較,如果不同,則說明資源已經被修改,則傳回200,如果相同則說明資源沒有被修改,傳回 304,用戶端可以繼續使用緩存。
以上便是HTTP緩存政策的相關理論知識,我們來看看具體實作。
Okhttp的緩存政策就是根據上述流程圖實作的,具體的實作類是CacheStrategy,CacheStrategy的構造函數裡有兩個參數:
undefined 這兩個參數參數的含義如下:
- networkRequest:網絡請求。
- cacheResponse:緩存響應,基于DiskLruCache實作的檔案緩存,可以是請求中url的md5,value是檔案中查詢到的緩存,這個我們下面會說。
CacheStrategy就是利用這兩個參數生成最終的政策,有點像map操作,将networkRequest與cacheResponse這兩個值輸入,處理之後再将這兩個值輸出,們的組合結果如下所示:
- 如果networkRequest為null,cacheResponse為null:only-if-cached(表明不進行網絡請求,且緩存不存在或者過期,一定會傳回503錯誤)。
- 如果networkRequest為null,cacheResponse為non-null:不進行網絡請求,而且緩存可以使用,直接傳回緩存,不用請求網絡。
- 如果networkRequest為non-null,cacheResponse為null:需要進行網絡請求,而且緩存不存在或者過期,直接通路網絡。
- 如果networkRequest為non-null,cacheResponse為non-null:Header中含有ETag/Last-Modified标簽,需要在條件請求下使用,還是需要通路網絡。
那麼這四種情況是如何判定的,我們來看一下。
CacheStrategy是利用Factory模式進行構造的,CacheStrategy.Factory對象建構以後,調用它的get()方法即可獲得具體的CacheStrategy,CacheStrategy.Factory.get()方法内部
調用的是CacheStrategy.Factory.getCandidate()方法,它是核心的實作。
如下所示:
undefined
整個函數的邏輯就是按照上面那個HTTP緩存判定流程圖來實作,具體流程如下所示:
- 如果緩存沒有命中,就直接進行網絡請求。
- 如果TLS握手資訊丢失,則傳回直接進行連接配接。
- 根據response狀态碼,Expired時間和是否有no-cache标簽就行判斷是否進行直接通路。
- 如果請求header裡有"no-cache"或者右條件GET請求(header裡帶有ETag/Since标簽),則直接連接配接。
- 如果緩存在過期時間内則可以直接使用,則直接傳回上次緩存。
- 如果緩存過期,且有ETag等資訊,則發送If-None-Match、If-Modified-Since、If-Modified-Since等條件請求交給服務端判斷處理
整個流程就是這樣,另外說一點,Okhttp的緩存是根據伺服器header自動的完成的,整個流程也是根據RFC文檔寫死的,用戶端不必要進行手動控制。
了解了緩存政策,我們來看看緩存在磁盤上是如何被管理的。
4.2 緩存管理
這篇文章我們來分析Okhttp的緩存機制,緩存機制是基于DiskLruCache做的。Cache類封裝了緩存的實作,實作了InternalCache接口。
InternalCache接口如下所示:
InternalCache
undefined 我們接着來看看它的實作類。
Cache沒有直接實作InternalCache這個接口,而是在其内部實作了InternalCache的匿名内部類,内部類的方法調用Cache對應的方法,如下所示:
undefined ` 在Cache類裡還定義一些内部類,這些類封裝了請求與響應資訊。
- Cache.Entry:封裝了請求與響應等資訊,包括url、varyHeaders、protocol、code、message、responseHeaders、handshake、sentRequestMillis與receivedResponseMillis。
- Cache.CacheResponseBody:繼承于ResponseBody,封裝了緩存快照snapshot,響應體bodySource,内容類型contentType,内容長度contentLength。
除了兩個類以外,Okhttp還封裝了一個檔案系統類FileSystem類,這個類利用Okio這個庫對Java的FIle操作進行了一層封裝,簡化了IO操作。了解了這些剩下的就是DiskLruCahe裡的插入緩存 、擷取緩存和删除緩存的操作。
關于這一部分的内容,可以參考我們之前寫的内容 07Android開源架構源碼分析:LruCache與DiskLruCache 。
好了,到這裡關于Okhttp的全部内容就都講完了,可以說Okhttp是設計非常優良的一個庫,有很多值得我們學習的地方,下一篇我們來分析它的好搭檔Retrofit。
來自:http://www.open-open.com/lib/view/open1517362688550.html
Good luck!
Reprinted by Jimmy.li