Okhttp的緩存機(jī)制

Okhttp的源碼分析

Okhttp的線程池和高并發(fā)

Okhttp鏈接池的使用

Okhttp的緩存機(jī)制

Okhttp的責(zé)任鏈模式

Okhttp的緩存機(jī)制

緩存目的:減少用戶向服務(wù)器發(fā)送請求的次數(shù)仁锯,從而加快響應(yīng)的速度福也,降低服務(wù)器的負(fù)載

Http協(xié)議下的緩存機(jī)制

強(qiáng)制緩存

通過http協(xié)議所傳送的數(shù)據(jù),會被保存到緩存數(shù)據(jù)庫中旨剥,強(qiáng)制緩存的意思是指抄瑟,若緩存數(shù)據(jù)庫中的數(shù)據(jù)仍未失效凡泣,則直接通過緩存數(shù)據(jù)庫獲得數(shù)據(jù),不再通過http向服務(wù)器發(fā)送請求锐借。其中有兩個比較重要的字段用于控制是否失效:

Expires

指過期的時間问麸,其值由服務(wù)器所決定。當(dāng)在緩存數(shù)據(jù)庫取得相應(yīng)的數(shù)據(jù)時钞翔,通過比較當(dāng)前時間與Expires來決定是否直接使用緩存數(shù)據(jù)庫中的數(shù)據(jù)严卖。然而值得注意的是,服務(wù)端和客戶端之間存在著延時布轿,沒有統(tǒng)一的時間標(biāo)準(zhǔn)哮笆,因此隨著Http協(xié)議的發(fā)展来颤,使用的機(jī)會也就越來越少

Cache-Control

字面理解為緩存的控制,其實(shí)際意思是緩存的屬性稠肘。類似于java中的作用域福铅,http中的緩存分為如下幾種類型:

  1. public:表示其中的數(shù)據(jù)完全可以被存儲,包括密碼等隱私信息项阴,且所有人就可以訪問滑黔,其安全性也較低
  2. private: 存儲到用戶的私有cache中去,只有用戶本身可以訪問(默認(rèn))
  3. no-cache 僅在客戶端與服務(wù)端建立認(rèn)證后环揽,才可以緩存(用于對比緩存)
  4. no-store 代表其中的請求和響應(yīng)等信息都不會被緩存
  5. max-age 表示所返回的數(shù)據(jù)已經(jīng)過期或失效

對比緩存

使用前要與服務(wù)器的緩存進(jìn)行對比略荡。通過服務(wù)器返回的狀態(tài)碼決定是否使用

304: 使用對比緩存的數(shù)據(jù)

200:使用服務(wù)器的最新數(shù)據(jù)

判定字段

Etag

資源的唯一標(biāo)識,類似于人們的身份證號碼歉胶,資源的內(nèi)容一旦發(fā)生改動汛兜,Etag就會發(fā)生改變⊥ń瘢客戶端發(fā)送請求的時候格式為 If-None-Match +Etag粥谬,服務(wù)器收到后則會與緩存的Etag進(jìn)行比對

Last-Modified

字面意思,最近修改的時間辫塌,由服務(wù)器所決定漏策,客戶端在發(fā)送請求的時候使用If-Modified-Since + 指定時間,若客戶端所存的數(shù)據(jù)≤該時間則說明資源沒有改動

To put into a nutshell :

Okhttp的緩存機(jī)制

可以看到璃氢,http本身協(xié)議的緩存機(jī)制較為簡單哟玷,不能很好的滿足實(shí)際的需求。okhttp相對來說便更加復(fù)雜一也。先說結(jié)論:

  1. 緩存基于文件存儲
  2. 內(nèi)部維護(hù)基于LRU算法的緩存清理線程

Okhttp讀取緩存流程

Okhttp 存儲緩存流程

源碼解析

CacheControl

用于指定緩存的規(guī)則

public final class CacheControl {

  //表示這是一個優(yōu)先使用網(wǎng)絡(luò)驗證巢寡,驗證通過之后才可以使用緩存的緩存控制,設(shè)置了noCache
  public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();

  //表示這是一個優(yōu)先先使用緩存的緩存控制椰苟,設(shè)置了onlyIfCached和maxStale的最大值
  public static final CacheControl FORCE_CACHE = new Builder()
      .onlyIfCached()
      .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
      .build();

  //以下的字段都是HTTP中Cache-Control字段相關(guān)的值
  private final boolean noCache;
  private final boolean noStore;
  private final int maxAgeSeconds;
  private final int sMaxAgeSeconds;
  private final boolean isPrivate;
  private final boolean isPublic;
  private final boolean mustRevalidate;
  private final int maxStaleSeconds;
  private final int minFreshSeconds;
  private final boolean onlyIfCached;
  private final boolean noTransform;

