okhttp 3.10緩存原理

主文okhttp 3.10詳細介紹okhttp的緩存機制,緩存代碼都在攔截器CacheInterceptor中實現(xiàn),在看代碼之前,先回顧http的緩存策略。

http緩存策略

http緩存中最常用的是下面幾個:

  • Expires
  • Cache-control
  • Last-Modified / If-Modified-Since
  • Etag / If-None-Match

Expires和Cache-control

Expires和Cache-control看的是資源過期時間绒瘦,如果在時間范圍內(nèi),緩存命中扣癣,直接使用緩存惰帽;否則需要向服務器發(fā)送請求,拿到完整的數(shù)據(jù)父虑。

Expires:Mon, 30 Apr 2018 05:24:14 GMT
Cache-Control:public, max-age=31536000

上面是第一次訪問資源時该酗,response返回的Expires和Cache-control。

Expires寫死資源的過期時間士嚎,在時間范圍內(nèi)呜魄,客戶端可以繼續(xù)使用緩存,不需要發(fā)送請求莱衩。Expires是http1時代的東西爵嗅,缺陷很明顯,時間是服務器時間笨蚁,和客戶端時間可能存在誤差睹晒。在http1.1,升級使用Cache-Control括细,同時存在Expires和Cache-control時伪很,以Cache-control為準。

Cache-control常見的可選項有:

  • private:客戶端可以緩存
  • public:客戶端和代理服務器都可以緩存
  • max-age=x-seconds:多少秒內(nèi)可以緩存
  • no-cache:不能直接緩存勒极,需要用后面介紹的緩存校驗
  • no-store:不能緩存

上面例子同時使用了public和max-age是掰,max-age=31536000表示在365天內(nèi)都可以直接使用緩存。

緩存校驗

資源過期后辱匿,需要向服務器發(fā)送請求,但資源可能在服務器上沒有修改過炫彩,沒有必要完整拿回整個資源匾七,這個時候緩存校驗就派上用場。

  • Last-Modified / If-Modified-Since
  • Etag / If-None-Match

上面兩組是緩存校驗相關的字段江兢,首先來看Last-Modified / If-Modified-Since昨忆。

第一次請求response
Last-Modified:Tue, 03 Apr 2018 10:26:36 GMT

第二次請求request
If-Modified-Since:Tue, 03 Apr 2018 10:26:36 GMT

第一次請求資源時,服務器會在response中帶上資源最后修改時間杉允,寫在Last-Modified邑贴。當客戶端再次請求資源席里,request用If-Modified-Since帶上上次response中的Last-Modified,詢問該時間后資源是否修改過:

  • 資源修改過拢驾,需要返回完整內(nèi)容奖磁,響應200;
  • 資源沒有修改過繁疤,只需要返回http頭咖为,響應304。

Last-Modified在時間上只到秒稠腊,Etag為資源生成唯一標識躁染,更加精確。

第一次請求response
ETag:"2400-5437207ef2880"

第二次請求request
If-None-Match:"2400-5437207ef2880"

第一次請求資源時架忌,response在ETag返回資源在服務器的唯一標識吞彤。當客戶端再次請求資源時,request在If-None-Match帶上上次的唯一標識叹放,詢問資源是否修改過:

  • 唯一標識不同饰恕,資源修改過,需要返回完整內(nèi)容许昨,響應200懂盐;
  • 唯一標識相同,資源沒有修改過糕档,只返回http頭莉恼,響應304。

Last-Modified和ETag同時存在時速那,當然ETag優(yōu)先俐银。

測試緩存效果

進入正題,先來展示okhttp上使用緩存的效果端仰。

Cache cache = new Cache(new File("/Users/heng/testCache"), 1024 * 1024);
OkHttpClient client = new OkHttpClient.Builder().cache(cache).build();

Request request = new Request.Builder().url("http://www.taobao.com/").
        cacheControl(new CacheControl.Builder().maxStale(365, TimeUnit.DAYS).build()).
        build();
Response response1 = client.newCall(request).execute();
response1.body().string();
System.out.println("response1.networkResponse:" + response1.networkResponse());
System.out.println("response1.cacheResponse:" + response1.cacheResponse());
System.out.println("");

Response response2 = client.newCall(request).execute();
response2.body().string();
System.out.println("response2.networkResponse:" + response2.networkResponse());
System.out.println("response2.cacheResponse:" + response2.cacheResponse());

