刨解OkHttp之緩存機制

時間一晃而過,今天想給大家?guī)鞳kHttp的zuihou最后一篇文章,主要講一下OkHttp的緩存機制。OkHttp的責任鏈中有一個攔截器就是專門應(yīng)對OkHttp的緩存的,那就是CacheInterceptor攔截器。

CacheInterceptor

其對應(yīng)的方法如下,我們就從這個方法講起:

public Response intercept(Chain chain) throws IOException {
    
    //假如有緩存,會得到拿到緩存,否則為null
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();

    //獲取緩存策略
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();

    //緩存策略請求
    Request networkRequest = strategy.networkRequest;
    //緩存策略響應(yīng)
    Response cacheResponse = strategy.cacheResponse;

    //緩存非空判斷
    if (cache != null) {
      cache.trackResponse(strategy);
    }

    //本地緩存不為空并且緩存策略響應(yīng)為空
    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }

    //緩存策略請求和緩存策略響應(yīng)為空,禁止使用網(wǎng)絡(luò)直接返回
    // If we're forbidden from using the network and the cache is insufficient, fail.
    if (networkRequest == null && cacheResponse == null) {
      return new Response.Builder()
          .request(chain.request())
          .protocol(Protocol.HTTP_1_1)
          .code(504)
          .message("Unsatisfiable Request (only-if-cached)")
          .body(Util.EMPTY_RESPONSE)
          .sentRequestAtMillis(-1L)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();
    }

    //緩存策略請求為空,即緩存有效則直接使用緩存不使用網(wǎng)絡(luò)
    // If we don't need the network, we're done.
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

    //緩存無效,則執(zhí)行下一個攔截器以獲取請求
    Response networkResponse = null;
    try {
      networkResponse = chain.proceed(networkRequest);
    } finally {
      // If we're crashing on I/O or otherwise, don't leak the cache body.
      if (networkResponse == null && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
      }
    }

    //假如本地也有緩存,則根據(jù)條件選擇使用哪個響應(yīng)
    // If we have a cache response too, then we're doing a conditional get.
    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();

        // Update the cache after combining headers but before stripping the
        // Content-Encoding header (as performed by initContentStream()).
        cache.trackConditionalCacheHit();
        //更新緩存
        cache.update(cacheResponse, response);
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }

    //沒有緩存,則直接使用網(wǎng)絡(luò)響應(yīng)
    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    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.
        }
      }
    }

    return response;
  }

這就是整個緩存攔截器的主要方法,首先會從cache去拿緩存,沒有則返回null,然后通過CacheStrategy來獲取緩存策略,CacheStrategy根據(jù)之前緩存的結(jié)果與當前將要發(fā)送Request的header進行策略,并得出是否進行請求的結(jié)果。由于篇幅關(guān)系,這一塊不細講因為涉及網(wǎng)絡(luò)協(xié)議,最終他的得出的規(guī)則如下如:


image.png

因為我把注釋流程都寫在代碼了,大家可以看上面方法代碼理解,其整體緩存流程如下:

  1. 如果有緩存,則取出緩存否則為null
  2. 根據(jù)CacheStrategy拿到它的緩存策略請求和響應(yīng)
  3. 緩存策略請求和緩存策略響應(yīng)為空,禁止使用網(wǎng)絡(luò)直接返回
  4. 緩存策略請求為空,即緩存有效則直接使用緩存不使用網(wǎng)絡(luò)
  5. 緩存無效,則執(zhí)行下一個攔截器以獲取請求
  6. 假如本地也有緩存,則根據(jù)條件選擇使用哪個響應(yīng),更新緩存
  7. 沒有緩存,則直接使用網(wǎng)絡(luò)響應(yīng)
  8. 添加緩存

到這里我們可以看到,緩存的“增刪改查”都是cache(Cache)類來進行操作的。下面讓我們來看一下這個類吧。

Cache

Cache的“增刪改查”其實都是基于DiskLruCache,下面我們會繼續(xù)講,先來看一下“增刪改查”的各個方法吧

  • 添加緩存
