Android小知識-剖析OkHttp中的五個攔截器(中篇)

本平臺的文章更新會有延遲,你們能夠關注微信公衆號-顧林海,包括年末前會更新kotlin由淺入深系列教程,目前計劃在微信公衆號進行首發,若是你們想獲取最新教程,請關注微信公衆號,謝謝java

在上一小節介紹了重試重定向攔截器RetryAndFollowUpInterceptor和橋接適配攔截器BridgeInterceptor,這節分析緩存攔截器CacheInterceptor。web

緩存策略

mHttpClient = new OkHttpClient.Builder()
                .cache(new Cache(new File("cache"),30*1024*1024))//使用緩存策略
                .build();
複製代碼

在OkHttp中使用緩存能夠經過OkHttpClient的靜態內部類Builder的cache方法進行配置,cache方法傳入一個Cache對象。緩存

public Cache(File directory, long maxSize) {
    this(directory, maxSize, FileSystem.SYSTEM);
  }
複製代碼

建立Cache對象時須要傳入兩個參數,第一個參數directory表明的是緩存目錄,第二個參數maxSize表明的是緩存大小。微信

在Chache有一個很重要的接口:網絡

public final class Cache implements Closeable, Flushable {
        final InternalCache internalCache = new InternalCache() {
            @Override
            public Response get(Request request) throws IOException {
                return Cache.this.get(request);
            }

            @Override
            public CacheRequest put(Response response) throws IOException {
                return Cache.this.put(response);
            }

            @Override
            public void remove(Request request) throws IOException {
                Cache.this.remove(request);
            }

            @Override
            public void update(Response cached, Response network) {
                Cache.this.update(cached, network);
            }

            @Override
            public void trackConditionalCacheHit() {
                Cache.this.trackConditionalCacheHit();
            }

            @Override
            public void trackResponse(CacheStrategy cacheStrategy) {
                Cache.this.trackResponse(cacheStrategy);
            }
        };
        ...
    }
複製代碼

經過InternalCache這個接口實現了緩存的一系列操做,接着咱們一步步看它是如何實現的,接下來分析緩存的get和put操做。框架

先看InternalCache的put方法,也就是存儲緩存:ide

public final class Cache implements Closeable, Flushable {
        final InternalCache internalCache = new InternalCache() {
            ...
            @Override
            public CacheRequest put(Response response) throws IOException {
                return Cache.this.put(response);
            }
            ...
        };
        ...
    }
複製代碼

InternalCache的put方法調用的是Cache的put方法,往下看:ui

@Nullable CacheRequest put(Response response) {
        //標記1:獲取請求方法
        String requestMethod = response.request().method();
        //標記2:判斷緩存是否有效
        if (HttpMethod.invalidatesCache(response.request().method())) {
            try {
                remove(response.request());
            } catch (IOException ignored) {
            }
            return null;
        }
        //標記3:非GET請求不使用緩存
        if (!requestMethod.equals("GET")) {
            return null;
        }

        if (HttpHeaders.hasVaryAll(response)) {
            return null;
        }
        //標記4:建立緩存體類
        Cache.Entry entry = new Cache.Entry(response);
        //標記5:使用DiskLruCache緩存策略
        DiskLruCache.Editor editor = null;
        try {
            editor = cache.edit(key(response.request().url()));
            if (editor == null) {
                return null;
            }
            entry.writeTo(editor);
            return new Cache.CacheRequestImpl(editor);
        } catch (IOException e) {
            abortQuietly(editor);
            return null;
        }
    }
複製代碼

首先在標記1處獲取咱們的請求方式,接着在標記2處根據請求方式判斷緩存是否有效,經過HttpMethod的靜態方法invalidatesCache。this

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

經過invalidatesCache方法,若是請求方式是POST、PATCH、PUT、DELETE以及MOVE中一個,就會將當前請求的緩存移除。加密

在標記3處會判斷若是當前請求不是GET請求,就不會進行緩存。

在標記4處建立Entry對象,Entry的構造器以下:

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