// run result
response1.networkResponse:Response{protocol=http/1.1, code=200, message=OK, url=https://www.taobao.com/}
response1.cacheResponse:null

response2.networkResponse:null
response2.cacheResponse:Response{protocol=http/1.1, code=200, message=OK, url=https://www.taobao.com/}

創(chuàng)建一個Cache對象捶惜,參數(shù)是緩存在磁盤的路徑和大小,傳遞給OkHttpClient荔烧。請求淘寶主頁兩次吱七,可以看到第一次請求是通過網(wǎng)絡,第二次請求是通過緩存鹤竭,networkResponse和cacheResponse分別表示請求從哪個途徑獲取數(shù)據(jù)踊餐。

查看磁盤,多了下面三個文件臀稚。

journal
bb35d9b59f4cc10d8fa23899f8cbb054.0  
bb35d9b59f4cc10d8fa23899f8cbb054.1

journal是DiskLruCache日志文件吝岭,用DiskLruCache注釋里的例子學習寫入格式:

libcore.io.DiskLruCache
1
100
2

CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
DIRTY 335c4c6028171cfddfbaae1a9c313c52
CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
REMOVE 335c4c6028171cfddfbaae1a9c313c52
DIRTY 1ab96a171faeeee38496d8b330771a7a
CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
READ 335c4c6028171cfddfbaae1a9c313c52
READ 3400330d1dfc7f3f7f4b8d4d803dfcf6

頭幾行每行是個字段,具體含義:

  • 第一行:固定寫libcore.io.DiskLruCache;
  • 第二行:緩存版本窜管;
  • 第三行:應用版本散劫;
  • 第四行:指valueCount,后文會介紹幕帆。

接下來每一行是一次操作記錄获搏,每次操作Cache都會產(chǎn)生一條。

  • DIRTY:說明緩存數(shù)據(jù)正在創(chuàng)建或更新蜓肆,每個成功的DIRTY都要對應一個CLEAN或REMOVE颜凯,如果對不上,說明操作失敗仗扬,要清理症概;
  • CLEAN:說明操作成功,每行后面記錄value的長度
  • READ:一次讀取
  • REMOVE:一次清除

Cache和文件

磁盤上的日志文件是如何關聯(lián)Cache并支持增刪改查的呢早芭,我們從底層File開始彼城,逐層解開okhttp對緩存數(shù)據(jù)的管理。

Cache內(nèi)部使用了DiskLruCache退个,這個DiskLruCache是okhttp自己實現(xiàn)的募壕,使用okio作為輸入輸出。

第一步:FileSystem封裝File的操作

首先是FileSystem语盈,封裝了File常用操作舱馅,沒有使用java.io的InputStream和OutputStream作為輸入輸出流,取而代之的是okio刀荒。FileSystem是個接口代嗤,直接在interface里提供了個實現(xiàn)類SYSTEM(我要參考)。

public interface FileSystem {
  Source source(File file) throws FileNotFoundException;
  Sink sink(File file) throws FileNotFoundException;
  Sink appendingSink(File file) throws FileNotFoundException;
  void delete(File file) throws IOException;
  boolean exists(File file);
  long size(File file);
  void rename(File from, File to) throws IOException;
  void deleteContents(File directory) throws IOException;
}

第二步:DiskLruCache.Entry和DiskLruCache.Editor

private final class Entry {
    final String key;
    final File[] cleanFiles;
    final File[] dirtyFiles;
    //...
}

DiskLruCache.Entry維護請求url對應的緩存文件缠借,url的md5作為key干毅,value_count說明對應幾多個文件,預設是2泼返。cleanFiles和dirtyFiles就是對應上面講的CLEAN和DIRTY硝逢,描述數(shù)據(jù)進入修改和已經(jīng)穩(wěn)定兩種狀態(tài)。

看上面我們實操得到的兩個緩存文件绅喉,名字都是key渠鸽,結(jié)尾不同:

  • .0:記錄請求的內(nèi)容和握手信息;
  • .1:真正緩存的內(nèi)容柴罐。

拒絕魔法數(shù)字拱绑,Cache上定義了0和1的常量:

private static final int ENTRY_METADATA = 0;
private static final int ENTRY_BODY = 1;

操作DiskLruCache.Entry的是DiskLruCache.Editor,它的構(gòu)造函數(shù)傳入DiskLruCache.Entry對象丽蝎,里面有兩個方法:

public Source newSource(int index){}
public Sink newSink(int index){}

通過傳入的index定位,讀取cleanFiles,寫入dirtyFiles屠阻,對外暴露okio的Source和Sink红省。于是,我們可以通過DiskLruCache.Editor讀寫磁盤上的緩存文件了国觉。

第三步:Snapshot封裝緩存結(jié)果

從DiskLruCache獲取緩存結(jié)果吧恃,不是返回DiskLruCache.Entry,而是緩存快照Snapshot麻诀。我們只關心當前緩存的內(nèi)容痕寓,其他東西知道得越少越好。

 public final class Snapshot implements Closeable {
    private final String key;
    private final Source[] sources;
}

Snapshot保存了key和sources蝇闭,sources的來源通過FileSystem獲取cleanFiles的Source呻率。

Snapshot snapshot() {
  if (!Thread.holdsLock(DiskLruCache.this)) throw new AssertionError();

  Source[] sources = new Source[valueCount];
  long[] lengths = this.lengths.clone(); // Defensive copy since these can be zeroed out.
  try {
    for (int i = 0; i < valueCount; i++) {
      sources[i] = fileSystem.source(cleanFiles[i]);
    }
    return new Snapshot(key, sequenceNumber, sources, lengths);
  } catch (FileNotFoundException e) {
    //
  }
}

緩存增刪改查

Cache通過InternalCache供外部包調(diào)用,提供增刪改查的能力呻引,實質(zhì)調(diào)用DiskLruCache對應方法礼仗。

  • get -> get
  • put -> edit
  • update -> edit
  • remove -> remove

箭頭左邊是Cache的方法,右邊是DiskLruCache的方法逻悠。

DiskLruCache核心的數(shù)據(jù)結(jié)構(gòu)是LinkedHashMap元践,key是字符串,對應一個Entry童谒,要注意Cache的Entry和DiskLruCache的Entry不是同一回事单旁。

final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<>(0, 0.75f, true);

簡單幾句回顧LinkedHashMap的特點,它在HashMap的基礎上饥伊,主要增加維護順序的雙向鏈表象浑,元素Entry增加before和after描述前后指向的元素。

順序的控制有兩種撵渡,由標志位accessOrder控制:

  • 插入順序
  • 訪問順序

如果使用LinkedHashMap實現(xiàn)LRU融柬,accessOrder需要設置為true,按訪問排序趋距,head后的第一個Entry就是最近最少使用的節(jié)點粒氧。


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

上面代碼片段是Cache.put重要部分,首先將response封裝到Cache.Entry节腐,然后獲取DiskLruCache.Editor外盯。

synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
   initialize();

   checkNotClosed();
   validateKey(key);
   Entry entry = lruEntries.get(key);
   if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
       || entry.sequenceNumber != expectedSequenceNumber)) {
     return null; // Snapshot is stale.
   }
   if (entry != null && entry.currentEditor != null) {
     return null; // Another edit is in progress.
   }
   if (mostRecentTrimFailed || mostRecentRebuildFailed) {
     // The OS has become our enemy! If the trim job failed, it means we are storing more data than
     // requested by the user. Do not allow edits so we do not go over that limit any further. If
     // the journal rebuild failed, the journal writer will not be active, meaning we will not be
     // able to record the edit, causing file leaks. In both cases, we want to retry the clean up
     // so we can get out of this state!
     executor.execute(cleanupRunnable);
     return null;
   }

   // Flush the journal before creating files to prevent file leaks.
   journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n');
   journalWriter.flush();

   if (hasJournalErrors) {
     return null; // Don't edit; the journal can't be written.
   }

   if (entry == null) {
     entry = new Entry(key);
     lruEntries.put(key, entry);
   }
   Editor editor = new Editor(entry);
   entry.currentEditor = editor;
   return editor;
 }

