OkHttp 源码分析(三) 缓存策略

 

目录

 

主流程分析

代码分析

代码总结

具体步骤分析 

获取缓存候选数据

设置缓存

历史缓存数据

通过request获取缓存数据

根据选数据获取缓存策略

缓存数据;

写入缓存 cacheWritingResponse

更新缓存


  • 主流程分析

Okhttp的缓存策略主要是通过拦截器 CacheInterceptor 来实现,关于拦截器的拦截流程请参考关于OkHttp的拦截器执行流程源码分析

  • 代码分析

关于CacheInterceptor我们从代码中一步步来分析,先从 intercept(Chain chain) 方法开始分析。

public Response intercept(Chain chain) throws IOException {
    //1.获取候选缓存数据
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    //2.获取缓存策略
    long now = System.currentTimeMillis();

    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;

    if (cache != null) {
      cache.trackResponse(strategy);
    }

    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }

    //3.根据缓存策略,没有有效的request,并且没有有效缓存,响应504
    // 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();
    }

    //4.根据缓存策略,没有有效的request,不进行网络请求,直接返回候选缓存
    // If we don't need the network, we're done.
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }
    //5.根据缓存策略,具有有效的request,执行网络请求,获取在线数据
    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());
      }
    }

    //6.如果候选缓存不为空,并且在线数据httpcode为304,则使用候选缓存,并更新缓存数据
    // 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());
      }
    }

    //7.根据在线数据,构建响应实例
    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    //8.缓存数据
    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;
  }
  • 代码总结

在以上代码中我们可以看到缓存拦截器对缓存的处理,共有以下几个步骤:

  1. 获取候选缓存数据;

  2. 根据候选缓存数据,获取缓存策略;

  3. 根据缓存策略,没有有效的request,并且没有有效缓存,响应504;

  4. 根据缓存策略,没有有效的request,不进行网络请求,直接返回候选缓存;

  5. 根据缓存策略,具有有效的request,执行网络请求,获取在线数据;

  6. 如果候选缓存不为空,并且在线数据httpcode为304,则使用候选缓存,并更新缓存数据;

  7. 根据在线数据,构建响应实例;

  8. 缓存数据;

  • 具体步骤分析 

通过以上的步骤,缓存拦截器完成了缓存的获取,更新,存储等,下面来具体分析一下每个步骤。

  • 获取缓存候选数据

在这个步骤中一共有两个前提条件 1.设置了cache。2.有历史缓存数据。满足这两个条件则可以通过request取到缓存数据。

  • 设置缓存

cache是在CacheInterceptor构造中方法中传入的,而 CacheInterceptor 的构造方法传入就是 client.internalCache()。client.internalCache()就是通过 builder.cache(new Cache()) 设置,所以在这里我们也可以看到为什么要通过builder 设置cache。

  • 历史缓存数据

是在其他步骤中获取到的数据缓存到本地的历史数据。

  • 通过request获取缓存数据

    Response get(Request request) {
        //1.通过 url 获取 MD5 key
        String key = key(request.url());
        //2.获取快照,快照不可用则return null
        Snapshot snapshot;
        try {
            snapshot = this.cache.get(key);
            if (snapshot == null) {
                return null;
            }
        } catch (IOException var7) {
            return null;
        }

        //3.通过快照创建entry
        Cache.Entry entry;
        try {
            entry = new Cache.Entry(snapshot.getSource(0));
        } catch (IOException var6) {
            Util.closeQuietly(snapshot);
            return null;
        }

        //4.获取候选的response对象
        Response response = entry.response(snapshot);
        if (!entry.matches(request, response)) {
            Util.closeQuietly(response.body());
            return null;
        } else {
            return response;
        }
    }

 

cache是通过request作为key获取的,在这段代码的第2步,是通过url作为key向cache中去取snapshot快照,在后面我们也可以看到通过url作为key向cache中put snapshot数据。

  • 根据选数据获取缓存策略

