1. 程式人生 > >OkHttp3 使用詳解及網路連線快取的處理機制

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。

  1. 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 {

    }
}
  1. 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 個概念:

  1. 新鮮度檢測
  2. 在驗證
  3. 在驗證命中

在使用快取將 url 響應資源的副本儲存在本地時,當我們再次對該 url 資源發起請求時,可以快速從本地儲存中獲取該 url 資源,而不需要重新發起網路連線。

但是,伺服器的 url 資源可能在一定時間後被修改,因此我們不能一直使用快取資源,在一定時間之前,認為可以使用快取資源,在這個時間之後認為不能再使用快取資源,需要重新請求網路資源。這個條件的判斷就是 新鮮度檢測。就像我們在吃食品之前要看看它有沒有過期。

url 資源快取超過一定時間,我們也不會直接重新請求 url 資源,而是去伺服器檢視資源是否已經發生改變,這就叫 再驗證

如果伺服器發現 url 資源沒有改變,就返回 304 Not Modified,並不在返回對應實體,這就叫 再驗證命中,此時我們可以繼續使用 url 資源快取。如果發生了變化,則返回 200 OK,並將改變後的 url 資源返回。

具體實現:

  1. 新鮮度檢測

    http1.1 規範中,通過響應首部的 Cache-Control:max-age 和 Last-Modified 計算出絕對時間。http1.0 規範中,響應首部 Expires 的值就是絕對時間。

  2. 再驗證

    超過絕對時間後要進行再驗證,需要根據響應首部具體的規範來進行 條件請求,通常有 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 有 networkRequestcacheResponse 兩個變數,根據工廠方法傳入的三個引數,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 方法快取下來

參考資料