通過key獲取editor,里面是一系列工作:

  • initialize初始化翼雀,關聯(lián)journal文件并按格式讀缺ス丁;
  • journal寫入DIRTY行狼渊;
  • 獲取或創(chuàng)建DiskLruCache.Entry箱熬;
  • 創(chuàng)建Editor對象类垦。

具體寫入文件有兩步,第一步調(diào)用entry.writeTo(editor)城须,里面是一堆write操作蚤认,寫入目標是ENTRY_METADATA,也就是上面說過以.0結(jié)尾的文件糕伐。

第二步調(diào)用new CacheRequestImpl(editor)砰琢,返回一個CacheRequest。

CacheRequestImpl(final DiskLruCache.Editor editor) {
  this.editor = editor;
  this.cacheOut = editor.newSink(ENTRY_BODY);
  this.body = new ForwardingSink(cacheOut) {
    @Override public void close() throws IOException {
      synchronized (Cache.this) {
        if (done) {
          return;
        }
        done = true;
        writeSuccessCount++;
      }
      super.close();
      editor.commit();
    }
  };
}

CacheRequestImpl在構(gòu)造函數(shù)里直接執(zhí)行邏輯良瞧,文件操作目標是ENTRY_BODY(具體的緩存數(shù)據(jù))陪汽。Editor有commit和abort兩個重要操作,我們來看commit褥蚯,里面繼續(xù)調(diào)用completeEdit:

