okhhtp缓存篇_上

okhhtp缓存篇_上#

注:本文分析的基础是在大概了解了okhhtp的基础上分析的如果不了解的话建议看下okhhtp的网络流程https://blog.csdn.net/wkk_ly/article/details/81004920

前言:okhttp_缓存篇分为上下两部分,主要从以下几个方面来分析okhhtp缓存

  1. okhttp缓存如何声明使用缓存
  2. okhttp在什么情况下缓存网络响应(这个涉及我们更好的使用okhttp缓存)
  3. okhhtp在什么情况下使用缓存而不是使用网络请求(这个涉及我们更好的使用okhttp缓存)
  4. okhhtp如何存储缓存的
  5. okhhtp是如何取出缓存的
  6. 总结okhhtp的缓存&&使用
  7. 附录cache请求(响应)头的各个字段及其含义

上篇分析 1,2,3,

下篇分析 4,5,6,7

如何使用okhhtp缓存

首先还是看下官方实例:

  private final OkHttpClient client;

public CacheResponse(File cacheDirectory) throws Exception {
int cacheSize = 10 * 1024 * 1024; // 10 MiB
//需要指定一个私有目录,以及缓存限制的大小,官方给出的限制是10MB
Cache cache = new Cache(cacheDirectory, cacheSize);

client = new OkHttpClient.Builder()
    .cache(cache)
    .build();
 }

嗯 okhttp实现缓存配置就是你需要在OkHttpClient配置中设置缓存,如上就是设置一个cache 这个cache类需要指定一个目录以及一个缓存的文件大小,但是有个要求就是:
这个缓存目录要是私有的,而且这个缓存目录只能是单个缓存访问否则就会异常,我理解的就是只能是一个应用访问或者说是只能是一个具有相同配置的OkHttpClient访问,

研究缓存前的准备

下面我们来看看okhttp内部是如何实现缓存的,因为okhttp是责任链模式,所以我们直接看okhttp的缓存拦截器,即:CacheInterceptor就可以分析它的缓存实现
注:如果不了解.okhttp的责任链可以看下我的上篇文章https://blog.csdn.net/wkk_ly/article/details/81004920
不过我们需要先做一点准备工作:我们要看下缓存拦截器是如何设置的(换句话说是如何添加进拦截链的)
在RealCall类的getResponseWithInterceptorChain()是设置拦截链并获取网络响应的方法:

Response getResponseWithInterceptorChain() throws IOException {
List<Interceptor> interceptors = new ArrayList<>();
....
//这行代码是设置缓存拦截器,
interceptors.add(new CacheInterceptor(client.internalCache()));
....  
  }

ok我们在这里知道了缓存拦截器的设置,那我们再来看看client的internalCache()方法是什么,点进来后是下面的内容

InternalCache internalCache() {
//这个cache记得我们使用okhttp缓存时设置的代码cache(cache)吗 对这个cache就是我们设置的cache,也就是说我们需要传入的cache的内部实现接口internalCache
//当我们没有设置cache是就会取默认的internalCache,那这个internalCache是什么呢,实际上okhttp.builder有如下的方法
// void setInternalCache(@Nullable InternalCache internalCache) {
//  this.internalCache = internalCache;
//  this.cache = null;
//}
//InternalCache是一个实现缓存策略的接口,就是说我们可以自定义缓存实现方法,只要实现InternalCache接口并设置进来就可以了,但是我们本文的主题是分析okhhtp的缓存策略,
//所以我们在这不在分析自定义的缓存策略
return cache != null ? cache.internalCache : internalCache;
 }

好了我们现在知道了,缓存拦截器需要初始化的时候穿进去换一个缓存实现策略,而这个策略就是我们在设置okhhtp缓存时设置进去的,下面粘贴下缓存策略接口InternalCache,注意我们在之前的设置的Cache类内部是实现了该接口的(他本是并没有实现该接口,而是在内部声明了一个内部类实现了该方法),关于Cache类的源码在此先不粘贴了 我们后面会进行研究讨论,

    public interface InternalCache {
    //根据请求获取缓存响应
  Response get(Request request) throws IOException;
    //存储网络响应,并将原始的请求处理返回缓存请求
  CacheRequest put(Response response) throws IOException;

  /**
   * Remove any cache entries for the supplied {@code request}. This is invoked when the client
   * invalidates the cache, such as when making POST requests.
   */
//当缓存无效的时候移除缓存(),特别是不支持的网络请求方法,okhhtp只支持缓存get请求方法,其他的都不支持,之后我们会在代码中看到的
  void remove(Request request) throws IOException;

  /**
   * Handles a conditional request hit by updating the stored cache response with the headers from
   * {@code network}. The cached response body is not updated. If the stored response has changed
   * since {@code cached} was returned, this does nothing.
   */
  void update(Response cached, Response network);

  /** Track an conditional GET that was satisfied by this cache. */
  void trackConditionalCacheHit();

  /** Track an HTTP response being satisfied with {@code cacheStrategy}. */
  void trackResponse(CacheStrategy cacheStrategy);
}

