OkHttp缓存之简单分析

缓存分类

http请求有服务端和客户端之分。因此缓存也可以分为两个类型服务端侧和客户端侧。

  • 缓存——服务端
    常见的服务端有Ngix和Apache。服务端缓存又分为代理服务器缓存和反向代理服务器缓存(了解就好了)。
  • 缓存——客户端
    客户端很多种,这里就不多说了。主要说说OkHttpClient。缓存其实就是为了下次请求时节省请求时间,可以更快的展示数据
    同时也有更好的用户体验。

常见控制缓存的HTTP头信息

  • Expires策略:在多长时间内是有效的。过了这个时间,缓存器就会向服务器发送请求,检验文档是否被修改。该属性对设置静态图片文件缓存特别有用。
  • Cache-Control策略:一组头信息属性。通过这个属性可以让发布者全面控制内容,并定位过期时间的限制。
  • Last-Modified/If-Modified-Since:Last-Modified/If-Modified-Since要配合Cache-Control使用。
  • ETag:服务器生成的唯一标识符ETag,每次副本的标签都会变化。客户端通过ETag询问服务器端资源是否改变。表示服务器返回的一个资源标识,下次客户端请求时将该值作为 key 为 If-None-Match 的值传给服务器判断,如果ETag没改变,则返回状态304。Etag/If-None-Match,这个也需要配合Cache-Control使用。

下面就说说我对OkHttpClient缓存的理解,如有做得不好之处,请多多指教,谢谢!

不多说直接上代码:

private void testOkHttpCache(){
        Log.e("TAG",mContext.getCacheDir().toString());
        /**
         * OKHTTP如果要设置缓存,首要的条件就是设置一个缓存文件夹,在android中为了安全起见,
         * 一般设置为私密数据空间。getCacheDir()获取。
         */
        //缓存文件夹
        File cacheFile = new File(mContext.getCacheDir().toString(),"cache");
        //缓存大小为10M
        int cacheSize = 10 * 1024 * 1024;
        //创建缓存对象
        final Cache cache = new Cache(cacheFile,cacheSize);

        new Thread(new Runnable() {
            @Override
            public void run() {
                OkHttpClient client = new OkHttpClient.Builder()
                        .cache(cache)//开启缓存
                        .connectTimeout(15,TimeUnit.SECONDS)
                        .readTimeout(15,TimeUnit.SECONDS)
                        .build();

                //缓存设置
                CacheControl cacheControl = new CacheControl.Builder()
                        .maxAge(3*60, TimeUnit.SECONDS)
                        .maxStale(3*60, TimeUnit.SECONDS)
                     // .noCache()//不使用缓存,用网络请求,即使有缓存也不使用
                     // .noStore()//不使用缓存,也不存储缓存
                     // .onlyIfCached()//只使用缓存
                        .build();
                Request request = new Request.Builder()
                        .url("http://img15.3lian.com/2015/h1/280/d/5.jpg")
                        .cacheControl(cacheControl)
                        .build();
                Response response1 =null;
                try {
                    response1 = client.newCall(request).execute();
                 // Log.e("TAG", "testCache: response1 :"+response1.body().string());
                    Log.e("TAG", "testCache: response1 cache :"+response1.cacheResponse());
                    Log.e("TAG", "testCache: response1 network :"+response1.networkResponse());
                    response1.body().close();
                } catch (IOException e) {
                    e.printStackTrace();
                }


                Call call12 = client.newCall(request);
                try {
                    //第二次网络请求
                    Response response2 = call12.execute();
                  //Log.e("TAG", "testCache: response2 :"+response2.body().string());
                    Log.e("TAG", "testCache: response2 cache :"+response2.cacheResponse());
                    Log.e("TAG", "testCache: response2 network :"+response2.networkResponse());
                    Log.e("TAG", "testCache: response1 equals response2:"+response2.equals(response1));
                    response2.body().close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }).start();
 }

运行的结果

07-03 12:15:03.864 17030-17286/com.lu.mystudy E/TAG: testCache: response1 cache :null
07-03 12:15:03.864 17030-17286/com.lu.mystudy E/TAG: testCache: response1 network :Response{protocol=http/1.1, code=200, message=OK, url=http://img15.3lian.com/2015/h1/280/d/5.jpg}
07-03 12:15:04.033 17030-17286/com.lu.mystudy E/TAG: testCache: response2 cache :Response{protocol=http/1.1, code=200, message=OK, url=http://img15.3lian.com/2015/h1/280/d/5.jpg}
07-03 12:15:04.033 17030-17286/com.lu.mystudy E/TAG: testCache: response2 network :null
07-03 12:15:04.033 17030-17286/com.lu.mystudy E/TAG: testCache: response1 equals response2:false

OKHTTP 的缓存原理?

OkHttp缓存,首先得在OkHttpClient#cache配置Cache,即给一个Cache对象。

//创建缓存对象
final Cache cache = new Cache(cacheFile,cacheSize);

本地是否有缓存?

cache 就是在 OkHttpClient.cache(cache) 配置的对象,该对象内部是使用 DiskLruCache 实现的。

底层使用的是 DiskLruCache 缓存机制,从 Cache 的构造中可以验证。源码如下:

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

  Cache(File directory, long maxSize, FileSystem fileSystem) {
    this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
  }

OkHttp的缓存Cache-Control相关头相息

一组头信息属性。通过这个属性可以让发布者全面控制内容,并定位过期时间的限制。

  • 看看如上代码缓存的头信息
http://img15.3lian.com/2015/h1/280/d/5.jpg
GET
0 
HTTP/1.1 200 OK 
9
Content-Type: image/jpeg 
Last-Modified: Sat, 09 Jan 2016 03:03:53 GMT 
Accept-Ranges: bytes 
ETag: "7f90895f8a4ad11:0" 
Server: Microsoft-IIS/8.5 
Date: Mon, 03 Jul 2017 04:15:02 GMT 
Content-Length: 99982 
OkHttp-Sent-Millis: 1499055303788 
OkHttp-Received-Millis: 1499055303850

不难知道OkHttp缓存通过CacheControl类配置,CacheControl指定请求和响应遵循的缓存机制。

CacheControl.java的介绍

  • 两个CacheControl常量
    CacheControl.FORCE_CACHE; //仅仅使用缓存
    CacheControl.FORCE_NETWORK;// 仅仅使用网络

源码如下:

/**
   * Cache control request directives that require network validation of responses. Note that such
   * requests may be assisted by the cache via conditional GET requests.
   */
  public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();

  /**
   * Cache control request directives that uses the cache only, even if the cached response is
   * stale. If the response isn't available in the cache or requires server validation, the call
   * will fail with a {@code 504 Unsatisfiable Request}.
   */
  public static final CacheControl FORCE_CACHE = new Builder()
      .onlyIfCached()
      .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
      .build();
  • 其他属性
    isPublic; 指示响应可被任何缓存区缓存。告诉缓存服务器, 即便是对于不该缓存的内容也缓存起来,比如当用户已经认证的时候。所有的静态内容(图片、Javascript、CSS等)应该是public的。
    isPrivate; 指示对于单个用户的整个或部分响应消息,不能被共享缓存处理。
    noCache();//不使用缓存,用网络请求
    noStore();//不使用缓存,也不存储缓存
    onlyIfCached();//只使用缓存
    noTransform();//禁止转码
    maxAge(10, TimeUnit.MILLISECONDS);//设置超时时间为10ms。
    maxStale(10, TimeUnit.SECONDS);//超时之外的超时时间为10s
    minFresh(10, TimeUnit.SECONDS);//超时时间为当前时间加上10秒钟。

主要是的接入点—— CacheInterceptor#intercept(),源码如下:

从缓存服务请求,并编写对缓存的响应。该拦截器用于处理缓存的功能,主要取得缓存 response 返回并刷新缓存。

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

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

    // If we don't need the network, we're done.
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

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

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

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

CacheStrategy

给定一个请求和缓存响应,这将决定是否使用网络、缓存或两者。内部有两个属性:networkRequest和cacheResponse,在 CacheStrategy 内部会对这个两个属性在特定的情况赋值。

/** The request to send on the network, or null if this call doesn't use the network. */
  public final Request networkRequest;//若是不为 null ,表示需要进行网络请求

  /** The cached response to return or validate; or null if this call doesn't use a cache. */
  public final Response cacheResponse;//若是不为 null ,表示可以使用本地缓存

  private CacheStrategy(Request networkRequest, Response cacheResponse) {
    this.networkRequest = networkRequest;
    this.cacheResponse = cacheResponse;
  }

获取一个CacheStrategy

  • CacheStrategy strategy = new CacheStrategy.Factory(xxx,xxx, xxx).get();
public Factory(long nowMillis, Request request, Response cacheResponse) {
      this.nowMillis = nowMillis;
      this.request = request;
      this.cacheResponse = cacheResponse;

      if (cacheResponse != null) {
        Headers headers = cacheResponse.headers();
        for (int i = 0, size = headers.size(); i < size; i++) {
          String fieldName = headers.name(i);
          String value = headers.value(i);
          if ("Date".equalsIgnoreCase(fieldName)) {
            servedDate = HttpDate.parse(value);
            servedDateString = value;
          } else if ("Expires".equalsIgnoreCase(fieldName)) {
            expires = HttpDate.parse(value);
          } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
            lastModified = HttpDate.parse(value);
            lastModifiedString = value;
          } else if ("ETag".equalsIgnoreCase(fieldName)) {
            etag = value;
          } else if ("Age".equalsIgnoreCase(fieldName)) {
            ageSeconds = HeaderParser.parseSeconds(value, -1);
          } else if (OkHeaders.SENT_MILLIS.equalsIgnoreCase(fieldName)) {
            sentRequestMillis = Long.parseLong(value);
          } else if (OkHeaders.RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) {
            receivedResponseMillis = Long.parseLong(value);
          }
        }
      }
    }

Factory(long nowMillis, Request request, Response cacheResponse)方法中对应的参数 :

  1. nowMillis 当前时间。
  2. request 请求对象。
  3. cacheResponse 从缓存中取出的 Response 对象。

在Factory方法中判断cacheResponse ,如果cacheResponse 对象不为 null ,那么会取出 cacheResponse 对象的头信息,并且将其保存到 CacheStrategy 属性中。
Factory.get 方法内部会通过 getCandidate() 方法获取一个 CacheStrategy,因为关键代码就在 getCandidate() 中。

 /**
     * Returns a strategy to satisfy {@code request} using the a cached response {@code response}.
     */
    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 对象。
    /** Returns a strategy to use assuming the request can use the network. */
    private CacheStrategy getCandidate() {
      // No cached response.
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }

      // Drop the cached response if it's missing a required handshake.
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }

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

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

      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;
      CacheControl responseCaching = cacheResponse.cacheControl();
      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());
      }

      Request.Builder conditionalRequestBuilder = request.newBuilder();

      if (etag != null) {
        conditionalRequestBuilder.header("If-None-Match", etag);
      } else if (lastModified != null) {
        conditionalRequestBuilder.header("If-Modified-Since", lastModifiedString);
      } else if (servedDate != null) {
        conditionalRequestBuilder.header("If-Modified-Since", servedDateString);
      }

      Request conditionalRequest = conditionalRequestBuilder.build();
      return hasConditions(conditionalRequest)
          ? new CacheStrategy(conditionalRequest, cacheResponse)
          : new CacheStrategy(conditionalRequest, null);
    }