synchronized void completeEdit(Editor editor, boolean success) throws IOException {
  Entry entry = editor.entry;
  //..

  for (int i = 0; i < valueCount; i++) {
    File dirty = entry.dirtyFiles[i];
    if (success) {
      if (fileSystem.exists(dirty)) {
        File clean = entry.cleanFiles[i];
        fileSystem.rename(dirty, clean);
        long oldLength = entry.lengths[i];
        long newLength = fileSystem.size(clean);
        entry.lengths[i] = newLength;
        size = size - oldLength + newLength;
      }
    } else {
      fileSystem.delete(dirty);
    }
  }

  redundantOpCount++;
  entry.currentEditor = null;
  if (entry.readable | success) {
    entry.readable = true;
    journalWriter.writeUtf8(CLEAN).writeByte(' ');
    journalWriter.writeUtf8(entry.key);
    entry.writeLengths(journalWriter);
    journalWriter.writeByte('\n');
    if (success) {
      entry.sequenceNumber = nextSequenceNumber++;
    }
  } else {
    lruEntries.remove(entry.key);
    journalWriter.writeUtf8(REMOVE).writeByte(' ');
    journalWriter.writeUtf8(entry.key);
    journalWriter.writeByte('\n');
  }
  journalWriter.flush();

  if (size > maxSize || journalRebuildRequired()) {
    executor.execute(cleanupRunnable);
  }
}

具體commit過程挚冤,是將DIRTY改為CLEAN,并寫入CLEAN行遵岩。

過了一遍最復雜的put你辣,里面還有很多細節(jié)沒有寫出來,但足夠讓我們了解寫入journal和緩存文件的過程尘执。

光速看完其他三個操作舍哄,update類似put,路過誊锭。

public synchronized Snapshot get(String key) throws IOException {
  initialize();

  checkNotClosed();
  validateKey(key);
  Entry entry = lruEntries.get(key);
  if (entry == null || !entry.readable) return null;

  Snapshot snapshot = entry.snapshot();
  if (snapshot == null) return null;

  redundantOpCount++;
  journalWriter.writeUtf8(READ).writeByte(' ').writeUtf8(key).writeByte('\n');
  if (journalRebuildRequired()) {
    executor.execute(cleanupRunnable);
  }

  return snapshot;
}

get方法直接從lruEntries獲取到entry表悬,轉(zhuǎn)為Snapshot返回,寫入一條READ行丧靡。最后會從ENTRY_METADATA再讀一次entry蟆沫,比較確認匹配。

boolean removeEntry(Entry entry) throws IOException {
  if (entry.currentEditor != null) {
    entry.currentEditor.detach(); // Prevent the edit from completing normally.
  }

  for (int i = 0; i < valueCount; i++) {
    fileSystem.delete(entry.cleanFiles[i]);
    size -= entry.lengths[i];
    entry.lengths[i] = 0;
  }

  redundantOpCount++;
  journalWriter.writeUtf8(REMOVE).writeByte(' ').writeUtf8(entry.key).writeByte('\n');
  lruEntries.remove(entry.key);

  if (journalRebuildRequired()) {
    executor.execute(cleanupRunnable);
  }

  return true;
}

刪除就是刪除clean文件和寫入REMOVE行温治。


補充介紹日志的清理饭庞,當滿足冗余日志超過日志本體或者超過2000(journalRebuildRequired),需要執(zhí)行清理熬荆。

執(zhí)行的線程池只有一條清理線程cleanupRunnable舟山,直接新建journal去除冗余記錄。(ConnectionPool都是用線程池執(zhí)行清理線程卤恳,好像挺好用累盗,記住)