okhttp在什么情况下缓存网络响应

我们这一小节的标题是分析存储缓存 所以我们忽略其他的部分,我们首先默认我们设置了缓存策略但是此时没有任何缓存存储,这样可以更方便我们分析
那我们直接看缓存拦截器的 拦截方法即是intercept(Chain chain)方法

@Override public Response intercept(Chain chain) throws IOException {
.....
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());
  }
}
...
Response response = networkResponse.newBuilder()
    .cacheResponse(stripBody(cacheResponse))
    .networkResponse(stripBody(networkResponse))
    .build();
//判断是否有缓存策略 我们默认是设置了缓存策略,所此时为true,
if (cache != null) {
    //这个判断是是否有响应体,我们首先假设是正常的请求并且获得正常的响应(关于如何判断是否有响应体,我们稍后研究),所以我们这里主要研究 CacheStrategy.isCacheable(response, networkRequest)
    //这个是判断是否缓存的主要依据
  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;
}

在上面的代码中 我们知道了如果要缓存网络请求主要是有3个依据
1.要有缓存策略(可以是自定义的,也可以是用okhttp提供的Cache),否则不缓存
2,要有响应体,否则不缓存
3,要满足缓存条件否则不缓存,
第一个条件我们都知道,必须设置缓存策略否则肯定不缓存的,那我们重点来看下面这两个条件

要有响应体否则不缓存

字面意思当然是很好理解了,没有网络响应体,的确没有响应体自然没有缓存的必要了 不过我们还是要看下这个判断的(虽然我认为没什么必要),我们看下这个方法

public static boolean hasBody(Response response) {
// HEAD requests never yield a body regardless of the response headers.
//如果网络请求的方法是head方法则返回false,
//Head方法 在服务器的响应中只返回响应头,不会返回响应体
if (response.request().method().equals("HEAD")) {
  return false;
}

int responseCode = response.code();
//HTTP_CONTINUE是100 HTTP_NO_CONTENT是204 HTTP_NOT_MODIFIED是304
//解释看下面(*)
if ((responseCode < HTTP_CONTINUE || responseCode >= 200)
    && responseCode != HTTP_NO_CONTENT
    && responseCode != HTTP_NOT_MODIFIED) {
  return true;
}

// If the Content-Length or Transfer-Encoding headers disagree with the response code, the
// response is malformed. For best compatibility, we honor the headers.
//如果响应头的Content-Length和Transfer-Encoding字段和返回状态码不一致的时候 按照响应头为准
//即是如果响应吗不在上述的要求内,但是响应头又符合又响应体的要求则返回true
//注:Content-Length:这是表示响应体的长度,contentLength(response)也就是获取该字段的值
//Transfer-Encoding:分块传输 就是将响应体分成多个块进行传输(这个也就代表着肯定有响应体,关于详细描述见下面头部字段附录)
if (contentLength(response) != -1
    || "chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
  return true;
}

return false;

}

(*) 我们首先大概描述下http各个响应码

  1. 1XX: 100-199 的状态码代表着信息性状态码
  2. 2xx: 200-299 的状态码代表着成功状态码,但是204状态码代表着没有实体仅仅有响应行和响应头
  3. 3xx: 300-399 的状态码代表着重定向 但是304状态码代表着请求资源未修改,请使用缓存,只有响应行和响应头没有响应体
  4. 4xx: 400-499 的状态码代表着客户端错误代码
  5. 5xx: 500-599 的状态码代表着服务器错误代码

这样我们也就可以理解上面的判断了, 响应吗<100或者响应码>=200,但是响应码!=204而且响应码!=304,
首先 http的响应码是没有100以下 关于小于100的判断是什么作用 暂时没有搞懂 不过这个不影响

我们看完了根据响应码判断是否有响应体的判断,我们接下来看是否符合缓存要求的判断CacheStrategy.isCacheable(response, networkRequest),这个算是我们该小段的重点内容了,

