Okhttp緩存源碼分析和自定義緩存實現(xiàn)

緩存的一般思路

下面是我理解的網(wǎng)絡請求框架的緩存基本實現(xiàn)搜贤。大致的過程是有緩存用緩存的數(shù)據(jù),沒緩存發(fā)起http請求取數(shù)據(jù)些举,得到最新數(shù)據(jù)后存到緩存里馒吴。


image.png

那么Okhttp怎么實現(xiàn)緩存的,我們從Okhttp發(fā)起一次請求的全過程中來看緩存是怎么實現(xiàn)的

Okhttp請求過程源碼分析

最簡單的使用(以下代碼都是okhttp3.8.0為基礎):

Response response = client.newCall(request).execute()

追蹤到Call接口的實現(xiàn)類RealCall的方法execute

@Override public Response execute() throws IOException {
  synchronized (this) {
    // 判斷是否在執(zhí)行脖母,是則拋出異常
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
  // 初始化跟蹤stack trace的對象士鸥,用來做日志,所以可以忽略先
  captureCallStackTrace();
  try {
    //將異步的請求丟到異步的雙端隊列(Deque<RealCall> runningSyncCalls)中等待處理谆级,這里可以先忽略烤礁,直接看同步的結果
    client.dispatcher().executed(this);
    //獲取Response
    Response result = getResponseWithInterceptorChain();
    if (result == null) throw new IOException("Canceled");
    return result;
  } finally {
    client.dispatcher().finished(this);
  }
}

很明顯,獲取response在getResponseWithInterceptorChain這個方法里肥照。這里代碼很簡單脚仔,就是初始化一個interceptor列表,然后調用RealInterceptorChainproceed函數(shù)建峭。

  Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);
  }

interceptors(攔截器)

那么這些interceptor有什么用呢玻侥,我們挑幾個重要的一一看一下,記住這些interceptors的add順序很重要
client.interceptors():
依次追蹤到interceptors的賦值的地方

    public Builder addInterceptor(Interceptor interceptor) {
      interceptors.add(interceptor);
      return this;
    }

    public List<Interceptor> interceptors() {
      return interceptors;
    }

這個是不是很熟悉亿蒸,這個就是我們利用OkHttpClient.Builder builder構造okhttpClient的地方傳入的interceptor凑兰,也就是常說的application interceptor
CacheInterceptor:看名字很明顯是用來做緩存的
ConnectInterceptor:用來建立http連接
client.networkInterceptors():同client.interceptors(),是我們創(chuàng)建okhttpclient時傳入的networkInterceptor
CallServerInterceptor:向server發(fā)請求的
RealInterceptorChain.proceed(request)
追蹤到下面的方法边锁,這時候傳入的streamAllocation姑食,httpCodec,connection都是null茅坛,index=0

      public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
          RealConnection connection) throws IOException {
        // 判斷index是否越界
        if (index >= interceptors.size()) throw new AssertionError();
    
        calls++;
    
        // If we already have a stream, confirm that the incoming request will use it.
        if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
          throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
              + " must retain the same host and port");
        }
    
        // If we already have a stream, confirm that this is the only call to chain.proceed().
        if (this.httpCodec != null && calls > 1) {
          throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
              + " must call proceed() exactly once");
        }
    
        // Call the next interceptor in the chain.
        // 創(chuàng)建一個新的RealInterceptorChain音半,除了index+1,其他的參數(shù)都和上一個RealInterceptorChain保持不變
        RealInterceptorChain next = new RealInterceptorChain(
            interceptors, streamAllocation, httpCodec, connection, index + 1, request);
        // 獲取當前index的Interceptor
        Interceptor interceptor = interceptors.get(index);
        // 執(zhí)行當前Interceptor的intercept方法贡蓖,傳入的參數(shù)為下一個RealInterceptorChain
        Response response = interceptor.intercept(next);
    
        // Confirm that the next interceptor made its required call to chain.proceed().
        if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
          throw new IllegalStateException("network interceptor " + interceptor
              + " must call proceed() exactly once");
        }
    
        // Confirm that the intercepted response isn't null.
        if (response == null) {
          throw new NullPointerException("interceptor " + interceptor + " returned null");
        }
    
        return response;
      }