CacheInterceptor

對日志的操作不感冒突琳,為了學習的完整性若债,分析了一輪。其實緩存機制不外乎就是對上面操作的調(diào)用拆融,磨刀不誤砍柴工蠢琳。

首先需要弄懂的是CacheStrategy啊终,顧名思義,定義了緩存的策略挪凑,基本就是http緩存協(xié)議的實現(xiàn)孕索。

CacheStrategy提供了Factory,傳入原始request和當前緩存response躏碳,從requst里讀取我們熟悉的"Expires"、"Last-Modified"散怖、"ETag"幾個緩存相關字段菇绵。CacheStrategy的get方法調(diào)用了getCandidate方法,getCandidate代碼很長镇眷,而且是根據(jù)RFC標準文檔對http協(xié)議的實現(xiàn)咬最,很死板貼。最后創(chuàng)建了CacheStrategy對象欠动,根據(jù)是否有緩存永乌、是否開啟緩存配置、緩存是否失效等設置networkRequest和cacheResponse具伍。

巴拉巴拉說了這么多翅雏,記住CacheStrategy的目標就是得到networkRequest和cacheResponse,具體代碼自己看人芽。

根據(jù)networkRequest和cacheResponse是否為空望几,兩兩組合有四種情況:

networkRequest cacheResponse 結(jié)果
null null 不需要網(wǎng)絡,又無緩存萤厅,所以配置了only-if-cached橄抹,返回504
null non-null 緩存有效,使用緩存惕味,不需要網(wǎng)絡
non-null null 無緩存或者失效楼誓,直接網(wǎng)絡
non-null non-null 緩存校驗,需要網(wǎng)絡

CacheInterceptor的實現(xiàn)就依據(jù)上面四種情況名挥,我們逐段分析:

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

獲取緩存和緩存策略疟羹,上面已經(jīng)講完,trackResponse統(tǒng)計緩存命中率躺同。

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

networkRequest和cacheResponse同時為空阁猜,說明設置了只用緩存,但又沒有緩存蹋艺,返回504剃袍。

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

不需要網(wǎng)路,緩存又ok捎谨,直接返回緩存response民效。

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

需要發(fā)網(wǎng)絡請求憔维,這時候可能是完整請求也可能是緩存校驗請求,在getCandidate里已經(jīng)設置好了畏邢。

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

如果是緩存校驗請求业扒,服務器又返回304,表示本地緩存可用舒萎,更新本地緩存并返回緩存吴侦。如果資源有更新,關閉原有的緩存排截。

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;

最后就是將普通請求寫入緩存税产。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市咆贬,隨后出現(xiàn)的幾起案子败徊,更是在濱河造成了極大的恐慌,老刑警劉巖掏缎,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件皱蹦,死亡現(xiàn)場離奇詭異,居然都是意外死亡眷蜈,警方通過查閱死者的電腦和手機沪哺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來端蛆,“玉大人凤粗,你說我怎么就攤上這事〗穸梗” “怎么了嫌拣?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長呆躲。 經(jīng)常有香客問我异逐,道長,這世上最難降的妖魔是什么插掂? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任灰瞻,我火速辦了婚禮,結(jié)果婚禮上辅甥,老公的妹妹穿的比我還像新娘酝润。我一直安慰自己,他們只是感情好璃弄,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布要销。 她就那樣靜靜地躺著,像睡著了一般夏块。 火紅的嫁衣襯著肌膚如雪疏咐。 梳的紋絲不亂的頭發(fā)上纤掸,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機與錄音浑塞,去河邊找鬼借跪。 笑死,一個胖子當著我的面吹牛酌壕,可吹牛的內(nèi)容都是我干的掏愁。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼仅孩,長吁一口氣:“原來是場噩夢啊……” “哼托猩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起辽慕,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎赦肃,沒想到半個月后溅蛉,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡他宛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年船侧,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片厅各。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡镜撩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出队塘,到底是詐尸還是另有隱情袁梗,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布憔古,位于F島的核電站遮怜,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏鸿市。R本人自食惡果不足惜锯梁,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望焰情。 院中可真熱鬧陌凳,春花似錦、人聲如沸内舟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谒获。三九已至蛤肌,卻和暖如春壁却,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背裸准。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工展东, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人炒俱。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓盐肃,卻偏偏與公主長得像,于是被迫代替她去往敵國和親权悟。 傳聞我的和親對象是個殘疾皇子砸王,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容