如下为isCacheable()方法:判断是否符合缓存要求

    public static boolean isCacheable(Response response, Request request) {
// Always go to network for uncacheable response codes (RFC 7231 section 6.1),
// This implementation doesn't support caching partial content.
switch (response.code()) {
  case HTTP_OK://200 成功返回状态码
  case HTTP_NOT_AUTHORITATIVE://203 实体首部包含的信息来至与资源源服务器的副本而不是来至与源服务器
  case HTTP_NO_CONTENT://204 只有首部没有实体,但是还记得根据响应码判断是否有响应体的判断吗 那时已经排除了204 和304的状态码
  case HTTP_MULT_CHOICE://300
  case HTTP_MOVED_PERM://301
  case HTTP_NOT_FOUND://404 找不到资源
  case HTTP_BAD_METHOD://405 请求方法不支持
  case HTTP_GONE://410 和404类似 只是服务器以前拥有该资源但是删除了
  case HTTP_REQ_TOO_LONG://414 客户端发出的url长度太长
  case HTTP_NOT_IMPLEMENTED://501 服务器发生一个错误 无法提供服务
  case StatusLine.HTTP_PERM_REDIRECT://308
    // These codes can be cached unless headers forbid it.
    //以上的状态码都可以被缓存除非 首部不允许
    break;

  case HTTP_MOVED_TEMP://302 请求的url已被移除,
  case StatusLine.HTTP_TEMP_REDIRECT://307 和302类似
    // These codes can only be cached with the right response headers.
    // http://tools.ietf.org/html/rfc7234#section-3
    // s-maxage is not checked because OkHttp is a private cache that should ignore s-maxage.
    //这些响应码也可以被缓存但是对应的头部 包含以下条件 需要包含 Expires(资源过期字段) 字段 且不为空
    //或者cacheControl的maxAge
    if (response.header("Expires") != null
        || response.cacheControl().maxAgeSeconds() != -1
        || response.cacheControl().isPublic()
        || response.cacheControl().isPrivate()) {
      break;
    }
    // Fall-through.
//注意此处 默认返回false 也就是说只有符合要求的状态码才可以被缓存 ,这个也就解决了上面我们提出的那个问题 
  default:
    // All other codes cannot be cached.
    return false;
}

// A 'no-store' directive on request or response prevents the response from being cached.
return !response.cacheControl().noStore() && !request.cacheControl().noStore();
 }

为了更好的理解上面的代码 我们先说下上面用到头部字段
下图是我用Fiddler随便抓取的一个网络请求包,我们看红色框中的几个字段


TIM截图20180712154651.png

Expires 指的是请求资源的过期时间源于http1.0
Cache-Control :这个是目前在做缓存时最重要的一个字段,用来指定缓存规则,他有很多的值,不同的值代表着不同的意义,而且这个值是可以组合的 如下图


TIM截图20180712155245.png

说下 Cache-Control各个字段的不同含义(首部字段不区分大小写):

  1. no-cache :在使用该请求缓存时必须要先到服务器验证 写法举例 Cache-Control:no-cache
  2. no-store :不缓存该请求,如果缓存了必须删除缓存 写法举例 Cache-Control:no-store
  3. max-age :该资源有效期的最大时间 写法举例 Cache-Control:max-age=3600
  4. s-maxage :和max-age 类似 不过该字段是用于共享(公共)缓存 写法举例 Cache-Control:s-maxage=3600
  5. private :表明响应只可以被单个用户缓存 不能被代理缓存 写法举例 Cache-Control:private
  6. public :表明响应可以被任何用户缓存 是共享(例如 客户端 代理服务器等等) 写法举例 Cache-Control:public
  7. must-revalidate:缓存必须在使用之前验证旧资源的状态,并且不可使用过期资源。 写法举例 Cache-Control:must-revalidate
  8. max-stale:表明缓存在资源过期后的max-stale指定的时间内还可以继续使用,但是超过这个时间就必须请求服务器 写法举例 Cache-Control:max-stale(代表着资源永不过期) Cache-Control:max-stale=3600(表明在缓存过期后的3600秒内还可以继续用)
  9. min-fresh:最小要保留的新鲜度 ,即是假如缓存设置的最大新鲜时间为max-age=500 最小新鲜度为min-fresh=300 则该缓存的真正的新鲜时间 是max-age-min-fresh=200 也就是说缓存在200秒内有效 超过200秒就必须要请求服务器验证
  10. only-if-cached:如果缓存存在就使用缓存 无论服务器是否更新(无论缓存是否过期) 写法举例 Cache-Control:only-if-cached
  11. no-transform:不得对资源进行转换或转变。Content-Encoding, Content-Range, Content-Type等HTTP头不能由代理修改。例如,非透明代理可以对图像格式进行转换,以便节省缓存空间或者减少缓慢链路上的流量。 no-transform指令不允许这样做。 写法举例 Cache-Control:no-transform
  12. immutable:表示响应正文不会随时间而改变。资源(如果未过期)在服务器上不发生改变,因此客户端不应发送重新验证请求头(例如If-None-Match或If-Modified-Since)来检查更新,即使用户显式地刷新页面 写法举例 Cache-Control:immutable