所以RealInterceptorChain.proceed的大致過程如下

  1. 獲取下一個RealInterceptorChain next
  2. 調用當前的interceptor的intercept方法曹鸠,傳入?yún)?shù)為next

所以我們要追蹤interceptor的intercept方法,下面我以我項目里的一個做統(tǒng)計的intercptor為例來分析

public class NetStatisticsInterceptor implements Interceptor {
    
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        HttpUrl httpUrl = request.url();
        HttpUrl.Builder urlBuilder = httpUrl.newBuilder();
        if (httpUrl.queryParameter("app_version") == null) {
            urlBuilder.addQueryParameter("app_version", BaseConfig.versionName);
            }
            // 用chain.request()構造一個新的傳入統(tǒng)計參數(shù)的request,作為參數(shù)調用chain.proceed
            return chain.proceed(request.newBuilder().url(urlBuilder.build()).build());
        }
    }

這里對舊的oldRequest做了一堆處理斥铺,加入了一些通用的統(tǒng)計參數(shù)彻桃,包裝成生成了一個新的newRequest,然后調用chain.proceed方法晾蜘,這里又會重新調用RealInterceptorChain.proceed的方法邻眷,只是參數(shù)index+1了眠屎,request為重新包裝后的request了(其他的參數(shù)也可能變了,取決于Interceptor怎么寫)肆饶。接著又會走到RealInterceptorChain.proceed代碼里改衩,走下一個Interceptor的流程。
可以得出如下結論:

  1. 只要Interceptor的intercept方法調用了chain.proceed(request),就會調用Interceptor列表里的下一個Interceptor驯镊;反之可以不調用chain.proceed來打斷這個請求鏈
  2. 我們自定義的application interceptor和network interceptor時葫督,都必須返回chain.proceed得到的結果;否則就會打斷okhttp內(nèi)部的請求鏈
  3. 寫application interceptor時阿宅,在調用chain.proceed(request)之前包裝request
  4. 寫network interceptor時,在調用chain.proceed(request)之后得到的response包裝response

看到這里的代碼設計候衍,是不是和職責鏈模式很相似,唯一不同的是okhttp利用index自增的方式來實現(xiàn)每個攔截器的傳遞洒放。這里我必須感嘆下蛉鹿,代碼設計的真的很巧妙,還有就是設計模式這東西平惩看不出有啥用妖异,到實際碰到了真的很棒。

了解完這些攔截器怎么運行的领追,接下來具體看看各個攔截器是怎么把請求給串聯(lián)起來的他膳。

應用攔截器(client.interceptors())

這個我們常說的application interceptor因為在攔截器list的最前面,所以最先執(zhí)行绒窑,一般用于給request做一些簡單的包裝棕孙,例如添加參數(shù),修改header等

CacheInterceptor

直接看intercept方法

      @Override public Response intercept(Chain chain) throws IOException {
        // 根據(jù)url獲取本地緩存
        Response cacheCandidate = cache != null
            ? cache.get(chain.request())
            : null;
    
        long now = System.currentTimeMillis();
    
        // 用當前時間now些膨、當前的請求request蟀俊、本地緩存cacheCandidate來構造CacheStrategy對象
        // 調用strategy對象的get方法去判斷本地緩存cacheCandidate是否可用
        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.如果networkRequest為null就表示走本地緩存
        if (networkRequest == null) {
          return cacheResponse.newBuilder()
              .cacheResponse(stripBody(cacheResponse))
              .build();
        }
    
        // 走后面的interceptor鏈去取網(wǎng)絡數(shù)據(jù)得到networkResponse
        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());
          }
        }
    
        // 用networkResponse、cacheResponse構造新的response
        Response response = networkResponse.newBuilder()
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
    
        if (cache != null) {
          if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
            // 如果response符合緩存的策略需要緩存订雾,則put到cache中
            // Offer this request to the cache.
            // 這里追蹤到put中肢预,可以發(fā)現(xiàn)只有method為GET才會add到cache中,所以okhttp是只支持get請求的緩存的洼哎;且key為response.request().url()
            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;
      }