在標記5處能夠看到原來OkHttp的緩存策略使用的是DiskLruCache,DiskLruCache是用於磁盤緩存的一套解決框架,OkHttp對DiskLruCache稍微作了點修改,而且OkHttp內部維護着清理內存的線程池,經過這個線程池完成緩存的自動清理和管理工做,這裏不作過多介紹。

拿到DiskLruCache的Editor對象後,經過它的edit方法建立緩存文件,edit方法傳入的是緩存的文件名,經過key方法將請求url進行MD5加密並獲取它的十六進制表示形式。

接着執行Entry對象的writeTo方法並傳入Editor對象,writeTo方法的目的是將咱們的緩存信息存儲在本地。

點進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');
        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');
        //判斷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();
    }
複製代碼

writeTo方法內部對Response的相關信息進行緩存,並判斷是不是https請求並緩存Https相關信息,從上面的writeTo方法中發現,返回的響應主體body並無在這裏進行緩存,最後返回一個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();
        }
      };
    }
}
複製代碼

在CacheRequestImpl類中有一個body對象,這個就是咱們的響應主體。CacheRequestImpl實現了CacheRequest接口,用於暴露給緩存攔截器,這樣的話緩存攔截器就能夠直接經過這個類來更新或寫入緩存數據。

看完了put方法,繼續看get方法:

public final class Cache implements Closeable, Flushable {
        final InternalCache internalCache = new InternalCache() {
            ...
            @Override public Response get(Request request) throws IOException {
               return Cache.this.get(request);
            }
            ...
        };
        ...
    }
複製代碼

查看Cache的get方法:

@Nullable Response get(Request request) {
        //獲取緩存的key
        String key = key(request.url());
        //建立快照
        DiskLruCache.Snapshot snapshot;
        Cache.Entry entry;
        try {
            //更加key從緩存獲取
            snapshot = cache.get(key);
            if (snapshot == null) {
                return null;
            }
        } catch (IOException e) {
            return null;
        }

        try {
            //從快照中獲取緩存
            entry = new Cache.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;
    }
複製代碼

get方法比較簡單,先是根據請求的url獲取緩存key,建立snapshot目標緩存中的快照,根據key獲取快照,當目標緩存中沒有這個key對應的快照,說明沒有緩存返回null;若是目標緩存中有這個key對應的快照,那麼根據快照建立緩存Entry對象,再從Entry中取出Response。

Entry的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();
        //建立Response
        return new Response.Builder()
                .request(cacheRequest)
                .protocol(protocol)
                .code(code)
                .message(message)
                .headers(responseHeaders)
                .body(new Cache.CacheResponseBody(snapshot, contentType, contentLength))
                .handshake(handshake)
                .sentRequestAtMillis(sentRequestMillis)
                .receivedResponseAtMillis(receivedResponseMillis)
                .build();
    }
複製代碼

Entry的response方法中會根據頭部信息建立緩存請求,而後建立Response對象並返回。

回到get方法,接着判斷響應和請求是否成對出現,若是不是成對出現,關閉流並返回null,不然返回Response。

到這裏緩存的get和put方法的總體流程已經介紹完畢,接下來介紹緩存攔截器。

CacheInterceptor

進入CacheInterceptor的intercept方法,下面貼出部分重要的代碼。

@Override public Response intercept(Interceptor.Chain chain) throws IOException {
        Response cacheCandidate = cache != null
                ? cache.get(chain.request())
                : null;
        ...
}
複製代碼

第一步先嚐試從緩存中獲取Response,這裏分兩種狀況,要麼獲取緩存Response,要麼cacheCandidate爲null。

@Override public Response intercept(Interceptor.Chain chain) throws IOException {
        ...
        CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
        Request networkRequest = strategy.networkRequest;
        Response cacheResponse = strategy.cacheResponse;
        ...
}
複製代碼

第二步,獲取緩存策略CacheStrategy對象,CacheStrategy內部維護了一個Request和一個Response,也就是說CacheStrategy能指定究竟是經過網絡仍是緩存,亦或是二者同時使用獲取Response。