好了 看完http对Cache-Control 字段的说明 我们再来看下面的代码(CacheInterceptor类中intercept方法)应该大概可以猜测到时是什么意思了,先说下 这里的response是服务器返回的响应,其获取的cacheControl也是服务器响应的头部
这里不对内部代码进行查看了 要不然文章太冗杂了

    //判断是否有首部Expires
 if (response.header("Expires") != null
        //判断 Cache-Control是否含有max-age字段的值 如果没有则为-1
        || response.cacheControl().maxAgeSeconds() != -1
        //判断 Cache-Control是否含有public 默认为false
        || response.cacheControl().isPublic()
        //判断 Cache-Control是否含有private 默认为false
        || response.cacheControl().isPrivate()) {
      break;
    }

所以如果响应码是302或者是307的时候 必须含有首部Expires 或者首部Cache-Control 有max-age public或者是private字段才可以继续往下走

return !response.cacheControl().noStore() && !request.cacheControl().noStore();

这行代码是指如果请求头 或者响应头的Cache-Control 含有no-store 字段则肯定不缓存

走到这里我们看到 okhttp缓存的条件是

1,首先请求头或者响应头的Cache-Control 不能含有no-store(一旦含有必定不缓存)

2,在满足条件1的情况下

(1)如果响应码是是302或307时 必须含有首部Expires 或者首部Cache-Control 有max-age public或者是private字段

(2)响应码为200 203 300 301 308 404 405 410 414 501 时缓存

但是我们继续看下面代码(CacheInterceptor类中intercept方法)

//如果请求方法不是支持的缓存方法则删除缓存
if (HttpMethod.invalidatesCache(networkRequest.method())) {
    try {
      cache.remove(networkRequest);
    } catch (IOException ignored) {
      // The cache cannot be written.
    }
  }

那我们继续看HttpMethod.invalidatesCache(networkRequest.method())方法,如下

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

}

也就是说如果请求方法是 post put delete move patch 则删除请求,我们应该还能想起 前面在判断是否有响应体的时候有这么一行代码

//Head方法 在服务器的响应中只返回响应头,不会返回响应体
if (response.request().method().equals("HEAD")) {
  return false;
}

也就是说缓存也是不支持head方法的

综上 我们可以得到okhttp支持存储的缓存 只能是get方法请求的响应,好了 我们记住这一条结论 后面我们还能得到这一印证(后面我们也会得到为什么缓存只支持get方法的原因)

在这里我们基本上已经完成了okhhtp缓存条件的分析 不过还有一些小细节我们可能没有分析到 我们继续看 缓存存储的方法put(也就是我们前面设置的缓存策略的put方法),这里的cache是个接口 但是它的真正实现是Cache类
所以我们直接看Cache类的 put方法就可以了

 CacheRequest cacheRequest = cache.put(response);

Cache的put方法

@Nullable CacheRequest put(Response response) {
String requestMethod = response.request().method();
//这里再次对请求方法进行检验
if (HttpMethod.invalidatesCache(response.request().method())) {
  try {
    remove(response.request());
  } catch (IOException ignored) {
    // The cache cannot be written.
  }
  return null;
}
//再次验证如果不是get方法则返回,这里也解释了 为什么只缓存get方法的请求而不缓存其他方法的请求响应,
//这里的解释是 技术上可以做到 但是花费的精力太大 而且效率太低 所以再次不做其他方法的缓存
if (!requestMethod.equals("GET")) {
  // Don't cache non-GET responses. We're technically allowed to cache
  // HEAD requests and some POST requests, but the complexity of doing
  // so is high and the benefit is low.
  return null;
}
//此处是检查 响应头部是否含有 * 如果含有* 也不缓存 这里我就不继续分析了 大家可以自己看看 
if (HttpHeaders.hasVaryAll(response)) {
  return null;
}

Entry entry = new Entry(response);
DiskLruCache.Editor editor = null;
try {
  editor = cache.edit(key(response.request().url()));
  if (editor == null) {
    return null;
  }
  entry.writeTo(editor);
  return new CacheRequestImpl(editor);
} catch (IOException e) {
  abortQuietly(editor);
  return null;
}

}


好了 这里我们彻底的完成了 我们这一小节的主题 okhttp在什么情况下缓存网络响应 下面我们总结下我们得到的结论:

1、Okhhtp只缓存Get请求

2、如果请求头或者响应头的Cache-Control 含有no-store 则一定不缓存

3、如果响应头含有*字符则不缓存

4、在满足 1、2、3 的条件下 okhttp对以下的条件进行缓存

(1)响应码是200 203 300 301 308 404 405 410 414 501 时缓存

(2)如果响应码是是302或307时 必须含有首部Expires 或者首部Cache-Control 有max-age public或者是private字段


好了上面我们分析完"okhttp在什么情况下缓存网络响应" 下面我们分析"okhhtp在什么情况下使用缓存而不是使用网络请求"

okhhtp在什么情况下使用缓存而不是使用网络请求

我们还是要研究缓存拦截器,研究的前提自然是我们的请求符合okhhtp存储缓存的要求 而且已经缓存成功了