CacheRequest put(Response response) {
    String requestMethod = response.request().method();
    //如果請求是"POST","PUT","PATCH","PROPPATCH","REPORT"則移除這些緩存  
    if (HttpMethod.invalidatesCache(response.request().method())) {
      try {
        remove(response.request());
      } catch (IOException ignored) {
      }
      return null;
    }
    //僅支持GET的請求緩存,其他請求不緩存
    if (!requestMethod.equals("GET")) {
       return null;
    }
    //判斷請求中的http數(shù)據(jù)包中headers是否有符號"*"的通配符,有則不緩存  
    if (HttpHeaders.hasVaryAll(response)) {
      return null;
    }
    //把response構(gòu)建成一個Entry對象
    Entry entry = new Entry(response);
    DiskLruCache.Editor editor = null;
    try {
      //生成DiskLruCache.Editor對象
      editor = cache.edit(key(response.request().url()));
      if (editor == null) {
        return null;
      }
      //對緩存進行寫入
      entry.writeTo(editor);
      //構(gòu)建一個CacheRequestImpl類,包含Ok.io的Sink對象
      return new CacheRequestImpl(editor);
    } catch (IOException e) {
      abortQuietly(editor);
      return null;
    }
  }
  • 得到緩存
Response get(Request request) {
    //獲取url轉(zhuǎn)換過來的key
    String key = key(request.url());
    DiskLruCache.Snapshot snapshot;
    Entry entry;
    try {
         //根據(jù)key獲取對應(yīng)的snapshot 
         snapshot = cache.get(key);
         if (snapshot == null) {
             return null;
         }
    } catch (IOException e) {
      return null;
    }
    try {
     //創(chuàng)建一個Entry對象,并由snapshot.getSource()獲取Sink
      entry = new Entry(snapshot.getSource(ENTRY_METADATA));
    } catch (IOException e) {
      Util.closeQuietly(snapshot);
      return null;
    }
    //通過entry和response生成respson,通過Okio.buffer獲取請求體,然后封裝各種請求信息
    Response response = entry.response(snapshot);
    if (!entry.matches(request, response)) {
      //對request和Response進行比配檢查,成功則返回該Response。
      Util.closeQuietly(response.body());
      return null;
    }
    return response;
  }
  • 更新緩存
void update(Response cached, Response network) {
    //用Respon構(gòu)建一個Entry
    Entry entry = new Entry(network);
    //從緩存中獲取DiskLruCache.Snapshot
    DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot;
    DiskLruCache.Editor editor = null;
    try {
      //獲取DiskLruCache.Snapshot.edit對象
      editor = snapshot.edit(); // Returns null if snapshot is not current.
      if (editor != null) {
        //將entry寫入editor中
        entry.writeTo(editor);
        editor.commit();
      }
    } catch (IOException e) {
      abortQuietly(editor);
    }
  }
  • 刪除緩存
void remove(Request request) throws IOException {
    //通過url轉(zhuǎn)化成的key去刪除緩存
    cache.remove(key(request.url()));
  }

Cache的"增刪改查"大體通過注釋代碼的方式給出,Cache還有一個更重要的緩存處理類就是DiskLruCache。

DiskLruCache