  //解析頭文件中的相關(guān)字段抑月,得到該緩存控制類
  public static CacheControl parse(Headers headers) {
    ...
  }

}

CacheStrategy

主要用于判斷是否使用緩存數(shù)據(jù)

public final class CacheStrategy {

    public Factory(long nowMillis, Request request, Response cacheResponse) {
      this.nowMillis = nowMillis;
      //網(wǎng)絡(luò)請求和緩存響應(yīng)
      this.request = request;
      this.cacheResponse = cacheResponse;

      if (cacheResponse != null) {
        //找到緩存響應(yīng)的響應(yīng)頭信息
        Headers headers = cacheResponse.headers();
        for (int i = 0, size = headers.size(); i < size; i++) {
          //查看響應(yīng)頭信息中是否有以下字段信息
          String fieldName = headers.name(i);
          String value = headers.value(i);
          if ("Date".equalsIgnoreCase(fieldName)) {
            servedDate = HttpDate.parse(value);
            servedDateString = value;
          } else if ("Expires".equalsIgnoreCase(fieldName)) {
            expires = HttpDate.parse(value);
          } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
            lastModified = HttpDate.parse(value);
            lastModifiedString = value;
          } else if ("ETag".equalsIgnoreCase(fieldName)) {
            etag = value;
          } else if ("Age".equalsIgnoreCase(fieldName)) {
            ageSeconds = HeaderParser.parseSeconds(value, -1);
          } else if (OkHeaders.SENT_MILLIS.equalsIgnoreCase(fieldName)) {
            sentRequestMillis = Long.parseLong(value);
          } else if (OkHeaders.RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) {
            receivedResponseMillis = Long.parseLong(value);
          }
        }
      }
    }

    public CacheStrategy get() {
      //獲取判定的緩存策略
      CacheStrategy candidate = getCandidate();

      if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
        // 如果判定的緩存策略的網(wǎng)絡(luò)請求不為空,但是只使用緩存舆蝴,則返回兩者都為空的緩存策略谦絮。
        return new CacheStrategy(null, null);
      }

      return candidate;
    }

    /** Returns a strategy to use assuming the request can use the network. */
    private CacheStrategy getCandidate() {
      // No cached response.
      //如果沒有緩存響應(yīng),則返回沒有緩存響應(yīng)的策略
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }

      // Drop the cached response if it's missing a required handshake.
      //如果請求是https洁仗,而緩存響應(yīng)的握手信息為空层皱,則返回沒有緩存響應(yīng)的策略
      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.
      //如果請求對應(yīng)的響應(yīng)不能被緩存,則返回沒有緩存響應(yīng)的策略
      if (!isCacheable(cacheResponse, request)) {
        return new CacheStrategy(request, null);
      }

      //獲取請求頭中的CacheControl信息
      CacheControl requestCaching = request.cacheControl();
      //如果請求頭中的CacheControl信息是不緩存的赠潦,則返回沒有緩存響應(yīng)的策略
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }

      //獲取響應(yīng)的年齡
      long ageMillis = cacheResponseAge();
      //計算上次響應(yīng)刷新的時間
      long freshMillis = computeFreshnessLifetime();
      //如果請求里有最大持續(xù)時間要求叫胖,則取較小的值作為上次響應(yīng)的刷新時間
      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;
      //獲取緩存響應(yīng)頭中的CacheControl信息
      CacheControl responseCaching = cacheResponse.cacheControl();
      //如果緩存響應(yīng)不是必須要再驗證她奥,并且請求有最大過期時間瓮增,則用請求的最大過期時間作為最大過期時間
      if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
      }

      //如果支持緩存怎棱,并且持續(xù)時間+最短刷新時間<上次刷新時間+最大驗證時間 則可以緩存
      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\\"");
        }
        //返回響應(yīng)緩存
        return new CacheStrategy(null, builder.build());
      }

      //構(gòu)造一個新的有條件的Request,添加If-None-Match绷跑,If-Modified-Since等信息
      Request.Builder conditionalRequestBuilder = request.newBuilder();

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

      Request conditionalRequest = conditionalRequestBuilder.build();
      //根據(jù)是否有If-None-Match拳恋,If-Modified-Since信息,返回不同的緩存策略
      return hasConditions(conditionalRequest)
          ? new CacheStrategy(conditionalRequest, cacheResponse)
          : new CacheStrategy(conditionalRequest, null);
    }

    /**
     * Returns true if the request contains conditions that save the server from sending a response
     * that the client has locally. When a request is enqueued with its own conditions, the built-in
     * response cache won't be used.
     */
    private static boolean hasConditions(Request request) {
      return request.header("If-Modified-Since") != null || request.header("If-None-Match") != null;
    }
}