从下面的代码中我们可以看到通过当前的时间戳、请求信息、缓存候选数据获得了一个缓存策略。缓存策略的创建规则如下:

  1. 缓存无效,返回networkRequest请求实体
  2. 缓存有效,但是请求是https并且http握手信息丢失,返回networkRequest请求实体
  3. 缓存数据不可以被复用,返回networkRequest请求实体
  4. 请求的缓存管理,强制要求不使用缓存,或者请求包头中包含“If-Modified-Since”或者“If-None-Match”字段,返回networkRequest请求实体
  5. 缓存有效,并且在有效的时间范围内,则返回cacheResponse缓存
  6. 如果缓存中“ETag”、“If-Modified”、“Date”等字段不为空则创建带参请求,返回conditionalRequest, cacheResponse。否则返回networkRequest请求实体

代码主要在CacheStrategy getCandidate 方法

    /** Returns a strategy to use assuming the request can use the network. */
    private CacheStrategy getCandidate() {
      //1.缓存无效,返回networkRequest请求实体
      // No cached response.
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }

      //2.缓存有效,但是请求是https并且http握手信息丢失,返回networkRequest请求实体
      // Drop the cached response if it's missing a required handshake.
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }

      //3.缓存数据不可以被复用,返回networkRequest请求实体
      // If this response shouldn't have been stored, it should never be used
      // as a response source. This check should be redundant as long as the
      // persistence store is well-behaved and the rules are constant.
      if (!isCacheable(cacheResponse, request)) {
        return new CacheStrategy(request, null);
      }

      //4.请求的缓存管理,强制要求不使用缓存,或者请求包头中包含“If-Modified-Since”或者“If-None-Match”字段,返回networkRequest请求实体
      CacheControl requestCaching = request.cacheControl();
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }

      //5.缓存有效,并且在有效的时间范围内,则返回cacheResponse缓存
      CacheControl responseCaching = cacheResponse.cacheControl();

      long ageMillis = cacheResponseAge();
      long freshMillis = computeFreshnessLifetime();

      if (requestCaching.maxAgeSeconds() != -1) {
        freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
      }

      long minFreshMillis = 0;
      if (requestCaching.minFreshSeconds() != -1) {
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
      }

      long maxStaleMillis = 0;
      if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
      }

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

      //6.如果缓存中“ETag”、“If-Modified”、“Date”等字段不为空则创建带参请求,返回conditionalRequest, cacheResponse。
      //否则返回networkRequest请求实体 
      // Find a condition to add to the request. If the condition is satisfied, the response body
      // will not be transmitted.
      String conditionName;
      String conditionValue;
      if (etag != null) {
        conditionName = "If-None-Match";
        conditionValue = etag;
      } else if (lastModified != null) {
        conditionName = "If-Modified-Since";
        conditionValue = lastModifiedString;
      } else if (servedDate != null) {
        conditionName = "If-Modified-Since";
        conditionValue = servedDateString;
      } else {
        return new CacheStrategy(request, null); // No condition! Make a regular request.
      }

      Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
      Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);

      Request conditionalRequest = request.newBuilder()
          .headers(conditionalRequestHeaders.build())
          .build();
      return new CacheStrategy(conditionalRequest, cacheResponse);
    }

  • 缓存数据;

判断是否为有效的response数据,包括是否存在body和是否可缓存,主要根据http协议约定。放入cache 队列

    CacheRequest put(Response response) {
        String requestMethod = response.request().method();
        if (HttpMethod.invalidatesCache(response.request().method())) {
            try {
                this.remove(response.request());
            } catch (IOException var6) {
            }

            return null;
        } else if (!requestMethod.equals("GET")) {
            return null;
        } else if (HttpHeaders.hasVaryAll(response)) {
            return null;
        } else {
            // 有效的缓存数据,构建包含editor的CacheRequestImpl实例
            Cache.Entry entry = new Cache.Entry(response);
            Editor editor = null;

            try {
                editor = this.cache.edit(key(response.request().url()));
                if (editor == null) {
                    return null;
                } else {
                    entry.writeTo(editor);
                    return new Cache.CacheRequestImpl(editor);
                }
            } catch (IOException var7) {
                this.abortQuietly(editor);
                return null;
            }
        }
    }