不仔細看還以為這個類和JakeWharton寫的DiskLruCache:[https://link.jianshu.com/t=https://github.com/JakeWharton/DiskLruCache(https://link.jianshu.com/t=https://github.com/JakeWharton/DiskLruCache)是一樣的,其實主體架構(gòu)差不多,只不過OkHttp的DiskLruCache結(jié)合了Ok.io,用Ok.io處理數(shù)據(jù)文件的儲存.
我們可以看到上面的DiskLruCache有shang三個內(nèi)部類,分別是Entry,Snapshot,Editor。

Entry

final String key;

    /** Lengths of this entry's files. */
    final long[] lengths;
    final File[] cleanFiles;
    final File[] dirtyFiles;

    /** True if this entry has ever been published. */
    boolean readable;

    /** The ongoing edit or null if this entry is not being edited. */
    Editor currentEditor;

    /** The sequence number of the most recently committed edit to this entry. */
    long sequenceNumber;

    Entry(String key) {
      this.key = key;

      lengths = new long[valueCount];
      cleanFiles = new File[valueCount];
      dirtyFiles = new File[valueCount];

      // The names are repetitive so re-use the same builder to avoid allocations.
      StringBuilder fileBuilder = new StringBuilder(key).append('.');
      int truncateTo = fileBuilder.length();
      for (int i = 0; i < valueCount; i++) {
        fileBuilder.append(i);
        cleanFiles[i] = new File(directory, fileBuilder.toString());
        fileBuilder.append(".tmp");
        dirtyFiles[i] = new File(directory, fileBuilder.toString());
        fileBuilder.setLength(truncateTo);
      }
    }
    
    //省略
    ......

實際上只是用于存儲緩存數(shù)據(jù)的實體類,一個url對應(yīng)一個實體,在Entry還有Snapshot對象,代碼如下:

Snapshot snapshot() {
      if (!Thread.holdsLock(DiskLruCache.this)) throw new AssertionError();

      Source[] sources = new Source[valueCount];
      long[] lengths = this.lengths.clone(); // Defensive copy since these can be zeroed out.
      try {
        for (int i = 0; i < valueCount; i++) {
          sources[i] = fileSystem.source(cleanFiles[i]);
        }
        return new Snapshot(key, sequenceNumber, sources, lengths);
      } catch (FileNotFoundException e) {
        // A file must have been deleted manually!
        for (int i = 0; i < valueCount; i++) {
          if (sources[i] != null) {
            Util.closeQuietly(sources[i]);
          } else {
            break;
          }
        }
        // Since the entry is no longer valid, remove it so the metadata is accurate (i.e. the cache
        // size.)
        try {
          removeEntry(this);
        } catch (IOException ignored) {
        }
        return null;
      }
    }

即一個Entry對應(yīng)著一個Snapshot對象,在看一下Snapshot的內(nèi)部代碼:

public final class Snapshot implements Closeable {
    private final String key;
    private final long sequenceNumber;
    private final Source[] sources;
    private final long[] lengths;

    Snapshot(String key, long sequenceNumber, Source[] sources, long[] lengths) {
      this.key = key;
      this.sequenceNumber = sequenceNumber;
      this.sources = sources;
      this.lengths = lengths;
    }

    public String key() {
      return key;
    }

    /**
     * Returns an editor for this snapshot's entry, or null if either the entry has changed since
     * this snapshot was created or if another edit is in progress.
     */
    public @Nullable Editor edit() throws IOException {
      return DiskLruCache.this.edit(key, sequenceNumber);
    }

    /** Returns the unbuffered stream with the value for {@code index}. */
    public Source getSource(int index) {
      return sources[index];
    }

    /** Returns the byte length of the value for {@code index}. */
    public long getLength(int index) {
      return lengths[index];
    }

    public void close() {
      for (Source in : sources) {
        Util.closeQuietly(in);
      }
    }
  }

初始化的Snapshot僅僅只是存儲了一些變量而已。

Editor

在Editor的初始化中要傳入Editor,其實Editor就是編輯entry的類。源碼如下:

public final class Editor {
    final Entry entry;
    final boolean[] written;
    private boolean done;

    Editor(Entry entry) {
      this.entry = entry;
      this.written = (entry.readable) ? null : new boolean[valueCount];
    }
  
    void detach() {
      if (entry.currentEditor == this) {
        for (int i = 0; i < valueCount; i++) {
          try {
            fileSystem.delete(entry.dirtyFiles[i]);
          } catch (IOException e) {
            // This file is potentially leaked. Not much we can do about that.
          }
        }
        entry.currentEditor = null;
      }
    }

    //返回指定index的cleanFile的讀入流
    public Source newSource(int index) {
      synchronized (DiskLruCache.this) {
        if (done) {
          throw new IllegalStateException();
        }
        if (!entry.readable || entry.currentEditor != this) {
          return null;
        }
        try {
          return fileSystem.source(entry.cleanFiles[index]);
        } catch (FileNotFoundException e) {
          return null;
        }
      }
    }
    
    //向指定index的dirtyFiles文件寫入數(shù)據(jù)
    public Sink newSink(int index) {
      synchronized (DiskLruCache.this) {
        if (done) {
          throw new IllegalStateException();
        }
        if (entry.currentEditor != this) {
          return Okio.blackhole();
        }
        if (!entry.readable) {
          written[index] = true;
        }
        File dirtyFile = entry.dirtyFiles[index];
        Sink sink;
        try {
          sink = fileSystem.sink(dirtyFile);
        } catch (FileNotFoundException e) {
          return Okio.blackhole();
        }
        return new FaultHidingSink(sink) {
          @Override protected void onException(IOException e) {
            synchronized (DiskLruCache.this) {
              detach();
            }
          }
        };
      }
    }

    //這里執(zhí)行的工作是提交數(shù)據(jù),并釋放鎖,最后通知DiskLruCache刷新相關(guān)數(shù)據(jù)
    public void commit() throws IOException {
      synchronized (DiskLruCache.this) {
        if (done) {
          throw new IllegalStateException();
        }
        if (entry.currentEditor == this) {
          completeEdit(this, true);
        }
        done = true;
      }
    }

    //終止編輯,并釋放鎖
    public void abort() throws IOException {
      synchronized (DiskLruCache.this) {
        if (done) {
          throw new IllegalStateException();
        }
        if (entry.currentEditor == this) {
          completeEdit(this, false);
        }
        done = true;
      }
    }

    //除非正在編輯,否則終止
    public void abortUnlessCommitted() {
      synchronized (DiskLruCache.this) {
        if (!done && entry.currentEditor == this) {
          try {
            completeEdit(this, false);
          } catch (IOException ignored) {
          }
        }
      }
    }
  }

各個方法對應(yīng)作用如下:

  • Source newSource(int index):返回指定index的cleanFile的讀入流
  • Sink newSink(int index):向指定index的dirtyFiles文件寫入數(shù)據(jù)
  • commit():這里執(zhí)行的工作是提交數(shù)據(jù),并釋放鎖,最后通知DiskLruCache刷新相關(guān)數(shù)據(jù)
  • abort():終止編輯,并釋放鎖
  • abortUnlessCommitted():除非正在編輯,否則終止

剩下關(guān)鍵來了,還記得上面我們講Cache添加有一行代碼entry.writeTo(editor);,里面操作如下:

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

      sink.writeUtf8(url)
          .writeByte('\n');
      sink.writeUtf8(requestMethod)
          .writeByte('\n');
      sink.writeDecimalLong(varyHeaders.size())
          .writeByte('\n');
      for (int i = 0, size = varyHeaders.size(); i < size; i++) {
        sink.writeUtf8(varyHeaders.name(i))
            .writeUtf8(": ")
            .writeUtf8(varyHeaders.value(i))
            .writeByte('\n');
      }

      sink.writeUtf8(new StatusLine(protocol, code, message).toString())
          .writeByte('\n');
      sink.writeDecimalLong(responseHeaders.size() + 2)
          .writeByte('\n');
      for (int i = 0, size = responseHeaders.size(); i < size; i++) {
        sink.writeUtf8(responseHeaders.name(i))
            .writeUtf8(": ")
            .writeUtf8(responseHeaders.value(i))
            .writeByte('\n');
      }
      sink.writeUtf8(SENT_MILLIS)
          .writeUtf8(": ")
          .writeDecimalLong(sentRequestMillis)
          .writeByte('\n');
      sink.writeUtf8(RECEIVED_MILLIS)
          .writeUtf8(": ")
          .writeDecimalLong(receivedResponseMillis)
          .writeByte('\n');

      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();
    }