好了我们先粘出缓存拦截器intercept方法全部内容

 @Override public Response intercept(Chain chain) throws IOException {
//前面我们已经知道了 这个cache就是我们设置的缓存策略
//这里是利用缓存策略根据请求获取缓存的响应(我们的前提是get请求并且已经成功缓存了,所以我们这里成功的获取了缓存响应)
//(1)
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;
  }

我们先根据结果然后推论原因,这样可能更好的理解

首先我们正确获取了缓存的网络响应cacheCandidate 代码位置为(1)

然后初始化得到一个CacheStrategy类 看名字缓存策略大概可以猜到这个是根据传入的请求和缓存响应 判断到底是使用缓存还是进行网络请求(其实我们之前研究存储缓存的时候就已经看过了,这里我们详细研究下)
这里我们详细看下 看下这个类CacheStrategy内部以及这个两个返回值(注:这两个返回值是否为空是缓存策略判断的结果,为空则不支持 不为空则支持)

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

我们先看下 new CacheStrategy.Factory(now, chain.request(), cacheCandidate);也就是CacheStrategy的内部类Factory的构造方法

public Factory(long nowMillis, Request request, Response cacheResponse) {
  this.nowMillis = nowMillis;
//这里将传入的网络请求和缓存响应设置为Factory内部成员
  this.request = request;
  this.cacheResponse = cacheResponse;
    //cacheResponse不为空 所以执行
  if (cacheResponse != null) {
    this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
    this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
    //这里主要是解析缓存响应头部 并将解析到的字段赋值给Factory的成员变量 这里主要是获取和缓存相关的字段
    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 = HttpHeaders.parseSeconds(value, -1);
      }
    }
  }
}

上面的代码主要是解析缓存头部字段 并存储在Factory类的成员变量中,下面解释下上面需要解析的和缓存相关的头部字段(首部 不区分大小写)

  1. Age 告诉接受端 响应已经产生了多长时间(这个是和max-age一起实现缓存的),单位是秒
  2. Date 报文创建的时间和日期(响应和报文产生的时间是不一样的概念 时间也不一定相等)
  3. ETag 报文中包含的实体(响应体)的标志 实体标识是标志资源的一种方式,用老确定资源是否有修改
  4. Last-Modified 实体最后一次呗修改的时间 举例说明 : Last-Modified : Fri , 12 May 2006 18:53:33 GMT

我们会继续看Factory的get方法

