OkHttpClient原始碼分析(三)—— 快取機制介紹

chaychan發表於2019-01-04

在講解CacheInterceptor之前,我們先了解一下OkHttp的快取機制,主要是Cache這個類,演示下如何使用OkHttp的快取:

private void cacheOkHttpRequest(){
        OkHttpClient okHttpClient = new OkHttpClient
                .Builder()
                .cache(new Cache(new File(Environment.getExternalStorageDirectory()+ "/okttp_caches"),24*1024*1024))
                .build();
        Request request = new Request
                .Builder()
                .url("http://www.ifeng.com")
                .build();
        Call call = okHttpClient.newCall(request);
        try {
            Response response = call.execute();
            Log.e(TAG,"response: " + response.body().string());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
複製程式碼

上述程式碼中在OkHttpClient的Builder中配置了cache,傳入快取目錄的File物件以及快取最大容量(單位位元組),這裡請求了鳳凰網,請求成功後,OkHttp會將請求的相關資料進行快取,當下次請求無法連結到網路的時候,它會讀取快取並將資料返回。

根據快取的流程,我們可以猜測到Cache類會有儲存和讀取快取的方法,我們通過檢視Cache類的原始碼,果然發現了put()和get()方法,分別用於儲存快取和讀取快取:

@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;
    }
    
    //二、非GET請求的方法不進行快取
    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快取策略
    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;
    }
  }
複製程式碼

在分析這個方法之前,需要先知道的是,OkHttp預設只會對get請求進行快取,post請求是不會進行快取,這也是有道理的,因為get請求的資料一般是比較持久的,而post一般是互動操作,沒太大意義進行快取;當然,也可以自己實現post請求的快取操作,這個需要根據自己的專案需求來。

回到put()方法的分析,上述程式碼中,先是對請求方法進行了判斷,是否需要快取下來。

註釋一處,呼叫了HttpMethod的invalidatesCache():

public final class HttpMethod {
  public static boolean invalidatesCache(String method) {
    return method.equals("POST")
        || method.equals("PATCH")
        || method.equals("PUT")
        || method.equals("DELETE")
        || method.equals("MOVE");     // WebDAV
  }
  ...
}
複製程式碼

如果請求方法是POST、PATCH、PUT、DELETE以及MOVE中一個,就會將當前請求的快取移除。

接著判斷是否是非GET請求,如果是則不進行快取。

最後,開始進行快取的操作,註釋三處建立了一個Entry類,傳入Response物件,檢視其構造方法:

  Entry(Response response) {
      this.url = response.request().url().toString();
      this.varyHeaders = HttpHeaders.varyHeaders(response);
      this.requestMethod = response.request().method();
      this.protocol = response.protocol();
      this.code = response.code();
      this.message = response.message();
      this.responseHeaders = response.headers();
      this.handshake = response.handshake();
      this.sentRequestMillis = response.sentRequestAtMillis();
      this.receivedResponseMillis = response.receivedResponseAtMillis();
 }
複製程式碼

Entry會將請求url、頭部、請求方式、協議、響應碼等一系列引數儲存下來。

註釋四處,建立快取策略,OkHttp的快取策略使用的是DiskLruCache,DiskLruCache是用於磁碟快取的一套解決框架,OkHttp對DiskLruCache稍微做了點修改,並且OkHttp內部維護著清理記憶體的執行緒池,通過這個執行緒池完成快取的自動清理和管理工作,本篇不做過多介紹。

獲取到DiskLruCache的Editor物件後,通過它的edit方法建立快取檔案,傳入快取的檔名,檔名的生成是通過key()方法將請求url進行MD5加密並獲取它的十六進位制表示形式。

接著執行Entry物件的writeTo()方法並傳入Editor物件,writeTo()方法是將快取資訊儲存在本地:

public void writeTo(DiskLruCache.Editor editor) throws IOException {
      BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));

      //快取請求的url
      sink.writeUtf8(url)
          .writeByte('\n');
      //快取請求方法
      sink.writeUtf8(requestMethod)
          .writeByte('\n');
      //快取請求頭部大小
      sink.writeDecimalLong(varyHeaders.size())
          .writeByte('\n');
     
     //快取請求頭部資訊
     //快取協議,響應碼
     //快取響應頭部資訊
     //快取傳送請求時間以及響應時間
      ...
      
      //判斷是否是https請求
      if (isHttps()) {
        //快取相關資訊
        sink.writeByte('\n');
        sink.writeUtf8(handshake.cipherSuite().javaName())
            .writeByte('\n');
        writeCertList(sink, handshake.peerCertificates());
        writeCertList(sink, handshake.localCertificates());
        sink.writeUtf8(handshake.tlsVersion().javaName()).writeByte('\n');
      }
      sink.close();
    }
複製程式碼

上述程式碼中並沒有對響應主體的body進行快取,在呼叫Entry的writeTo()方法之後,返回了一個CacheRequestImpl物件:

  private final class CacheRequestImpl implements CacheRequest {
    private final DiskLruCache.Editor editor;
    private Sink cacheOut;
    private Sink body;
    boolean done;

    CacheRequestImpl(final DiskLruCache.Editor editor) {
      this.editor = editor;
      this.cacheOut = editor.newSink(ENTRY_BODY);
      this.body = new ForwardingSink(cacheOut) {
        @Override public void close() throws IOException {
          synchronized (Cache.this) {
            if (done) {
              return;
            }
            done = true;
            writeSuccessCount++;
          }
          super.close();
          editor.commit();
        }
      };
    }
複製程式碼

這裡我們可以看到Repsonse的body是在這裡被寫入快取的,CacheRequestImpl實現CacheRequest介面,用於暴露給快取攔截器,快取攔截器可以通過這個類來寫入或更新快取的資料。

接下來分析獲取快取的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;
  }
複製程式碼

顯示通過請求的url獲取到快取對應的檔名key,然後建立一個DiskLruCache.Snapshot快取快照物件,根據key獲取對應的快取快照,如果獲取到的為null,則說明沒有找到快取,直接返回null;如果找到對應的快取快照,則根據快照生成Entry物件,再通過呼叫Entry的response()方法獲取到快取的Response物件。

response()方法:

public Response response(DiskLruCache.Snapshot snapshot) {
      String contentType = responseHeaders.get("Content-Type");
      String contentLength = responseHeaders.get("Content-Length");
      Request cacheRequest = new Request.Builder()
          .url(url)
          .method(requestMethod, null)
          .headers(varyHeaders)
          .build();
      return new Response.Builder()
          .request(cacheRequest)
          .protocol(protocol)
          .code(code)
          .message(message)
          .headers(responseHeaders)
          .body(new CacheResponseBody(snapshot, contentType, contentLength))
          .handshake(handshake)
          .sentRequestAtMillis(sentRequestMillis)
          .receivedResponseAtMillis(receivedResponseMillis)
          .build();
    }
}
複製程式碼

這裡實際上根據快取的資料建立了Request請求,並返回生成的Response物件。

獲取到快取的Response物件後,get()方法裡將其與傳入的request進行比對,如果二者不是成對的,則關閉流且返回null,如果是的話,則返回由快取資料生成的Response物件。

關於OkHttpClient的快取機制,這裡已經初步介紹完了,本篇主要是為介紹CacheInterceptor做鋪墊,下一篇我們將瞭解第三個攔截器CacheInterceptor快取攔截器,感興趣的朋友可以繼續閱讀:

OkHttpClient原始碼分析(四)—— CacheInterceptor

相關文章