Cache

對外開放的緩存類砸捏,類似數(shù)據(jù)庫能夠增刪改查

  1. 增添緩存
CacheRequest put(Response response) {
    String requestMethod = response.request().method();
    //如果請求是"POST","PUT","PATCH","PROPPATCH","REPORT"則移除這些緩存  
    if (HttpMethod.invalidatesCache(response.request().method())) {
      try {
        remove(response.request());
      } catch (IOException ignored) {
      }
      return null;
    }
    //僅支持GET的請求緩存谬运,其他請求不緩存
    if (!requestMethod.equals("GET")) {
       return null;
    }
    //判斷請求中的http數(shù)據(jù)包中headers是否有符號"*"的通配符,有則不緩存  
    if (HttpHeaders.hasVaryAll(response)) {
      return null;
    }
    //把response構(gòu)建成一個Entry對象
    Entry entry = new Entry(response);
    DiskLruCache.Editor editor = null;
    try {
      //生成DiskLruCache.Editor對象
      editor = cache.edit(key(response.request().url()));
      if (editor == null) {
        return null;
      }
      //對緩存進(jìn)行寫入
      entry.writeTo(editor);
      //構(gòu)建一個CacheRequestImpl類垦藏,包含Ok.io的Sink對象
      return new CacheRequestImpl(editor);
    } catch (IOException e) {
      abortQuietly(editor);
      return null;
    }
  }

  1. 查找緩存
Response get(Request request) {
    //獲取url轉(zhuǎn)換過來的key
    String key = key(request.url());
    DiskLruCache.Snapshot snapshot;
    Entry entry;
    try {
         //根據(jù)key獲取對應(yīng)的snapshot 
         snapshot = cache.get(key);
         if (snapshot == null) {
             return null;
         }
    } catch (IOException e) {
      return null;
    }
    try {
     //創(chuàng)建一個Entry對象,并由snapshot.getSource()獲取Sink
      entry = new Entry(snapshot.getSource(ENTRY_METADATA));
    } catch (IOException e) {
      Util.closeQuietly(snapshot);
      return null;
    }
    //通過entry和response生成respson吩谦,通過Okio.buffer獲取請求體,然后封裝各種請求信息
    Response response = entry.response(snapshot);
    if (!entry.matches(request, response)) {
      //對request和Response進(jìn)行比配檢查膝藕,成功則返回該Response。
      Util.closeQuietly(response.body());
      return null;
    }
    return response;
  }

  1. 更新緩存
void update(Response cached, Response network) {
    //用Respon構(gòu)建一個Entry
    Entry entry = new Entry(network);
    //從緩存中獲取DiskLruCache.Snapshot
    DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot;
    DiskLruCache.Editor editor = null;
    try {
      //獲取DiskLruCache.Snapshot.edit對象
      editor = snapshot.edit(); // Returns null if snapshot is not current.
      if (editor != null) {
        //將entry寫入editor中
        entry.writeTo(editor);
        editor.commit();
      }
    } catch (IOException e) {
      abortQuietly(editor);
    }
  }

  1. 刪除緩存

主體位于DiskLruCache之中

void remove(Request request) throws IOException {
    //通過url轉(zhuǎn)化成的key去刪除緩存
    cache.remove(key(request.url()));
  }

  1. writeTo ok.io
 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());
        sink.writeUtf8(handshake.tlsVersion().javaName()).writeByte('\\n');
      }
      sink.close();
    }

DiskLruCache

真實(shí)存儲(文件格式)的緩存功能類咐扭,使用了基于LinkedHashedMap芭挽。可以看到除了一些關(guān)鍵的方法之外其主要包括了三個重要的內(nèi)部類蝗肪。

  1. Entry

用于存儲緩存數(shù)據(jù)的實(shí)體類袜爪,一個url對應(yīng)一個實(shí)體,在Entry還有Snapshot對象

private final class Entry {
  final String key;

  /** Lengths of this entry's files. */
  final long[] lengths;
  final File[] cleanFiles;
  final File[] dirtyFiles;

  /** True if this entry has ever been published. */
  boolean readable;

  /** The ongoing edit or null if this entry is not being edited. */
  Editor currentEditor;