大致流程如下

  1. 獲取本地緩存cacheCandidate
  2. 如果本地緩存可用則直接返回cacheCandidate烫映,從而打斷interceptor鏈
  3. 走剩下的interceptor獲取networkResponse
  4. networkResponse、cacheResponse構造新的response
  5. 根據(jù)新的response里的header定制緩存策略噩峦,存入緩存中


    image.png
CacheStrategy

從上面的代碼來看锭沟,主要的緩存策略都是在這個類里實現(xiàn)。
我們關注這兩個變量,networkRequest為null就不走網(wǎng)絡取數(shù)據(jù)识补,cacheResponse為null則不用緩存

      /** The request to send on the network, or null if this call doesn't use the network. */
      public final @Nullable Request networkRequest;
    
      /** The cached response to return or validate; or null if this call doesn't use a cache. */
      public final @Nullable Response cacheResponse;

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

追蹤到public CacheStrategy get()方法

        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.
          // 請求為https且緩存沒有TLS握手
          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.
          // 跟進緩存Response的code族淮,response和request的cache-control的noStore字段判斷是否需要緩存
          if (!isCacheable(cacheResponse, request)) {
            return new CacheStrategy(request, null);
          }
    
          CacheControl requestCaching = request.cacheControl();
          // 請求的header不要緩存
          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);
        }

看這個方法大部分都是返回CacheStrategy(request, null)也就是走網(wǎng)絡,那么我們直接看唯一的返回緩存的代碼:return new CacheStrategy(null, builder.build());什么條件呢?

    // ageMillis是response的maxAge時間和當前時間算出來的cache的有效時間瞧筛。。导盅。较幌。具體我也沒看明白哈
    //response不是no-cache且(ageMillis+request的min-fresh時間)<(request的max-age時間+request的max-stale)
    !responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis

總結下

  1. request的header有only-if-cached:啥緩存都不用
  2. 沒有緩存:當然不用緩存
  3. request為https且緩存丟失了TLS握手:不用緩存
  4. request或者response的header有no-store:不用緩存
  5. response除了200一些的status code以外:不用緩存
  6. 滿足這個條件ageMillis + minFreshMillis < freshMillis + maxStaleMillis的request和response:用緩存
  7. 其他的一些情況(我看暈了)
  8. 總之就是根據(jù)request和response的header的cache-control來做緩存,我們可以嚴格按照http協(xié)議的來做緩存策略白翻,而不用去看okhttp協(xié)議怎么實現(xiàn)的(嗯乍炉,okhttp應該是嚴格按照http協(xié)議來寫的吧?)

ConnectInterceptor,CallServerInterceptor

ConnectInterceptor

Opens a connection to the target server and proceeds to the next interceptor
關鍵類:StreamAllocation

CallServerInterceptor

This is the last interceptor in the chain. It makes a network call to the server.

最佳實踐

服務端控制緩存

  1. 客戶端請求時滤馍,header傳入想要的緩存時間策略岛琼,例如
 @Headers("Cache-Control: no-cache")// 不要緩存
 @Headers("Cache-Control: public, max-age=604800")//緩存時間為604800秒
  1. 服務端指定緩存策略词裤,返回相應的response Cache-Control

然而很不幸惕澎,大部分的服務端都沒有返回Cache-Control來控制緩存,所以就有了下面的辦法

