OKHTTP拦截器缓存策略CacheInterceptor的简单分析

OKHTTP异步和同步请求简单分析
OKHTTP拦截器缓存策略CacheInterceptor的简单分析
OKHTTP拦截器ConnectInterceptor的简单分析
OKHTTP拦截器CallServerInterceptor的简单分析
OKHTTP拦截器BridgeInterceptor的简单分析
OKHTTP拦截器RetryAndFollowUpInterceptor的简单分析
OKHTTP结合官网示例分析两种自定义拦截器的区别

为什么需要缓存 Response?

  • 客户端缓存就是为了下次请求时节省请求时间,可以更快的展示数据。
  • OKHTTP 支持缓存的功能

HTTP 中几个常见的缓存相关的头信息

  • Expire 一般会放在响应头中,表示过期时间
    Expires: Thu, 12 Jan 2017 11:01:33 GMT
  • Cache-Control 表示缓存的时间 max-age = 60 表示可以缓存 60s
  • e-Tag 表示服务器返回的一个资源标识,下次客户端请求时将该值作为 key 为 If-None-Match 的值传给服务器判断,如果ETag没改变,则返回状态304。
  • Last-Modified 在浏览器第一次请求某一个URL时,服务器端的返回状态会是200,内容是你请求的资源,同时有一个Last-Modified的属性标记此文件在服务期端最后被修改的时间,格式类似这样:Last-Modified:Tue, 24 Feb 2009 08:01:04 GMT,第二次请求浏览器会向服务器传送If-Modified-Since,询问该时间之后文件是否有被修改过,如果资源没有变化,则自动返回HTTP304状态码,内容为空,这样就节省了传输数据量。

示例代码

String url = "http://www.imooc.com/courseimg/s/cover005_s.jpg";

//配置缓存的路径,和缓存空间的大小
Cache cache = new Cache(new File("/Users/zeal/Desktop/temp"),10*10*1024);

OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .readTimeout(15, TimeUnit.SECONDS)
                .writeTimeout(15, TimeUnit.SECONDS)
                .connectTimeout(15, TimeUnit.SECONDS)
                //打开缓存
                .cache(cache)
                .build();

final Request request = new Request.Builder()
                .url(url)
                //request 请求单独配置缓存策略
                //noCache(): 就算是本地有缓存,也不会读缓存,直接访问服务器
                //noStore(): 不会缓存数据,直接访问服务器
                //onlyIfCached():只请求缓存中的数据,不靠谱
                .cacheControl(new CacheControl.Builder().build())
                .build();
Call call = okHttpClient.newCall(request);

Response response = call.execute();
//读取数据
response.body().string();

System.out.println("network response:"+response.networkResponse());
System.out.println("cache response:"+response.cacheResponse());

//在创建 cache 开始计算
System.out.println("cache hitCount:"+cache.hitCount());//使用缓存的次数
System.out.println("cache networkCount:"+cache.networkCount());//使用网络请求的次数
System.out.println("cache requestCount:"+cache.requestCount());//请求的次数


//第一次的运行结果(没有使用缓存)
network response:Response{protocol=http/1.1, code=200, message=OK, url=http://www.imooc.com/courseimg/s/cover005_s.jpg}
cache response:null
cache hitCount:0
cache networkCount:1
cache requestCount:1
//第二次的运行结果(使用了缓存)
network response:null
cache response:Response{protocol=http/1.1, code=200, message=OK, url=http://www.imooc.com/courseimg/s/cover005_s.jpg}
cache hitCount:1
cache networkCount:0
cache requestCount:1

OKHTTP 的缓存原理?

  • 底层使用的是 DiskLruCache 缓存机制,这一点可以从 Cache 的构造中可以验证。
Cache(File directory, long maxSize, FileSystem fileSystem) {
     this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
}
缓存文件.png
050ddcd579f740670cf782629b66eb92.0
//缓存响应的头部信息
http://www.qq.com/
GET
1
Accept-Encoding: gzip
HTTP/1.1 200 OK
14
Server: squid/3.5.20
Date: Sun, 02 Jul 2017 02:54:01 GMT
Content-Type: text/html; charset=GB2312
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
Expires: Sun, 02 Jul 2017 02:55:01 GMT
Cache-Control: max-age=60
Vary: Accept-Encoding
Content-Encoding: gzip
Vary: Accept-Encoding
X-Cache: HIT from nanjing.qq.com
OkHttp-Sent-Millis: 1498964041246
OkHttp-Received-Millis: 1498964041330


050ddcd579f740670cf782629b66eb92.1
该文件缓存的内容是请求体,都是经过编码的,所以就不贴出来了。
  • 缓存的切入点 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(EMPTY_BODY)
          .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 (validate(cacheResponse, networkResponse)) {
        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;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }

    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    if (HttpHeaders.hasBody(response)) {
      CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
      response = cacheWritingResponse(cacheRequest, response);
    }

    return response;
  }
  • 从本地中寻找是否有缓存?

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

Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

CacheStrategy

它是一个策略器,负责判断是使用缓存还是请求网络获取新的数据。内部有两个属性:networkRequest和cacheResponse,在 CacheStrategy 内部会对这个两个属性在特定的情况赋值。

  • networkRequest:若是不为 null ,表示需要进行网络请求
/** The request to send on the network, or null if this call doesn't use the network. */
  public final Request networkRequest;
  • cacheResponse:若是不为 null ,表示可以使用本地缓存
/** The cached response to return or validate; or null if this call doesn't use a cache. */
  public final Response cacheResponse;

得到一个 CacheStrategy 策略器

cacheCandidate它表示的是从缓存中取出的 Response 对象,有可能为null(在缓存为空的时候),在 new CacheStrategy.Factory 内部如果 cacheCandidate 对象不为 null ,那么会取出 cacheCandidate 的头信息,并且将其保存到 CacheStrategy 属性中。

CacheStrategy strategy = new CacheStrategy.Factory(now, 
chain.request(), cacheCandidate).get();
public Factory(long nowMillis, Request request, Response cacheResponse) {
  this.nowMillis = nowMillis;
  this.request = request;
  this.cacheResponse = cacheResponse;
  //在 cacheResponse 缓存不为空的请求,将头信息取出。
  if (cacheResponse != null) {
    this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
    this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
    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);
      }
    }
  }
}
  • get() 方法获取一个 CacheStrategy 对象。