  /** The sequence number of the most recently committed edit to this entry. */
  long sequenceNumber;

  Entry(String key) {
    this.key = key;

    lengths = new long[valueCount];
    cleanFiles = new File[valueCount];
    dirtyFiles = new File[valueCount];

    // The names are repetitive so re-use the same builder to avoid allocations.
    StringBuilder fileBuilder = new StringBuilder(key).append('.');
    int truncateTo = fileBuilder.length();
    for (int i = 0; i < valueCount; i++) {
      fileBuilder.append(i);
      cleanFiles[i] = new File(directory, fileBuilder.toString());
      fileBuilder.append(".tmp");
      dirtyFiles[i] = new File(directory, fileBuilder.toString());
      fileBuilder.setLength(truncateTo);
    }
  }

  /** Set lengths using decimal numbers like "10123". */
  void setLengths(String[] strings) throws IOException {
    if (strings.length != valueCount) {
      throw invalidLengths(strings);
    }

    try {
      for (int i = 0; i < strings.length; i++) {
        lengths[i] = Long.parseLong(strings[i]);
      }
    } catch (NumberFormatException e) {
      throw invalidLengths(strings);
    }
  }

  /** Append space-prefixed lengths to {@code writer}. */
  void writeLengths(BufferedSink writer) throws IOException {
    for (long length : lengths) {
      writer.writeByte(' ').writeDecimalLong(length);
    }
  }

  private IOException invalidLengths(String[] strings) throws IOException {
    throw new IOException("unexpected journal line: " + Arrays.toString(strings));
  }

  /**
   * Returns a snapshot of this entry. This opens all streams eagerly to guarantee that we see a
   * single published snapshot. If we opened streams lazily then the streams could come from
   * different edits.
   */
  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) {
      // A file must have been deleted manually!
      for (int i = 0; i < valueCount; i++) {
        if (sources[i] != null) {
          Util.closeQuietly(sources[i]);
        } else {
          break;
        }
      }
      // Since the entry is no longer valid, remove it so the metadata is accurate (i.e. the cache
      // size.)
      try {
        removeEntry(this);
      } catch (IOException ignored) {
      }
      return null;
    }
  }
}

  1. Snapshot
public final class Snapshot implements Closeable {
    private final String key;
    private final long sequenceNumber;
    private final Source[] sources;
    private final long[] lengths;

    Snapshot(String key, long sequenceNumber, Source[] sources, long[] lengths) {
      this.key = key;
      this.sequenceNumber = sequenceNumber;
      this.sources = sources;
      this.lengths = lengths;
    }

    public String key() {
      return key;
    }

    /**
     * Returns an editor for this snapshot's entry, or null if either the entry has changed since
     * this snapshot was created or if another edit is in progress.
     */
    public @Nullable Editor edit() throws IOException {
      return DiskLruCache.this.edit(key, sequenceNumber);
    }

    /** Returns the unbuffered stream with the value for {@code index}. */
    public Source getSource(int index) {
      return sources[index];
    }

    /** Returns the byte length of the value for {@code index}. */
    public long getLength(int index) {
      return lengths[index];
    }

    public void close() {
      for (Source in : sources) {
        Util.closeQuietly(in);
      }
    }
  }

  1. Editor

在Editor的初始化中要傳入Editor,其實(shí)Editor就是編輯entry的類

public final class Editor {
  final Entry entry;
  final boolean[] written;
  private boolean done;

  Editor(Entry entry) {
    this.entry = entry;
    this.written = (entry.readable) ? null : new boolean[valueCount];
  }

  /**
   * Prevents this editor from completing normally. This is necessary either when the edit causes
   * an I/O error, or if the target entry is evicted while this editor is active. In either case
   * we delete the editor's created files and prevent new files from being created. Note that once
   * an editor has been detached it is possible for another editor to edit the entry.
   */
  void detach() {
    if (entry.currentEditor == this) {
      for (int i = 0; i < valueCount; i++) {
        try {
          fileSystem.delete(entry.dirtyFiles[i]);
        } catch (IOException e) {
          // This file is potentially leaked. Not much we can do about that.
        }
      }
      entry.currentEditor = null;
    }
  }

  /**
   * Returns an unbuffered input stream to read the last committed value, or null if no value has
   * been committed.
   */
  public Source newSource(int index) {
    synchronized (DiskLruCache.this) {
      if (done) {
        throw new IllegalStateException();
      }
      if (!entry.readable || entry.currentEditor != this) {
        return null;
      }
      try {
        return fileSystem.source(entry.cleanFiles[index]);
      } catch (FileNotFoundException e) {
        return null;
      }
    }
  }