客戶端控制緩存時間

  1. 客戶端傳入header
    @Headers("Cache-Control: public, max-age=30")//緩存時間為30秒
    
  2. 添加networkInterceptor
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Response originalResponse = chain.proceed(request);
        if (TextUtils.isEmpty(originalResponse.header("Cache-Control"))) {
            // 這里把request傳入的header傳遞給response
            return originalResponse.newBuilder().header("Cache-Control", request.header("Cache-Control")).build();
        }
        return originalResponse;
     }
    

客戶端控制緩存時間崎页,同時要求無網(wǎng)絡的時候使用緩存

有網(wǎng)絡的時候同上阁苞;無網(wǎng)絡的時候困檩,如果超過一天則顯示error,沒超過一天用緩存

  1. 客戶端傳入header同上
  2. networkInterceptor同上
  3. 添加applicationInterceptor,傳入一個max-age為無限大數(shù)的header就能強制用緩存了那槽,或者設置Cache-Control的FORCE_CACHE
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        CacheControl cacheControl = request.cacheControl();
        boolean noCache = cacheControl.noCache() || cacheControl.noStore() || cacheControl.maxAgeSeconds() == 0;
        // 如果header強制要求不用緩存就不走這個邏輯
        if (!noCache && !NetworkUtils.isNetworkAvailable(context)) {
            Request.Builder builder = request.newBuilder();
            //if network not available, load in cache
            CacheControl newCacheControl = new CacheControl.Builder()
                   .maxAge(Integer.MAX_VALUE, TimeUnit.SECONDS).build();
            request = builder.cacheControl(newCacheControl).build();
            return chain.proceed(request);
         }
         return chain.proceed(request);
    }
    

客戶端控制緩存時間悼沿,同時要求無網(wǎng)絡的時候使用緩存,并且這個緩存超過一天就失效了

基本同上骚灸,但是傳入的header不是maxAge而是max-stale糟趾,設置緩存過期后還能可用的時間為一天即可

CacheControl newCacheControl = new CacheControl.Builder().maxStale(ONE_DAY, TimeUnit.SECONDS).build();

客戶端控制緩存時間,request的時間和response的時間不同

前面的幾個方式,response的header實際上是從request取出來的甚牲,也就是說我們的response的header時間和request的header時間是一樣的义郑。但是如果要不一樣的情況怎么辦呢?舉個我項目里的例子

  1. 發(fā)送A請求(緩存為30分鐘)鳖藕,發(fā)現(xiàn)這個商品沒有買
  2. 花錢把這個商品買了魔慷,再次請求A請求刷新頁面
  3. 因為A請求有30分鐘緩存沒有刷新數(shù)據(jù);于是乎我修改了request的header為不使用緩存(也就是age為0),這時數(shù)據(jù)刷新了
  4. 幾分鐘后著恩,我下次進來這個頁面院尔,再次請求A(因為之前age為0,所以并沒有緩存)喉誊,我又發(fā)了次請求(實際我期望的是使用緩存的)

實際上我希望的是在步驟3里發(fā)送A請求時邀摆,request的header為age=0,response的age=30min伍茄,那么怎么實現(xiàn)呢栋盹,所以提供了下面的方法

首先提供了一個工具類,用來存放header的時間和生成header敷矫。這里用ThreadLocal變量存放了response的時間

    public final class NetAccessStrategy {
        private NetAccessStrategy() {
    
        }
        private static final ThreadLocal<Integer> localCacheTime = new ThreadLocal<>();
    
        public static void setThreadLocalCacheTime(int cacheTime) {
            localCacheTime.set(cacheTime);
        }
    
        public static int getThreadLocalCacheTime() {
            Integer time = localCacheTime.get();
            localCacheTime.remove();
            if (time == null) {
                return 0;
            }
            return time;
        }
    
        public static final String NET_REQUEST = "net-";
    
    
        /**
         * @param requestCacheTime 本地緩存在超過這個時間后失效
         * @param localCacheTime   本地緩存的時間
         * @return
         */
        public static String getRequestNetHeader(int requestCacheTime, int localCacheTime) {
            return NET_REQUEST + requestCacheTime + "-" + +localCacheTime;
        }
    
        public static int[] getRequestCacheTime(String netHeader) {
            int index1 = netHeader.indexOf("-", 1);
            int index2 = netHeader.indexOf("-", index1 + 1);
            int time1 = -1;
            int time2 = -1;
            if (index1 != -1 && index2 != -1) {
                try {
                    time1 = Integer.parseInt(netHeader.substring(index1 + 1, index2));
                } catch (NumberFormatException ignored) {
                }
                try {
                    time2 = Integer.parseInt(netHeader.substring(index2 + 1));
                } catch (NumberFormatException ignored) {
                }
            }
            return new int[]{time1, time2};
        }
    }