//获取CacheStrategy实例
public CacheStrategy get() {
//获取CacheStrategy实例
  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(),这个方法实现了基本上的缓存策略
---注:为了大家更好的理解下面的缓存策略,我先说下okhttp缓存判断依据结论 后面我们再做验证

这里是CacheStrategy的构造方法
CacheStrategy(Request networkRequest, Response cacheResponse) {
this.networkRequest = networkRequest;
this.cacheResponse = cacheResponse;
 }

重要这里说下缓存判断依据

1如果networkRequest为null cacheResponse不为null 则使用缓存

2如果networkRequest不为null cacheResponse为null 则进行网络请求

3其他情况我们下面再解释 我们先记住这2个结论 这个能更好帮助我们理解下面的缓存策略判断 大家可以先看这一小节的结论好 请求头的注释以及说明 然后再看下面的缓存策略 这样可能更好理解些(当然如果对http的Cache
比较了解的话,则不必了)

 private CacheStrategy getCandidate() {
  // No cached response.
    //如果缓存为空 则初始化一个CacheStrategy返回 
  if (cacheResponse == null) {
    return new CacheStrategy(request, null);
  }
    //如果请求方式是https而且缓存的没有TLS握手(注:这个我没有深入的研究 如果大家有结论可以告诉我)
  // 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.
    //此时再次判读是否应该缓存 如果不应该缓存则将cacheResponse设为null
  if (!isCacheable(cacheResponse, request)) {
    return new CacheStrategy(request, null);
  }
    //获取请求的CacheControl字段的信息类
  CacheControl requestCaching = request.cacheControl();
    //如果请求含有nocache字段并且如果请求头部含有If-Modified-Since或者If-None-Match字段(下面会介绍这些条件缓存字段的含义) 则将cacheResponse设为null
    //hasConditions(request)的方法详情如下:
    //private static boolean hasConditions(Request request) {
    // return request.header("If-Modified-Since") != null || request.header("If-None-Match") != null;
    //}
  if (requestCaching.noCache() || hasConditions(request)) {
    return new CacheStrategy(request, null);
  }
//获取缓存响应的头部Cache-Control字段的信息
  CacheControl responseCaching = cacheResponse.cacheControl();
//Cache-Control字段是否含有immutable  前面我们已经说过immutable 是表明 响应不会随着时间而变化  这里将netrequest设置为null 并return(意思是使用缓存)
  if (responseCaching.immutable()) {
    return new CacheStrategy(null, cacheResponse);
  }
//获取缓存里面响应的age 注意该age不是响应的真实age
  long ageMillis = cacheResponseAge();
//获取缓存响应里声明的新鲜时间,如果没有设置则为0
  long freshMillis = computeFreshnessLifetime();
    //如果请求中的含有最大响应新鲜时间 则和缓存中的最大响应新鲜时间进行比较 取最小值并赋值给freshMillis
  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;
//判断Cache-Control 是否含有must-revalidate字段 该字段我们前面说到了是 使用缓存前必须对资源进行验证 不可使用过期时间,换句话说如果设置该字段了 那过期后还能使用时间就没有意义了
  if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
    maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
  }
//如果缓存头部Cache-Control 不含有nocache,并且 (缓存响应年龄+最小新鲜时间)<(响应新鲜时间+响应过期后还可用的时间) 则使用缓存,不过根据响应时间还要加上一些警告 Warning
  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;
    //如果响应年龄大于一天 并且响应使用启发式过期时间
    //我们看下isFreshnessLifetimeHeuristic()就知道什么是启发式过期了
    //private boolean isFreshnessLifetimeHeuristic() {
    //return cacheResponse.cacheControl().maxAgeSeconds() == -1 && expires == null;
    //}
    //好吧 启发式过期 就是没有设置获取过期时间字段
    //如果是这样的 则需要添加警告头部字段 这是试探性过期(不过如果使用缓存的话 不建议这么做)
    //关于试探性过期 我们下面介绍 (稍安勿躁) 因为篇幅较多 这些写不下
    if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
      builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
    }
    return new CacheStrategy(null, builder.build());
  }

  // 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;
    //缓存响应头部 lastModified 最后一次修改时间
  } else if (lastModified != null) {
    conditionName = "If-Modified-Since";
    conditionValue = lastModifiedString;
    //缓存响应头部 servedDate Date字段的值 就是 原始服务器发出响应时的服务器时间
  } else if (servedDate != null) {
    conditionName = "If-Modified-Since";
    conditionValue = servedDateString;
  } else {
    //如果上述字段都没有则发出原始的网络请求 不使用缓存
    return new CacheStrategy(request, null); // No condition! Make a regular request.
  }
    //如果上述的条件之一满足 则添加条件头部 返回含有条件头部的请求和缓存响应
    //复制一份和当前request的header
  Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
    //在header中添加条件头部, 注:Internal是个抽象方法 该方法的唯一实现是在okhhtpclient的内部实现 okhhtpclient本身没有实现它
  Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);

  Request conditionalRequest = request.newBuilder()
        //删除原有的所有header 将现在的header添加上 注意上面已经说过了 添加的header 是复制原来的header 并在它的基础上添加一个条件头部
      .headers(conditionalRequestHeaders.build())
      .build();
//返回处理过的 请求和 缓存响应
  return new CacheStrategy(conditionalRequest, cacheResponse);
}

okhhtp的缓存策略分析完了 但是大家可能对上面出现的头部字段和条件字段 以及新鲜度什么的比较迷糊,这里我们先说下这些东西 也好大家更好的理解

好吧我们先说下新鲜度( 这里说明:以下所说的服务器都是源服务器就是最初发出响应报文的服务器,代理服务器会特别说明是代理服务器)

http为了实现缓存 提出了新鲜度的概念 那什么是新鲜度呢?比如一个西红柿 刚从地里摘下拿回家放着,那它就是新鲜的,而且是最新鲜,那过了3天后 这个西红柿坏掉了 那它就是不新鲜的了 不能用了,
从地里摘掉到坏掉的这3天时间内 就是他的新鲜时间,这个在http中 土地相当于服务器,家里存放相当于缓存,客户端使用就相当于要吃掉这个西红柿

好了新鲜度的大概意思我们了解了 我们在详细的说说 http针对新鲜度设置的各个头部
age :年龄 从响应产生的时间(单独字段 不属于Cache-Control)
max-age:最大年龄 响应可以产生的最大年龄 只看该字段的话 我们可以说 该响应的新鲜时间是max-age 也就是说只要 age<max-age 该响应就是新鲜的 是可以用的不用请求服务器客户已直接使用缓存(属于 Cache-Control的命令)
上面2个字段在计算新鲜度时具有最高优先级
expires :过期时间 这个字段是说明响应过期的时间点(单独字段 不属于Cache-Control)
这个字段的优先级其次
date :日期 这个是指服务器产生响应的时间
注:如果上面3个字段都没有设置 可以用这个字段计算新鲜时间 这个时间也叫做试探性过期时间 计算方式 采用LM_Factor算法计算
方式如下 time_since_modify = max(0,date-last-modified); 
        freshness_time = (int)(time_since_modify*lm_factor);在okhttp中这个lm_factor值是10%,就是0.1;
