面试官:Okhttp中缓存和缓存策略如何设置?DiskLruCache中是如何实现缓存的?

缓存

Okhttp中设置缓存包含两个方面:

  1. 在OkHttpClient中设置缓存的路径和缓存的大小。
  2. 在Request中设置缓存条件。

首先我们创建OkHttpClient对象时,通过调用 cache 方法来设置缓存的路径和缓存的大小。在Request的构造链中,通过 cacheControl 方法,可以用来设置每个请求的缓存条件。

    private val request1 :Request = Request.Builder()
        .url("https://www.wanandroid.com/article/list/0/json")
        .method("GET",null)
        .cacheControl(CacheControl.Builder().noStore().build())
        .build()

    private val okhttpClient1:OkHttpClient = OkHttpClient.Builder()
        .cache(Cache(application.cacheDir,10*1024*1024))
        .build()

在针对单条Request设置缓存条件时,可以根据不同的场景应用不同的条件,Okhttp中具体的封装类为CacheControl

//不使用缓存,但是保存缓存数据  Cache-Control: no-cache
fun noCache() = apply {
      this.noCache = true
    }
//不使用缓存,也不保存缓存数据    Cache-Control: no-store  
fun noStore() = apply {
      this.noStore = true
    }
//只使用缓存     Cache-Control: only-if-cached
fun onlyIfCached() = apply {
      this.onlyIfCached = true
    }
// 指定时间之内的响应可以被使用 Cache-Control: max-age=10    
fun maxAge(maxAge: Int, timeUnit: TimeUnit) = apply {
      require(maxAge >= 0) { "maxAge < 0: $maxAge" }
      val maxAgeSecondsLong = timeUnit.toSeconds(maxAge.toLong())
      this.maxAgeSeconds = maxAgeSecondsLong.clampToInt()
    }
//超出指定时间的响应可以被接收  Cache-Control: max-stale=10    
fun maxStale(maxStale: Int, timeUnit: TimeUnit) = apply {
      require(maxStale >= 0) { "maxStale < 0: $maxStale" }
      val maxStaleSecondsLong = timeUnit.toSeconds(maxStale.toLong())
      this.maxStaleSeconds = maxStaleSecondsLong.clampToInt()
    }
//位于当前时间+指定的时间之内的响应可以被接收    
fun minFresh(minFresh: Int, timeUnit: TimeUnit) = apply {
      require(minFresh >= 0) { "minFresh < 0: $minFresh" }
      val minFreshSecondsLong = timeUnit.toSeconds(minFresh.toLong())
      this.minFreshSeconds = minFreshSecondsLong.clampToInt()
    }

通过 CacheControl 设置了缓存条件后,后被添加在Header的头部,字段为 Cache-Control

Okhttp中对缓存的实现依照了HTTP中的首部字段 Cache-Control 具体的细节以及字段的含义推荐 图解HTTP

缓存策略

Okhttp中通过 CacheStrategy 来设置缓存策略,缓存拦截器中缓存黁策略的生成与Request和缓存的Response有关。

class CacheStrategy internal constructor(
  // null表示禁止使用网络
  val networkRequest: Request?,
  // null表示禁止使用缓存
  val cacheResponse: Response?
) 

在CacheStrategy中影响到缓存策略的因素有:

  1. Request中HTTP和缓存条件。
  2. 缓存Response中code和headers中的头部字段:Date、Expires、Last-Modified、ETag、Age。

最终会得到CacheStrategy的两个成员变量 networkRequestcacheResponse

CacheInterceptor拦截器

缓存拦截器的步骤主要分为以下六步:

  1. 尝试通过request去获取缓存。
Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;
  1. 若是禁止网络请求且缓存为null,则直接返回一个code=504的响应Response并返回(504对应Cache-Control: only-if-cached,表明只使用缓存)。
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();
    }
  1. 若是禁止网络请求,但是有缓存,则直接返回缓存。
if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }
  1. 调用下一层拦截器,获取响应。
Response networkResponse = null;
networkResponse = chain.proceed(networkRequest);
  1. 如果缓存不为null,网络请求返回的code=304,则直接使用缓存。
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;
      }
    }
  1. 使用网络请求返回的Response,并缓存。
Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

缓存的细节

缓存拦截器中对于缓存的操作可以分为获取缓存,缓存策略,存缓存。

获取缓存

缓存拦截器中首先就是获取缓存,在cache!=null的情况下,会调用Cache的get方法获取缓存,我们首先来看在什么情况下cache!=null。

public CacheInterceptor(InternalCache cache) {
    this.cache = cache;
  }
interceptors.add(new CacheInterceptor(client.internalCache()));
InternalCache internalCache() {
    return cache != null ? cache.internalCache : internalCache;
  }

可以看到默认会获取OkhttpClient的中的InternalCache对象,该对象是一个接口,默认创建的OkhttpClient并不会创建InternalCache对象,所以不存在缓存。如果需要加入缓存可以通过下面的方法添加:

OkHttpClient okHttpClient= new OkHttpClient.Builder()
                .cache(new Cache(getCacheDir(),1024))
                ......
                .build();

通过cache方法来添加一个Cache对象,该对象内部实现了InternalCache接口。在Cache的构造函数中需要加入缓存的路径,和缓存的字节最大值,Cache中通过DiskLruCache来实现缓存。调用InternalCache的get方法获取缓存值时,会回调到Cache的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;
  }

根据request中的url来获取缓存值,并返回一个Response对象。Okhttp中利用Okio这个来对输入输出流操作做了优化。

缓存策略

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

根据time、request、response来设置缓存策略,用于判断如何使用缓存。

存缓存

在设置了缓存的情况下可以执行存缓存,缓存Response的header和body信息,注意只能存储GET或HEAD请求的缓存信息。

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

小结

默认的OkhttpClient没有缓存,需要我们主动设置。Okhttp中的缓存通过Cache类和DiskLruCache来实现。

缓存拦截器中关于缓存可以分为一下几个步骤:

  1. 通过request去获取缓存,内部通过request的url来获取缓存。
  2. 根据time、request、response来设置缓存策略。
  3. 如是禁止网络请求且缓存为null,创建一个code=504的response返回。
  4. 如果是禁止网络缓存但缓存不为null,则直接使用缓存并返回。
  5. 调用下一拦截器,获取响应。
  6. 如果缓存不为null且网络请求的响应返回的code=304,则使用缓存(有缓存的话客户端会发一个条件性的请求,由服务端告诉是否使用缓存)。
  7. 最后使用请求返回的respone,并存储response(前提是设置了缓存)。

Okhttp中单独实现了DiskLruCache类来实现缓存的存取,具体的代码分析我准备在写一篇单独的文章,和LruCache合并分析。

有问题烦请各位在评论区指正,如果文章对你有帮助,请给我点个小赞,这对我意义重大!

猜你喜欢

转载自blog.csdn.net/Androiddddd/article/details/112253807
今日推荐