当内部的 networkRequest 不为 null,表示需要进行网络请求,若是 cacheResponse 不为表示可以使用缓存,这两个属性是通过 CacheStrategy 构造方法进行赋值的,调用者可以通过两个属性是否有值来决定是否要使用缓存还是直接进行网络请求。
cacheResponse 判空,为空,直接使用网络请求。
isCacheable 方法判断 cacheResponse 和 request 是否都支持缓存,只要一个不支持那么直接使用网络请求。
requestCaching 判断 noCache 和 判断请求头是否有 If-Modified-Since 和 If-None-Match
判断 cacheResponse 的过期时间(包括 maxStaleMillis 的判断),如果没有过期,则使用 cacheResponse。
cacheResponse 过期了,那么如果 cacheResponse 有 eTag/If-None-Match 属性则将其添加到请求头中。

  • CacheStrateg取得对象结果后
    通过cacheCandidate 、cacheResponse作出判断。如果缓存不为空,但是策略器得到的结果是不能用缓存,也就是 cacheResponse 为 null,这种情况就是将 cacheCandidate.body() 进行 close 操作。源码如下:
if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }
  • 被禁止使用网络,而缓存不足,则失败。
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();
    }
  • 当 networkrequest 和 cacheResponse 都不为空,那么进行网络请求。
Response networkResponse = null;
  //进行网络请求。
  networkResponse = chain.proceed(networkRequest);

    //进行了网络请求,但是缓存策略器要求可以使用缓存,那么
    // If we have a cache response too, then we're doing a conditional get.
    if (cacheResponse != null) {
      //validate 方法会校验该网络请求的响应码是否未 304 
      if (validate(cacheResponse, networkResponse)) {
        //表示 validate 方法返回 true 表示可使用缓存 cacheResponse
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .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
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容