详细解释下:time_since_modify =  max(0,date - last-modified) 就是将服务器响应时间(date) 减去 服务器最后一次修改资源的时间(last-modified) 得到一个时间差 用这个时间差和0比较取最大值得到time_since_modify,
freshness_time = (int)(time_since_modify*lm_factor); 前面我们已经得到time_since_modify 这里我们取其中一小段时间作为过期时间,lm_factor就是这一小段的比例,在okhttp中比例是10%
注:date时间比修改使劲越大说明资源修改越不频繁(新鲜度越大) ,

在前面分析缓存策略的时候有这么一行代码 我当时的解释是获取新鲜度 但是并没有详细的分析这个方法,这里我们进去看看 这个计算新鲜度的算法是否和我们上面分析的一致
long freshMillis = computeFreshnessLifetime();

computeFreshnessLifetime()方法如下:
    
 private long computeFreshnessLifetime() {
  CacheControl responseCaching = cacheResponse.cacheControl();
    //这个是我们前面说的 max-age是优先级最高的计算新鲜时间的方式 和我们说的一致
  if (responseCaching.maxAgeSeconds() != -1) {
    return SECONDS.toMillis(responseCaching.maxAgeSeconds());
    //当没有max-age字段时 使用expires - date 获取新鲜度时间 和我们说的也一致
  } else if (expires != null) {
    long servedMillis = servedDate != null
        ? servedDate.getTime()
        : receivedResponseMillis;
    long delta = expires.getTime() - servedMillis;
    return delta > 0 ? delta : 0;
    //当上述2个字段都不存在时 进行试探性过期计算 还是和我们说的一致
  } else if (lastModified != null
      && cacheResponse.request().url().query() == null) {
    // As recommended by the HTTP RFC and implemented in Firefox, the
    // max age of a document should be defaulted to 10% of the
    // document's age at the time it was served. Default expiration
    // dates aren't used for URIs containing a query.
    long servedMillis = servedDate != null
        ? servedDate.getTime()
        : sentRequestMillis;
    long delta = servedMillis - lastModified.getTime();
    return delta > 0 ? (delta / 10) : 0;
  }
    //当上述方式3种方式都不可取时返回新鲜时间0 也就是说不能获取缓存 一定要进行网络请求
  return 0;
}

好了经过上面的讨论 我们知道了新鲜度 以及如何计算一个响应的新鲜时间,接下来缓存真正的可用的时间 上面我们看到了Cache-control的这两个字段

min-fresh:最小要保留的新鲜度 ,即是假如缓存设置的最大新鲜时间为max-age=500 最小新鲜度为min-fresh=300 则该缓存的真正的新鲜时间 是max-age-min-fresh=200 也就是说缓存在200秒内有效 超过200秒就必须要请求服务器验证

max-stale:缓存过期后还可以用的时间

也就是缓存真正的可用的新鲜时间是 realFreshTime =(freshTime+max-stale) - (min-fresh);

现在我们应该对上面分析的缓存策略更明白点了,下面我们分析上面我们说过的 缓存的命令头部

  1. If-Modify-since:date , 条件判断 如果在date时间之后服务器没有修改该资源 就返回304 并且在头部添加新的资源过期时间,如果修改了则返回200并且正确返回正确的请求资源以及必要响应头部,注意这个date我们一般设置为服务器上次修改资源的时间即是:Last-Modified,因为有些服务器在处理这个条件缓存判断是是将该时间当做字符串和上次修改的时间进行比对,如果相同则返回304否则返回200,不过这也违反了该条件字段设计的初衷,
  2. If-None-Match:ETag ,条件判断 ,我们前面解释了什么是ETag,即实体标识,如果客户端发出的请求带有这个条件判断,服务器会根据服务器当前的ETag和客户端给出的ETag进行比对如果相同,则返回304即是服务器资源没变,继续使用缓存,如果不相同,即是服务器资源发生改变,这是服务器返回200,以及新的ETag实体标识,还有请求的资源(就是当做正常请求一样),

我们现在将okhttp涉及到的http知识都说完了 那么我们继续上面的分析

上面我们分析完 private CacheStrategy getCandidate() {} 我之前说过这个是okhttp缓存策略的真正体现 那么我们对getCandidate()方法从头到尾的总结一遍

  1. 首先根据缓存的响应码判断是否可以缓存 不可以返回 cacheResponse为null
  2. 上述条件都不满足 判断请求头部是否含有no-cache以及头部是否含有条件头部If-Modify-since或者If-None-Match,三个条件满足一个 cacheResponse为null 返回
  3. 上述条件都不满足 判断cache-control 是否有immutable 有的话 使用缓存 networkRequest为null 返回
  4. 上述条件都不满足 计算age 最小新鲜时间minFresh 新鲜时间Fresh 过期可使用时间stale 如果age+minfresh<fresh+stale 说明当前响应满足新鲜度 networkRequest为null 返回
  5. 判断缓存存储的响应是否含有响应头ETag,date,Last-Modified其中的一个或多个字段如果没有则返回 cacheResponse为null 返回 ,如果有,则先判断ETag是否存在如果存在 则在networkRequest请求头部添加If-None-Match:ETag,返回(cacheResponse,networkRequest)都不为空,如果ETag不存在 则date,Last-Modified(Last-Modified优先级大于date)是否存在,存在则在networkRequest请求头部添加If-Modify-since:date并返回