上面的都是Ok.io的操作了,不懂OK.io的可以去看一下相關(guān)知識。BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));editor.newSink拿到ok.io版的OutputStream(Sink)生成Ok.io的輸入類,剩下的就是把數(shù)據(jù)用ok.io寫入文件,然后關(guān)閉輸出類。

同理我看們可以一下上面Cache獲取緩存的代碼 Response response = entry.response(snapshot);,在response方法里又有一個方法:CacheResponseBody()就是獲取緩存的方法,代碼如下:

 CacheResponseBody(final DiskLruCache.Snapshot snapshot,String contentType, String contentLength) {
      this.snapshot = snapshot;
      this.contentType = contentType;
      this.contentLength = contentLength;

      Source source = snapshot.getSource(ENTRY_BODY);
      bodySource = Okio.buffer(new ForwardingSource(source) {
        @Override public void close() throws IOException {
          snapshot.close();
          super.close();
        }
      });
    }

new ForwardingSource(source)相當于傳入ok.io版的InputStream(Source)生成Ok.io的讀取類,剩下的都是讀取緩存數(shù)據(jù)然后生成Response.

而上面Cache的Update()方法,其寫入過程也和上面的添加是一樣的,不同的只不過先構(gòu)造成一個就得Entry然后再把新的緩存寫上去更新而已,因為涉及我重要的Ok.io是一樣的,所以不細講。

剩下就是刪除了,在Cache的delete方法里,在removeEntry就是執(zhí)行刪除操作,代碼如下:

 boolean removeEntry(Entry entry) throws IOException {
  
    //省略

    journalWriter.writeUtf8(REMOVE).writeByte(' ').writeUtf8(entry.key).writeByte('\n');
    lruEntries.remove(entry.key);

    //省略
    return true;
  }

上面這兩句代碼就是刪除的關(guān)鍵, journalWriter.writeUtf8表示在DiskLruCache的本地緩存清單列表里刪除,lruEntries.remove表示在緩存內(nèi)存里刪除。

到此增刪給查的流程基本結(jié)束,其實DiskLruCache還有很多可以講,但是我的重心是OKhttp的緩存底層是用Ok.io,為此在這里點到為止。

內(nèi)容有點多,如有錯誤請多多指出

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容