CacheStrategy內部工廠類Factory的get方法以下:

    public CacheStrategy get() {
      CacheStrategy candidate = getCandidate();

      if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
        // We're forbidden from using the network and the cache is insufficient.
        return new CacheStrategy(null, null);
      }

      return candidate;
    }
複製代碼

方法中經過getCandidate方法獲取CacheStrategy對象,繼續點進去:

private CacheStrategy getCandidate() {
        //標記1:沒有緩存Response
        if (cacheResponse == null) {
            return new CacheStrategy(request, null);
        }

        //標記2
        if (request.isHttps() && cacheResponse.handshake() == null) {
            return new CacheStrategy(request, null);
        }

        ...

        CacheControl requestCaching = request.cacheControl();
        //標記3
        if (requestCaching.noCache() || hasConditions(request)) {
            return new CacheStrategy(request, null);
        }

        CacheControl responseCaching = cacheResponse.cacheControl();
        //標記4
        if (responseCaching.immutable()) {
            return new CacheStrategy(null, cacheResponse);
        }

        ...

        //標記5
        if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
            Response.Builder builder = cacheResponse.newBuilder();
            if (ageMillis + minFreshMillis >= freshMillis) {
                builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
            }
            long oneDayMillis = 24 * 60 * 60 * 1000L;
            if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
                builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
            }
            return new CacheStrategy(null, builder.build());
        }
        ...
    }
複製代碼

標記1處,能夠看到先對cacheResponse進行判斷,若是爲空,說明沒有緩存對象,這時建立CacheStrategy對象而且第二個參數Response傳入null。

標記2處,判斷請求是不是https請求,若是是https請求但沒有通過握手操做 ,建立CacheStrategy對象而且第二個參數Response傳入null。

標記3處,判斷若是不使用緩存或者服務端資源改變,亦或者驗證服務端發過來的最後修改的時間戳,一樣建立CacheStrategy對象而且第二個參數Response傳入null。

標記4處,判斷緩存是否受影響,若是不受影響,建立CacheStrategy對象時,第一個參數Request爲null,第二個參數Response直接使用cacheResponse。

標記5處,根據一些信息添加頭部信息 ,最後建立CacheStrategy對象。

回到CacheInterceptor的intercept方法:

@Override public Response intercept(Chain chain) throws IOException {
    ...
    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();
    }

    ...
  }
複製代碼

第三步,判斷當前若是不能使用網絡同時又沒有找到緩存,這時會建立一個Response對象,code爲504的錯誤。

@Override public Response intercept(Chain chain) throws IOException {
    ...
      if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }
    ...
  }
複製代碼

第四步,若是當前不能使用網絡,就直接返回緩存結果。

@Override public Response intercept(Chain chain) throws IOException {
    ...
    Response networkResponse = null;
    try {
      networkResponse = chain.proceed(networkRequest);
    } finally {
      ...
    }
    ...
  }
複製代碼

第五步,調用下一個攔截器進行網絡請求。

@Override public Response intercept(Chain chain) throws IOException {
    ...
    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();
        cache.trackConditionalCacheHit();
        cache.update(cacheResponse, response);
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }
    ...
  }
複製代碼

第六步,當經過下個攔截器獲取Response後,判斷當前若是有緩存Response,而且網絡返回的Response的響應碼爲304,表明從緩存中獲取。

@Override public Response intercept(Chain chain) throws IOException {
    ...
    if (cache != null) {
      //標記1
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {

        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }
      //標記2
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {

        }
      }
    }
    return response;
  }
複製代碼

第七步,標記1判斷http頭部有沒有響應體,而且緩存策略能夠被緩存的,知足這兩個條件後,網絡獲取的Response經過cache的put方法寫入到緩存中,這樣下次取的時候就能夠從緩存中獲取;標記2處判斷請求方法是不是無效的請求方法,若是是的話,從緩存池中刪除這個Request。最後返回Response給上一個攔截器。


838794-506ddad529df4cd4.webp.jpg

搜索微信「顧林海」公衆號,按期推送優質文章。

相關文章
相關標籤/搜索