在 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 对象。当内部的 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 属性则将其添加到请求头中。
/** 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());
  }

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

策略器得出结果之后

  • 如果缓存不为空,但是策略器得到的结果是不能用缓存,也就是 cacheResponse 为 null,这种情况就是将 cacheCandidate.body() 进行 close 操作。
if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }
  • networkRequest 为空,表示不能进行网络请求,但是 cacheResponse 不为空,可以使用缓存中的 cacheResponse。
   // If we don't need the network, we're done.
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .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());
      }
    }
  • invalite

校验是使用缓存中的 response 还是使用网络请求的 response
当返回 true 表示可以使用 缓存中的 response 当返回 false 表示需要使用网络请求的 response。

/**
 * Returns true if {@code cached} should be used; false if {@code network} response should be
 * used.
 */
private static boolean validate(Response cached, Response network) {
  //304 表示资源没有发生改变,服务器要求客户端继续使用缓存
  if (network.code() == HTTP_NOT_MODIFIED) return true;
  // The HTTP spec says that if the network's response is older than our
  // cached response, we may return the cache's response. Like Chrome (but
  // unlike Firefox), this client prefers to return the newer response.
  Date lastModified = cached.headers().getDate("Last-Modified");
  if (lastModified != null) {
    Date networkLastModified = network.headers().getDate("Last-Modified");
    //在缓存范围内,因此可以使用缓存
    if (networkLastModified != null
        && networkLastModified.getTime() < lastModified.getTime()) {
      return true;
    }
  }
  //表示不可以使用缓存
  return false;
}
  • 使用网络请求回来的 networkResponse

当缓存 cacheResponse 不可用时或者为空那就直接使用网络请求回来的 networkResponse。

Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

缓存 response

if (HttpHeaders.hasBody(response)) {
      CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
      response = cacheWritingResponse(cacheRequest, response);
    }
  • maybeCache

CacheStrategy.isCacheable 通过该方法判断是否支持缓存。
HttpMethod.invalidatesCache 通过该方法判断该请求是否为 GET 请求。

private CacheRequest maybeCache(Response userResponse, Request networkRequest,
    InternalCache responseCache) throws IOException {
  if (responseCache == null) return null;
  // Should we cache this response for this request?
  if (!CacheStrategy.isCacheable(userResponse, networkRequest)) {
    if (HttpMethod.invalidatesCache(networkRequest.method())) {
      try {
        responseCache.remove(networkRequest);
      } catch (IOException ignored) {
        // The cache cannot be written.
      }
    }
    return null;
  }
  // Offer this request to the cache.
  return responseCache.put(userResponse);
}
  • responseCache.put(userResponse);
    • 通过 DiskLruCache 将响应头信息写入到磁盘中。
      entry.writeTo(editor);
    • 将响应体写入到磁盘中。new CacheRequestImpl(editor)

该方法是 Cache 中的方法,负责将 userResponse 缓存到本地。

private 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;
  }
  //OKHTTP 只支持 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(urlToKey(response.request()));
    if (editor == null) {
      return null;
    }  
    //通过 DiskLruCache 将响应头信息写入到磁盘中。
    entry.writeTo(editor);
    //将响应体写入到磁盘中。
    return new CacheRequestImpl(editor);
  } catch (IOException e) {
    abortQuietly(editor);
    return null;
  }
}
  • 写入头信息
public void writeTo(DiskLruCache.Editor editor) throws IOException {
  BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));
  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');
  if (isHttps()) {
    sink.writeByte('\n');
    sink.writeUtf8(handshake.cipherSuite().javaName())
        .writeByte('\n');
    writeCertList(sink, handshake.peerCertificates());
    writeCertList(sink, handshake.localCertificates());
    // The handshake’s TLS version is null on HttpsURLConnection and on older cached responses.
    if (handshake.tlsVersion() != null) {
      sink.writeUtf8(handshake.tlsVersion().javaName())
          .writeByte('\n');
    }
  }
  sink.close();
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  • 大家好,之前我们讲解了Okhttp网络数据请求相关的内容,这一节我们讲讲数据缓存的处理。本节按以下内容讲解Okht...
    Ihesong阅读 10,124评论 6 26
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,544评论 25 707
  • 前言 在Android开发中我们经常要进行各种网络访问,比如查看各类新闻、查看各种图片。但有一种情形就是我们每次重...
    SnowDragonYY阅读 6,180评论 2 11
  • 中午去万达吃饭,突然听到一声刺耳的尖叫声,宝贝立刻抱紧了我,幸好没有被吓哭。 我一边捂住宝贝的耳朵,不忘好奇的往楼...
    青柠檬静语阅读 620评论 2 3