上面就是getCandidate()的全部流程,我们继续分析public CacheStrategy get() {}方法

public CacheStrategy get() {
  CacheStrategy candidate = getCandidate();
    //就是如果缓存策略要进行网络请求,但是请求中又设置要使用缓存则将cacheResponse,networkRequest全部为null,好了这个判断我们可以在上面分析getCandidate()方法中加上一条
    //6 ,就是如果缓存策略要进行网络请求,但是请求中又设置要使用缓存onlyIfCache则将cacheResponse,networkRequest全部为nul
  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;
}

到此我们算是彻底的完成了CacheStrategy的策略分析 那我们继续回到okhttp的网络缓存拦截器CacheInterceptor分析我们没有分析完的代码

 @Override public Response intercept(Chain chain) throws IOException {

....
//我们上面已经分析完CacheStrategy的初始化方法 也知道了networkRequest,cacheResponse为空的条件和含义
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.
//此处就是我们上面总结的第六条,如果我们想要进行网络请求但是却在cache-control设置了 only-if-cache(如果有缓存就使用缓存),则返回一个空的响应体并且响应码是504
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为null 我们就将缓存的响应返回,也就是我们使用了缓存,到此我们的分析算是大部分结束了,
// If we don't need the network, we're done.
if (networkRequest == null) {
  return cacheResponse.newBuilder()
      .cacheResponse(stripBody(cacheResponse))
      .build();
}

Response networkResponse = null;
try {
    //进行网络请求获取网络响应 但是需要注意的是这个networkRequest是经过okhhtp改变过的 就是可能加上了条件缓存头字段
    //所以这个请求可能是304 就是服务器资源没有改变,没有返回响应体
  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.
//如果cacheResponse不为null
if (cacheResponse != null) {
//如果网络响应码是304的话 则返回缓存响应并更新缓存(如果缓存头部和请求得到的头部不一致则使用请求得到的头部)
  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());
  }
}
......
}   

到此我们这一小节的分析彻底的结束了,下面我们总结下整个流程


++++++++++++++++++++++这个很重要+++++++++++++++++++++++++++++++++
我们总结下okhhtp在什么请求下使用缓存

  1. 首先我们要设置缓存策略Cache,(否则肯定不会缓存响应,当然也不会使用缓存响应,也不会设置缓存条件头部, 关于这个缓存策略我们也可以定义自己实现InternalCache接口 然后在okhttpclient.build中设置,注意自定义的缓存策略和Cache只能有一个,) 如果没有设置返回 ->如果cachecontrol 设置了only-if-cached则返回504 没有设置则进行网络请求,
  2. 判断缓存中是否含有该网络请求的缓存如果没有返回->如果cachecontrol 设置了only-if-cached则返回504 没有设置则进行网络请求,
  3. 判断请求方法是否支持,如果不支持返回->如果cachecontrol 设置了only-if-cached则返回504 没有设置则进行网络请求,
  4. 判断请求头cache-control是否含有nocache,如果有返回->如果cachecontrol 设置了only-if-cached则返回504 没有设置则进行网络请求,
  5. 判断请求头是否含有条件判断字段If-Modified-Since或者是If-None-Match,如果有返回->如果cachecontrol 设置了only-if-cached则返回504 没有设置则进行网络请求,
  6. 判断请求头cache-control是否含有immutable字段 如果有则返回缓存
  7. 判断缓存新鲜度是否满足 满足返回缓存
  8. 判断请求头部是否含有Date,ETag,Last-Modified 其中一个或者多个字段 没有则返回->如果cachecontrol 设置了only-if-cached则返回504 没有设置则进行网络请求
  9. 根据ETag>Last-Modified>Date的优先级生成 If-None-Match:ETag>If-Modified-Since:Last-Modified>If-Modified-Since:Date的条件验证字段,但是只能生成一个 返回 >如果cachecontrol 设置了only-if-cached则返回504 没有设置则进行网络请求,
  10. 含有条件首部的网络请求返回304的时候返回缓存,并更新缓存

注:
1.上述判断的执行顺序是从1到10只有上面的条件满足才可以执行下面的逻辑

2.上述的3中请求方法只支持get和head方法

3.详细上述的7中的情况我在上面已经解释了 不明白可以往上翻翻

okhttp整个缓存流程图如下:

okhttp缓存流程图.jpg

++++++++++++++++++++++这个很重要+++++++++++++++++++++++++++++++++


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349