在上面的代码中看到,在这里显示判断了缓存的可用性,然后构建了包含editor的CacheRequestImpl实例,在这里缓存并没有真正的写入文件,缓存并不可用。

写入缓存 cacheWritingResponse

/**
   * Returns a new source that writes bytes to {@code cacheRequest} as they are read by the source
   * consumer. This is careful to discard bytes left over when the stream is closed; otherwise we
   * may never exhaust the source stream and therefore not complete the cached response.
   */
  private Response cacheWritingResponse(final CacheRequest cacheRequest, Response response)
      throws IOException {
    // Some apps return a null body; for compatibility we treat that like a null cache request.
    if (cacheRequest == null) return response;
    Sink cacheBodyUnbuffered = cacheRequest.body();
    if (cacheBodyUnbuffered == null) return response;

    final BufferedSource source = response.body().source();
    final BufferedSink cacheBody = Okio.buffer(cacheBodyUnbuffered);

    Source cacheWritingSource = new Source() {
      boolean cacheRequestClosed;

      @Override public long read(Buffer sink, long byteCount) throws IOException {
        long bytesRead;
        try {
          bytesRead = source.read(sink, byteCount);
        } catch (IOException e) {
          if (!cacheRequestClosed) {
            cacheRequestClosed = true;
            cacheRequest.abort(); // Failed to write a complete cache response.
          }
          throw e;
        }

        if (bytesRead == -1) {
          if (!cacheRequestClosed) {
            cacheRequestClosed = true;
            cacheBody.close(); // The cache response is complete!
          }
          return -1;
        }

        sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);
        cacheBody.emitCompleteSegments();
        return bytesRead;
      }

      @Override public Timeout timeout() {
        return source.timeout();
      }

      @Override public void close() throws IOException {
        if (!cacheRequestClosed
            && !discard(this, ExchangeCodec.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
          cacheRequestClosed = true;
          cacheRequest.abort();
        }
        source.close();
      }
    };

    String contentType = response.header("Content-Type");
    long contentLength = response.body().contentLength();
    return response.newBuilder()
        .body(new RealResponseBody(contentType, contentLength, Okio.buffer(cacheWritingSource)))
        .build();
  }

在上一段我们提交,缓存put之后并没有真正的写入文件,缓存并不可用,在这一步骤中可以看到原因。只有在Callback.onResponse中读取了字节流信息,才会关闭流,并完成缓存。这里可以看到调用editor.commit

     public void close() throws IOException {
          synchronized (Cache.this) {
            if (done) {
              return;
            }
            done = true;
            writeSuccessCount++;
          }
          super.close();
          editor.commit();
        }
      }

而只有调用了editor.commit,entry.readable设置为完,缓存才算是完成,后续才可以使用该缓存。我们可以看到在get的时候会判断readable这个标志

  public synchronized Snapshot get(String key) throws IOException {
    initialize();

    checkNotClosed();
    validateKey(key);
    Entry entry = lruEntries.get(key);
    // 判断标志位,缓存是否可用
    if (entry == null || !entry.readable) return null;

    Snapshot snapshot = entry.snapshot();
    if (snapshot == null) return null;

    redundantOpCount++;
    journalWriter.writeUtf8(READ).writeByte(' ').writeUtf8(key).writeByte('\n');
    if (journalRebuildRequired()) {
      executor.execute(cleanupRunnable);
    }

    return snapshot;
  }

 到这里缓存已经可用了。

  • 更新缓存

看过上文分支的分析,更新缓存这里就比较简单了,主要是cache的update方法中,下面我们来看代码

  void update(Response cached, Response network) {
    Entry entry = new Entry(network);
    DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot;
    DiskLruCache.Editor editor = null;
    try {
      editor = snapshot.edit(); // Returns null if snapshot is not current.
      if (editor != null) {
        entry.writeTo(editor);
        editor.commit();
      }
    } catch (IOException e) {
      abortQuietly(editor);
    }
  }

从上面代码中可以看到通过cached获取到editor,写入并commit提交,完成了缓存的更新。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值