在application Interceptor里加上

    // 如果發(fā)現(xiàn)net-開頭的自定義header時
    if (header.startsWith(NetAccessStrategy.NET_REQUEST)) {
                Request.Builder builder = request.newBuilder();
                // 解析得到request和response的時間
                int[] timeArray = NetAccessStrategy.getRequestCacheTime(header);
                // 傳入request的age時間
                CacheControl cacheControl = new CacheControl.Builder().maxAge(timeArray[0], TimeUnit.SECONDS).build();
                // 存入response的時間
                NetAccessStrategy.setThreadLocalCacheTime(timeArray[1]);
                builder.cacheControl(cacheControl);
                return chain.proceed(builder.build());
            }

在network Interceptor里加上

     Response originalResponse = chain.proceed(request);
                int time = NetAccessStrategy.getThreadLocalCacheTime();
                if (time > 0) {
                    // 取出response time例获,如果大于0則放到header里
                    return originalResponse.newBuilder().header("Cache-Control", "public, max-age=" + time)
                            .build();
                }
                return originalResponse.newBuilder().header("Cache-Control", request.header("Cache-Control"))
                        .build();

發(fā)請求時加上header汉额,這樣就能實現(xiàn)強制刷新,且緩存為300秒的功能了

NetAccessStrategy.getRequestNetHeader(0, 300)

我的主頁

PS:https://github.com/nppp1990榨汤,平常隨便寫寫蠕搜,還有kotlin的demo

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市收壕,隨后出現(xiàn)的幾起案子妓灌,更是在濱河造成了極大的恐慌,老刑警劉巖蜜宪,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件虫埂,死亡現(xiàn)場離奇詭異,居然都是意外死亡圃验,警方通過查閱死者的電腦和手機掉伏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來澳窑,“玉大人岖免,你說我怎么就攤上這事≌占瘢” “怎么了颅湘?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長栗精。 經(jīng)常有香客問我闯参,道長,這世上最難降的妖魔是什么悲立? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任鹿寨,我火速辦了婚禮,結果婚禮上薪夕,老公的妹妹穿的比我還像新娘脚草。我一直安慰自己,他們只是感情好原献,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布馏慨。 她就那樣靜靜地躺著,像睡著了一般姑隅。 火紅的嫁衣襯著肌膚如雪写隶。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天讲仰,我揣著相機與錄音慕趴,去河邊找鬼。 笑死,一個胖子當著我的面吹牛冕房,可吹牛的內(nèi)容都是我干的躏啰。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼耙册,長吁一口氣:“原來是場噩夢啊……” “哼丙唧!你這毒婦竟也來了?” 一聲冷哼從身側響起觅玻,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎培漏,沒想到半個月后溪厘,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡牌柄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年畸悬,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片珊佣。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡蹋宦,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出咒锻,到底是詐尸還是另有隱情冷冗,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布惑艇,位于F島的核電站蒿辙,受9級特大地震影響,放射性物質發(fā)生泄漏滨巴。R本人自食惡果不足惜思灌,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望恭取。 院中可真熱鬧泰偿,春花似錦、人聲如沸蜈垮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽攒发。三九已至课兄,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間晨继,已是汗流浹背烟阐。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蜒茄。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓唉擂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親檀葛。 傳聞我的和親對象是個殘疾皇子玩祟,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

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