OkHttpClient源碼分析(三)—— 緩存機制介紹

在講解CacheInterceptor以前,咱們先了解一下OkHttp的緩存機制,主要是Cache這個類,演示下如何使用OkHttp的緩存:java

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()方法,分別用於保存緩存和讀取緩存:bash

@Nullable CacheRequest put(Response response) {
    //獲取請求方法
    String requestMethod = response.request().method();
    
    //1、傳入請求方法,判斷是否要緩存
    if (HttpMethod.invalidatesCache(response.request().method())) {
      try {
        remove(response.request());
      } catch (IOException ignored) {
        // The cache cannot be written.
      }
      return null;
    }
    
    //2、非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;
    }

    //3、建立緩存實體
    Entry entry = new Entry(response);
   
    //4、使用 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():ide

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請求,若是是則不進行緩存。post

最後,開始進行緩存的操做,註釋三處建立了一個Entry類,傳入Response對象,查看其構造方法:ui

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、頭部、請求方式、協議、響應碼等一系列參數保存下來。this

註釋四處,建立緩存策略,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

相關文章
相關標籤/搜索