  /**
   * Returns a new unbuffered output stream to write the value at {@code index}. If the underlying
   * output stream encounters errors when writing to the filesystem, this edit will be aborted
   * when {@link #commit} is called. The returned output stream does not throw IOExceptions.
   */
  public Sink newSink(int index) {
    synchronized (DiskLruCache.this) {
      if (done) {
        throw new IllegalStateException();
      }
      if (entry.currentEditor != this) {
        return Okio.blackhole();
      }
      if (!entry.readable) {
        written[index] = true;
      }
      File dirtyFile = entry.dirtyFiles[index];
      Sink sink;
      try {
        sink = fileSystem.sink(dirtyFile);
      } catch (FileNotFoundException e) {
        return Okio.blackhole();
      }
      return new FaultHidingSink(sink) {
        @Override protected void onException(IOException e) {
          synchronized (DiskLruCache.this) {
            detach();
          }
        }
      };
    }
  }

  /**
   * Commits this edit so it is visible to readers.  This releases the edit lock so another edit
   * may be started on the same key.
   */
  public void commit() throws IOException {
    synchronized (DiskLruCache.this) {
      if (done) {
        throw new IllegalStateException();
      }
      if (entry.currentEditor == this) {
        completeEdit(this, true);
      }
      done = true;
    }
  }

  /**
   * Aborts this edit. This releases the edit lock so another edit may be started on the same
   * key.
   */
  public void abort() throws IOException {
    synchronized (DiskLruCache.this) {
      if (done) {
        throw new IllegalStateException();
      }
      if (entry.currentEditor == this) {
        completeEdit(this, false);
      }
      done = true;
    }
  }

  public void abortUnlessCommitted() {
    synchronized (DiskLruCache.this) {
      if (!done && entry.currentEditor == this) {
        try {
          completeEdit(this, false);
        } catch (IOException ignored) {
        }
      }
    }
  }
}

  1. 刪除
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;
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末薛闪,一起剝皮案震驚了整個濱河市辛馆,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌豁延,老刑警劉巖昙篙,帶你破解...
    沈念sama閱讀 222,729評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異诱咏,居然都是意外死亡苔可,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評論 3 399
  • 文/潘曉璐 我一進(jìn)店門袋狞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來焚辅,“玉大人,你說我怎么就攤上這事苟鸯⊥撸” “怎么了?”我有些...
    開封第一講書人閱讀 169,461評論 0 362
  • 文/不壞的土叔 我叫張陵早处,是天一觀的道長湾蔓。 經(jīng)常有香客問我,道長陕赃,這世上最難降的妖魔是什么卵蛉? 我笑而不...
    開封第一講書人閱讀 60,135評論 1 300
  • 正文 為了忘掉前任颁股,我火速辦了婚禮,結(jié)果婚禮上傻丝,老公的妹妹穿的比我還像新娘甘有。我一直安慰自己,他們只是感情好葡缰,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,130評論 6 398
  • 文/花漫 我一把揭開白布亏掀。 她就那樣靜靜地躺著,像睡著了一般泛释。 火紅的嫁衣襯著肌膚如雪滤愕。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,736評論 1 312
  • 那天怜校,我揣著相機(jī)與錄音间影,去河邊找鬼。 笑死茄茁,一個胖子當(dāng)著我的面吹牛魂贬,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播裙顽,決...
    沈念sama閱讀 41,179評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼付燥,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了愈犹?” 一聲冷哼從身側(cè)響起键科,我...
    開封第一講書人閱讀 40,124評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎漩怎,沒想到半個月后勋颖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,657評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡勋锤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,723評論 3 342
  • 正文 我和宋清朗相戀三年牙言,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片怪得。...
    茶點(diǎn)故事閱讀 40,872評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡咱枉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出徒恋,到底是詐尸還是另有隱情蚕断,我是刑警寧澤,帶...
    沈念sama閱讀 36,533評論 5 351
  • 正文 年R本政府宣布入挣,位于F島的核電站亿乳,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜葛假,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,213評論 3 336
  • 文/蒙蒙 一障陶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧聊训,春花似錦抱究、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至勋磕,卻和暖如春妈候,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背挂滓。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評論 1 274
  • 我被黑心中介騙來泰國打工苦银, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人赶站。 一個月前我還...
    沈念sama閱讀 49,304評論 3 379
  • 正文 我出身青樓墓毒,卻偏偏與公主長得像,于是被迫代替她去往敵國和親亲怠。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,876評論 2 361

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