OkHttp3 使用詳解及網路連線快取的處理機制
1. 建立 OkHttpClient 物件
可以直接新建,也可以用建造者模式建造出來。直接新建時,其實也是使用建造者設定了預設的請求引數。
OkHttpClient client = new OkHttpClient();
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.writeTimeout(1000, TimeUnit.SECONDS)
.readTimeout(1000, TimeUnit.SECONDS)
.build();
功能 | 函式 | 註釋 |
---|---|---|
新增應用攔截器 | addInterceptor(Interceptor) | 最接近應用的攔截器 |
新增網路攔截器 | addNetworkInterceptor(Interceptor) | 接近網路的攔截器 |
設定快取物件 | cache(Cache cache) | 使用 DiskLruCache 實現 |
okhttp3.Cache
構造時只需要指定快取目錄和大小即可。
網路攔截器例項:給返回的響應新增快取過期時間
private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Response originResponse = chain.proceed(chain.request());
// 5 分鐘後過期
CacheControl.Builder builder = new CacheControl.Builder()
.maxAge(5, TimeUnit.MINUTES);
return originResponse.newBuilder()
.header("Cache-Control", builder.build().toString())
.build();
}
};
2. 準備好 Request 物件
通常用建造者模式來建造。
new Request.Builder().build();
功能 | 函式 | 註釋 |
---|---|---|
新增 URL | url(String url) | |
GET 請求 | get() | |
POST 方式提交表單 | post(RequestBody body) | |
替換請求頭 | header(String name, String value) | 先刪除 name 對應的原有的欄位對,再新增 |
新增請求頭 | addHeader(String name, String value) | 直接往 ArrayList 中新增兩個 String |
表單的構建
new FormBody.Builder().build()
功能 | 函式 | 註釋 |
---|---|---|
新增表單項 | add(String name, String value) | 沒有 encode 的字串 |
新增表單項 | addEncoded(String name, String value) | 已經 encode 的字串 |
3. 傳送請求
OkHttpClient 例項先用 newCall(Request) 方法對請求再一次包裝,返回 Call 物件,表示請求已經準備好。然後有同步和非同步兩種方式來發送請求:
同步方式:
Response response = client.newCall(request).execute();
非同步方式:
Response response = client.newCall(request).enqueue(callback);
非同步方式會用一個 CachedThreadPool 執行緒池來管理非同步任務,任務一提交就馬上在非核心執行緒中執行,非核心執行緒的空閒存活期是 60s。
- enqueue 方法會傳入一個 okhttp3.Callback 物件,需要注意的是回撥時還是在子執行緒。
new Callback() {
@Override
public void onFailure(Call call, IOException e) {
// 通過runOnUiThread()方法回到主執行緒處理邏輯
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
closeProgressDialog();
Toast.makeText(getContext(), "載入失敗", Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void onResponse(Call call, Response response) throws IOException {
}
}
- enqueue 方法中,callback 被包裝成實現了 Runnable 介面的 AsyncCall 物件,傳入 OkHttpClient 的 Dispatcher 。Dispatcher 會使用執行緒池執行 AsyncCall 的 execute 方法,最終的執行結果回撥 Callback。
@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);
}
}
4. 攔截器
不論是 execute 方法還是 enqueue 方法傳送連結請求,最終都會呼叫 getResponseWithInterceptorChain
方法。
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
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, 0,
originalRequest, this, eventListener,
client.connectTimeoutMillis(),
client.readTimeoutMillis(),
client.writeTimeoutMillis());
return chain.proceed(originalRequest);
}
在請求往外發送時一次經過7個攔截器,第2~5個可以認為是 OkHttpCore 核心。
序號 | 名稱 | 註釋 |
---|---|---|
1 | 應用攔截器 interceptors |
由使用者在建立 OkHttpClient 時設定 |
2 | 重試和重定向攔截器 RetryAndFollowUpInterceptor |
1.網路請求出現異常時,如果滿足重試條件就傳送重試請求; 2.如果網路響應包含重定向資訊,就建立重定向請求併發送 |
3 | 橋接攔截器 BridgeInterceptor |
1.深加工使用者傳遞來的請求,設定預設請求頭; 2. 使用者沒有設定時預設採用 gzip 壓縮解壓資料 |
4 | 快取攔截器 CacheInterceptor |
快取的查詢和儲存 |
5 | 連線攔截器 ConnectInterceptor |
給網路請求提供一個連線,之後攔截器中 chain.connection() 才不為 null |
6 | 網路攔截器 networkInterceptors |
由使用者在建立 OkHttpClient 時設定 |
7 | 呼叫伺服器攔截器 CallServerInterceptor |
網路請求最終從這裡傳送出去,幷包裝響應物件 |
5. CacheInterceptor 詳解
網路請求中與快取有關的頭域:
序號 | 名稱 | 註釋 |
---|---|---|
1 | Cache-Control:max-age | 快取的推薦保質期 |
2 | Cache-Control:max-stale | 快取超過保質期多久仍可接受 |
3 | Cache-Control:min-fresh | 快取距離推薦保質期還有多久就認為快取不新鮮了 |
4 | Cache-Control:immutable | 快取永不過期 |
5 | Cache-Control:only-if-cached | 只允許使用當前快取資料,沒有快取就只能返回 504 響應 |
6 | Cache-Control:No-Cache | 不希望使用快取 |
7 | Age | 響應物件在代理快取中存在的時間,以秒為單位 |
8 | Date | 當前的GMT時間 |
9 | ETag | 對於某個資源的某個特定版本的一個識別符號 |
10 | Expires | 超過該時間則認為此迴應已經過期 |
11 | Last-Modified | 伺服器資源的最後修改時間 |
相關響應頭:
序號 | 名稱 | 註釋 |
---|---|---|
1 | 200 OK | 合適的資源作為響應體傳回客戶端 |
2 | 301 Moved Permanently | 所請求的 URL 資源路徑已經改變, 新的 URL 在響應的 Location 頭字 段 |
3 | 304 Not Modified | 所請求的內容距離上次訪問並沒有 變化,瀏覽器快取有效 |
4 | 404 Not Found | 伺服器找不到所請求的資源 |
5 | 504 Gateway TImeout | 伺服器作為閘道器不能從上游伺服器 得到響應返回給客戶端 |
下面將介紹快取有效性相關的 3 個概念:
- 新鮮度檢測
- 在驗證
- 在驗證命中
在使用快取將 url 響應資源的副本儲存在本地時,當我們再次對該 url 資源發起請求時,可以快速從本地儲存中獲取該 url 資源,而不需要重新發起網路連線。
但是,伺服器的 url 資源可能在一定時間後被修改,因此我們不能一直使用快取資源,在一定時間之前,認為可以使用快取資源,在這個時間之後認為不能再使用快取資源,需要重新請求網路資源。這個條件的判斷就是 新鮮度檢測。就像我們在吃食品之前要看看它有沒有過期。
url 資源快取超過一定時間,我們也不會直接重新請求 url 資源,而是去伺服器檢視資源是否已經發生改變,這就叫 再驗證。
如果伺服器發現 url 資源沒有改變,就返回 304 Not Modified,並不在返回對應實體,這就叫 再驗證命中,此時我們可以繼續使用 url 資源快取。如果發生了變化,則返回 200 OK,並將改變後的 url 資源返回。
具體實現:
新鮮度檢測
http1.1 規範中,通過響應首部的 Cache-Control:max-age 和 Last-Modified 計算出絕對時間。http1.0 規範中,響應首部 Expires 的值就是絕對時間。
再驗證
超過絕對時間後要進行再驗證,需要根據響應首部具體的規範來進行 條件請求,通常有 5 種條件請求首部。
序號 | 名稱 | 註釋 |
---|---|---|
1 | If-Modified-Since | 和響應首部 Last-Modified 配合使用,詢問 伺服器 url 資源的最後修改時間是否變化, 沒有的話就返回 304 未修改 |
2 | If-None-Match | 和響應首部 Etag 配合使用,Etag 相當於伺服器 對 url 資源定義的粗粒度的版本號,即使資源修 改了,但如果版本號沒有變化,仍然認為再驗證 命中。 |
3 | If-Unmodified-Since | |
4 | If-Range | |
5 | If-Match |
在 CacheInterceptor 原始碼中,首先獲取快取的 Response 物件(可能為 null)。然後用快取的 Response,以及當前的時間和 Request 構建 CacheStrategy 物件。
CacheStrategy strategy = new CacheStrategy
.Factory(now, chain.request(), cacheCandidate).get();
CacheStrategy 有 networkRequest
和 cacheResponse
兩個變數,根據工廠方法傳入的三個引數,get 方法中有 5 種判斷條件來設定它們的值。
時序 | 條件 | networkRequest |
cacheResponse |
---|---|---|---|
1 |
沒有快取時 2. 請求採用 https 但是快取沒有進行握手的資料 3. 快取不應該被儲存(保留了一些不應該快取的資料) 4. 請求添加了 Cache-Control:No-Cache ,或者一些條件請求首部,說明不希望使用快取 |
傳入的 resquest | null |
2 | 快取響應首部包含 Cache-Control:immutable (不屬於 http 協議),說明資源不會改變 |
null | 傳入的快取響應 |
3 | 新鮮度驗證通過 | null | 傳入的快取響應 (可能會新增一些首部) |
4 | 新鮮度驗證不通過,使用 Etag 或 Last-Modified 或 Date 首部構造條件請求並返回 |
條件請求 | 傳入的快取響應 |
5 | 新鮮度驗證不通過,且快取響應沒有 Etag、 Last-Modified 和 Date 中的任何一個 |
傳入的 resquest | null |
6 | 上述 5 種情況中 networkRequest 不為空時,若 請求通過 Cache-Control:only-if-cached 只允許我們使用當前快取 |
null | null |
在 CacheInterceptor 的 intercept 方法中,會根據建立的 CacheStrategy 物件的兩個變數的值來進行處理。
時序 | networkRequest |
cacheResponse |
處理方法 |
---|---|---|---|
1 | null | null | 直接返回 504 Unsatisfiable Request (only-if-cached) 響應。 |
2 | null | 非 null | 說明快取有效,直接返回 cacheResponse |
3 | 非 null | null 或 非 null |
說明需要向網路傳送請求(原始 request 或新鮮度驗證後 的條件請求): 1. 如果有快取資料,在獲得再驗證的響應後,使用 cache 的 update 方法更新快取; 2. 如果沒有快取資料,判斷請求是否可以被快取,可以的 話就使用 cache 的 put 方法快取下來 |