OKHTTP攔截器緩存策略CacheInterceptor的簡單分析

OKHTTP異步和同步請求簡單分析
OKHTTP攔截器緩存策略CacheInterceptor的簡單分析
OKHTTP攔截器ConnectInterceptor的簡單分析
OKHTTP攔截器CallServerInterceptor的簡單分析
OKHTTP攔截器BridgeInterceptor的簡單分析
OKHTTP攔截器RetryAndFollowUpInterceptor的簡單分析
OKHTTP結合官網示例分析兩種自定義攔截器的區(qū)別

為什么需要緩存 Response益兄?

  • 客戶端緩存就是為了下次請求時節(jié)省請求時間竖螃,可以更快的展示數(shù)據(jù)。
  • 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沒改變葡兑,則返回狀態(tài)304没咙。
  • Last-Modified 在瀏覽器第一次請求某一個URL時缨历,服務器端的返回狀態(tài)會是200以蕴,內容是你請求的資源,同時有一個Last-Modified的屬性標記此文件在服務期端最后被修改的時間辛孵,格式類似這樣:Last-Modified:Tue, 24 Feb 2009 08:01:04 GMT丛肮,第二次請求瀏覽器會向服務器傳送If-Modified-Since,詢問該時間之后文件是否有被修改過魄缚,如果資源沒有變化宝与,則自動返回HTTP304狀態(tài)碼,內容為空冶匹,這樣就節(jié)省了傳輸數(shù)據(jù)量习劫。

示例代碼

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(): 不會緩存數(shù)據(jù),直接訪問服務器
                //onlyIfCached():只請求緩存中的數(shù)據(jù)嗓蘑,不靠譜
                .cacheControl(new CacheControl.Builder().build())
                .build();
Call call = okHttpClient.newCall(request);

Response response = call.execute();
//讀取數(shù)據(jù)
response.body().string();

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

//在創(chuàng)建 cache 開始計算
System.out.println("cache hitCount:"+cache.hitCount());//使用緩存的次數(shù)
System.out.println("cache networkCount:"+cache.networkCount());//使用網絡請求的次數(shù)
System.out.println("cache requestCount:"+cache.requestCount());//請求的次數(shù)


//第一次的運行結果(沒有使用緩存)
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 實現(xiàn)的闸天。

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

CacheStrategy

它是一個策略器暖呕,負責判斷是使用緩存還是請求網絡獲取新的數(shù)據(jù)。內部有兩個屬性: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 表示資源沒有發(fā)生改變拇涤,服務器要求客戶端繼續(xù)使用緩存
  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();
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市永票,隨后出現(xiàn)的幾起案子卵贱,更是在濱河造成了極大的恐慌,老刑警劉巖侣集,帶你破解...
    沈念sama閱讀 216,692評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件键俱,死亡現(xiàn)場離奇詭異,居然都是意外死亡世分,警方通過查閱死者的電腦和手機编振,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來臭埋,“玉大人踪央,你說我怎么就攤上這事∑耙酰” “怎么了畅蹂?”我有些...
    開封第一講書人閱讀 162,995評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長炫掐。 經常有香客問我魁莉,道長,這世上最難降的妖魔是什么募胃? 我笑而不...
    開封第一講書人閱讀 58,223評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮畦浓,結果婚禮上痹束,老公的妹妹穿的比我還像新娘。我一直安慰自己讶请,他們只是感情好祷嘶,可當我...
    茶點故事閱讀 67,245評論 6 388
  • 文/花漫 我一把揭開白布屎媳。 她就那樣靜靜地躺著,像睡著了一般论巍。 火紅的嫁衣襯著肌膚如雪烛谊。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,208評論 1 299
  • 那天嘉汰,我揣著相機與錄音丹禀,去河邊找鬼。 笑死鞋怀,一個胖子當著我的面吹牛双泪,可吹牛的內容都是我干的。 我是一名探鬼主播密似,決...
    沈念sama閱讀 40,091評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼焙矛,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了残腌?” 一聲冷哼從身側響起村斟,我...
    開封第一講書人閱讀 38,929評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎抛猫,沒想到半個月后邓梅,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 45,346評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡邑滨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,570評論 2 333
  • 正文 我和宋清朗相戀三年日缨,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掖看。...
    茶點故事閱讀 39,739評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡匣距,死狀恐怖,靈堂內的尸體忽然破棺而出哎壳,到底是詐尸還是另有隱情毅待,我是刑警寧澤,帶...
    沈念sama閱讀 35,437評論 5 344
  • 正文 年R本政府宣布归榕,位于F島的核電站尸红,受9級特大地震影響,放射性物質發(fā)生泄漏刹泄。R本人自食惡果不足惜外里,卻給世界環(huán)境...
    茶點故事閱讀 41,037評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望特石。 院中可真熱鬧盅蝗,春花似錦、人聲如沸姆蘸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至狂秦,卻和暖如春灌侣,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背裂问。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評論 1 269
  • 我被黑心中介騙來泰國打工侧啼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人愕秫。 一個月前我還...
    沈念sama閱讀 47,760評論 2 369
  • 正文 我出身青樓慨菱,卻偏偏與公主長得像,于是被迫代替她去往敵國和親戴甩。 傳聞我的和親對象是個殘疾皇子符喝,可洞房花燭夜當晚...
    茶點故事閱讀 44,647評論 2 354

推薦閱讀更多精彩內容

  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn)甜孤,斷路器协饲,智...
    卡卡羅2017閱讀 134,652評論 18 139
  • 大家好,之前我們講解了Okhttp網絡數(shù)據(jù)請求相關的內容缴川,這一節(jié)我們講講數(shù)據(jù)緩存的處理茉稠。本節(jié)按以下內容講解Okht...
    Ihesong閱讀 10,378評論 6 26
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,082評論 25 707
  • 前言 在Android開發(fā)中我們經常要進行各種網絡訪問,比如查看各類新聞把夸、查看各種圖片而线。但有一種情形就是我們每次重...
    SnowDragonYY閱讀 6,244評論 2 11
  • 中午去萬達吃飯,突然聽到一聲刺耳的尖叫聲恋日,寶貝立刻抱緊了我膀篮,幸好沒有被嚇哭。 我一邊捂住寶貝的耳朵岂膳,不忘好奇的往樓...
    青檸檬靜語閱讀 664評論 2 3