OkHttp 源碼分析系列(三)- 5大Interceptor

??今天我們來看看OkHttp中另外一個大頭--InterceptorOkhttp真正的網絡請求都在這個攔截器鏈中執行的,所以分析OkHttp的攔截器鏈是非常有必要的。

1. 概述

??在正式分析攔截器鏈之前,我們先來每一個攔截器有一個大概的認識,同時對攔截器鏈的調用過程有一個宏觀上的理解。
??在OkHttp中,給我們分配了5種攔截器,它們分別是:RetryAndFollowUpInterceptorBridgeInterceptorCacheInterceptorConnectInterceptorCallServerInterceptor。這里先對每一種攔截器的作用做一個總結。

類名 作用
RetryAndFollowUpInterceptor 主要負責網絡請求的失敗重連
BridgeInterceptor 主要是將用戶的Request轉換為能夠真正進行網絡請求的Request,負責添加一些相應頭;其次就是將服務器返回的Response解析成為用戶用夠真正使用的Response,負責GZip解壓之類的
CacheInterceptor 主要負責網絡請求的Response緩存管理
ConnectInterceptor 主要負責打開與服務器的連接,正式進行網絡請求
CallServerInterceptor 主要負責往網絡數據流寫入數據,同時接收服務器返回的數據

??在這里,再對整個攔截器鏈做一個小小的總結。
??攔截器鏈的調用是從RealCallgetResponseWithInterceptorChain方法開始的,在這個方法里面,會創建一個RealInterceptorChain對象,然后調用了RealInterceptorChainproceed方法,進而,在RealInterceptorChainproceed方法里面會調用第一個攔截器的intercept方法。而在攔截器的intercept方法里面,會再創建一個RealInterceptorChain對象,然后調用proceed方法,而此時,在proceed方法里面,調用就是第二個攔截器的intercept方法。如下圖:

2. RetryAndFollowUpInterceptor

??在getResponseWithInterceptorChain方法里面,除了我們自定義的攔截器之外,第一個調用的就是RetryAndFollowUpInterceptor,所以,我們來看看RetryAndFollowUpInterceptor究竟為我們做了那些事情。
??RetryAndFollowUpInterceptor主要是負責網絡請求的失敗重連。

  @Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Call call = realChain.call();
    EventListener eventListener = realChain.eventListener();

    StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
        createAddress(request.url()), call, eventListener, callStackTrace);
    this.streamAllocation = streamAllocation;

    int followUpCount = 0;
    Response priorResponse = null;
    while (true) {
        //...代碼省略
    }
  }

??從上面的代碼中,我們可以看到,創建了StreamAllocation對象,這個類主要負責網絡數據流的管理。在RetryAndFollowUpInterceptor類里面只是對StreamAllocation對象進行了初始化,并沒有使用它來進行網絡請求,真正使用它的地方是在ConnectInterceptor里面。
??其次,就是在while循環里面,這里就不詳細的展開了,只是對一個進行簡單的看看:

      if (++followUpCount > MAX_FOLLOW_UPS) {
        streamAllocation.release();
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }

??這句話非常的簡單,就是如果當前重試的次數超過最大次數,就是結束當前的重連操作,并且拋出異常。

3. BridgeInterceptor

??在前面的總結表中,已經對BridgeInterceptor做了一個小小的總結。
??BridgeInterceptor在調用RealInterceptorChainproceed方法之前,會向RequestBuilder里面添加很多的信息,包括Content-TypeContent-Length等等。這些都是網絡請求的必要信息,這里我們就不需要過多的關注。這個在后面講解ConnectionPool時,會詳細的解釋
??但是,有一個參數需要的特別注意,那就是Connection在不為null時,會設置為Keep-Alive,這個參數表示,一些TCP連接是否保持連接,如果保持連接,就可以達到復用的效果。
??當調用proceed返回結果時,此時還需要一步操作,那就是如果Response就是經過GZip壓縮的,就需要解壓。如下代碼:

    if (transparentGzip
        && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
        && HttpHeaders.hasBody(networkResponse)) {
      GzipSource responseBody = new GzipSource(networkResponse.body().source());
      Headers strippedHeaders = networkResponse.headers().newBuilder()
          .removeAll("Content-Encoding")
          .removeAll("Content-Length")
          .build();
      responseBuilder.headers(strippedHeaders);
      String contentType = networkResponse.header("Content-Type");
      responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
    }

??當然這個服務端是否返回GZip壓縮的數據,還要取決于在添加Request的響應頭時,是否告訴服務端,客戶端支持Gzip壓縮;如果客戶端支持,那么服務端就會返回Gzip壓縮過的數據。

4. CacheInterceptor

??CacheInterceptor的職責我們從這個類的名字就可以知道,它主要負責Response的緩存管理,包括哪些請求可以從緩存中取,哪些數據需要重新進行網絡請求;哪些數據可以緩存,哪些數據不能被緩存等等。這些操作都是由CacheInterceptor來管理的。

(1).緩存原理

??在正式了解CacheInterceptor之前,我們還是先來看看OkHttp怎么使用Cache,同時了解OkHttp的緩存原理。

  private final OkHttpClient mOkHttpClient = new OkHttpClient.Builder()
    .readTimeout(50, TimeUnit.SECONDS)
    .cache(new Cache(new File("pby123"),1000))
    .build();

??如果我們需要將某些Response緩存,可以直接在創建OkHttpClient的對象時,調用cache方法來進行配置。
??CacheInterceptor在使用緩存時,是通過調用Cacheget方法和put方法來進行數據的獲取和緩存。我們分別來看看。

(2).put方法

??在Cache類里面,我們需要重點關注的是put方法和get方法,因為CacheInterceptor是通過調用這個方法來進行數據緩存的相關操作的。我們先來看看put方法:

  @Nullable 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;
    }
    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(key(response.request().url()));
      if (editor == null) {
        return null;
      }
      entry.writeTo(editor);
      return new CacheRequestImpl(editor);
    } catch (IOException e) {
      abortQuietly(editor);
      return null;
    }
  }

??我將put方法分為兩個大步驟。
??首先是判斷當前的Response是否支持緩存,比如當前的請求方式是Post,就不支持緩存:

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

??如果當前的Response支持緩存,那么就可以進行緩存的操作,這就是第二步。首先是,將當前的Response使用Entry包裝起來,然后將創建DiskLruCache.Editor的對象,最后就是使用DiskLruCache.Editor對象來對數據進行寫入。
??總體上來說,還是比較簡單的。不過在在這里我們發現,OkHttp是DiskLruCache來實現緩存的操作,這一點需要特別的注意。

(3).get方法

??說了put方法,get方法就更加的簡單了。put方法是緩存數據,那么get方法就是獲取緩存數據。我們還是簡單的看看:

  @Nullable Response get(Request request) {
    String key = key(request.url());
    DiskLruCache.Snapshot snapshot;
    Entry entry;
    try {
      snapshot = cache.get(key);
      if (snapshot == null) {
        return null;
      }
    } catch (IOException e) {
      // Give up because the cache cannot be read.
      return null;
    }

    try {
      entry = new Entry(snapshot.getSource(ENTRY_METADATA));
    } catch (IOException e) {
      Util.closeQuietly(snapshot);
      return null;
    }

    Response response = entry.response(snapshot);

    if (!entry.matches(request, response)) {
      Util.closeQuietly(response.body());
      return null;
    }

    return response;
  }

??get方法總體來說比較,這里就不做過多的解釋了。
??OkHttp的緩存原理也沒有傳說中那么高大上,就是一些普通的操作。在簡單的了解緩存原理之后,我們來看看CacheInterceptor

(4). CacheInterceptor

??CacheInterceptorintercept方法比較長,這里就不完全貼出來,這里以分段的形式解釋。
??我覺得,可以將CacheInterceptorintercept過程分為三個步驟。

1.從緩存中獲取數據,如果緩存數據有效的,之前返回,不進行網絡請求。
2.調用RealInterceptorChainproceed方法,進行網絡請求,獲取數據。
3.如果請求的數據支持緩存的話,那么緩存起來。

??我們一個一個的來看看。首先來看獲取緩存數據這部分。

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

??在整個過程中,思路還是比較清晰的。首先是調用Cacheget方法獲取相應的緩存數據;然后就是創建一個緩存策略對象,這個緩存策略對象在后面有很大的作用,后面對它單獨的講解,這里就先對它有一個了解吧。
??接下就有四個判斷,需要重點解釋一下。

    if (cache != null) {
      cache.trackResponse(strategy);
    }

??上面代碼表示的意思是,如果當前的Cache不為空,那么就追蹤一下當前的緩存數據,主要是更新一下,獲取緩存的次數,以及緩存命中率。

  synchronized void trackResponse(CacheStrategy cacheStrategy) {
    requestCount++;

    if (cacheStrategy.networkRequest != null) {
      // If this is a conditional request, we'll increment hitCount if/when it hits.
      networkCount++;
    } else if (cacheStrategy.cacheResponse != null) {
      // This response uses the cache and not the network. That's a cache hit.
      hitCount++;
    }
  }

??然后就是第二個判斷。

    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }

??上面代碼的意思表示,如果從緩存里面獲取的數據不為空,但是緩存策略里面的Response為空,就表示當前從緩存中獲取數據無效的,所以調用closeQuietly方法關閉緩存相應的數據。
??我們再來看看第三個判斷。

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

??如果緩存策略里面的RequestResponse都為空,表示當前網絡不可用,并且沒有緩存數據,所以就是返回504。
??最后我們來看看判斷。

    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

??這里就非常簡單了,如果當前的網絡請求不可用,并且緩存數據不為空,那么就返回緩存的數據。
??第二個過程就是調用RealInterceptorChainproceed方法,這里就不詳細的解釋。我們直接來看看第三個過程。

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

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

??緩存數據的過程也是非常的簡單的。
??首先返回的數據如果是HTTP_NOT_MODIFIED并且緩存數據不為空,那么更新一下緩存數據,否則的話,就關閉緩存的數據。
??其次,就是返回的數據支持緩存,那么就是調用cacheWritingResponse方法來緩存。
??如上就是整個RealInterceptorChain的緩存過程。接下來,我們來分析一下整個過程中貫穿的緩存策略類CacheStrategy

(5).CacheStrategy

??我們直接來看CacheStrategy.Factoryget方法。看看這個方法到底做了什么。
??不過在看get方法之前,我們必須得有認知,那就是CacheStrategy里面的networkRequestcacheResponse到底代表著什么。

  1. networkRequest:表示一個網絡請求,如果這個對象不為空的話,那么在CacheStrategy里面肯定會進行網絡請求,至于最后是選擇緩存數據還是請求回來的數據,得看具體的情況。
  2. cacheResponse:表示緩存的Response,如果當前的網絡不可用,也就是networkRequest為空,那么會直接返回緩存的數據;如果networkRequest不為空,那么就得跟請求回來的數據比較,具體的比較,可以參考上面的第三個過程。

??現在,我們來正式看看get方法。

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

??其實get方法也沒有做什么,核心操作還是在getCandidate里面。由于getCandidate方法過于長,這里直接貼出代碼,在代碼中解釋。

    private CacheStrategy getCandidate() {
      // 沒有緩存,返回一個request和一個為null的cacheResponse
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }

      // 如果當前是Https的請求,并且沒有握手
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }

      //如果當前的cacheResponse不支持緩存
      if (!isCacheable(cacheResponse, request)) {
        return new CacheStrategy(request, null);
      }

      CacheControl requestCaching = request.cacheControl();
      // 如果當前沒有緩存或者請求有條件
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }

      CacheControl responseCaching = cacheResponse.cacheControl();
      // 如果當前的Response不可以被改變
      if (responseCaching.immutable()) {
        return new CacheStrategy(null, cacheResponse);
      }
      // 省略代碼
    }

??其實,到最后來看,緩存策略CacheStrategy并不是那么的可怕,還是比較通俗易懂的。

5. ConnectInterceptor

??現在,我們來看看ConnectInterceptor這個類。我們先來看看ConnectInterceptorproceed方法:

  @Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    StreamAllocation streamAllocation = realChain.streamAllocation();

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }

??整個過程還是簡單的,不過有幾個地方還是需要我們注意的。

    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);

??還記得在RetryAndFollowUpInterceptor里面,在那里創建了StreamAllocation對象,但是沒有用。當時,我也說了,StreamAllocation對象真正的使用實在ConnectInterceptor里面,這也正證實了當時的描述。
??然而這一句有什么用呢?通過調用StreamAllocationnewStream方法,返回了一個HttpCodec對象。這個HttpCodec對象的作用是對網絡請求進行編碼和對網絡請求回來的數據進行解碼,這些操作都是HttpCodec類給我們分裝好的。
??然后就是調用StreamAllocationconnection方法獲取RealConnection對象。
??最后就調用RealInterceptorChainproceed方法。這一步是常規操作。
??看到這里來,是不是感覺心里面有一點失落?感覺ConnectInterceptorintercept方法里面并沒有做什么事。
??其實真正的操作并沒有在intercept方法里面,而是在StreamAllocationnewStream方法里面。我們來看看newStream方法。

  public HttpCodec newStream(
      OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    int connectTimeout = chain.connectTimeoutMillis();
    int readTimeout = chain.readTimeoutMillis();
    int writeTimeout = chain.writeTimeoutMillis();
    int pingIntervalMillis = client.pingIntervalMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();

    try {
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
      HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);

      synchronized (connectionPool) {
        codec = resultCodec;
        return resultCodec;
      }
    } catch (IOException e) {
      throw new RouteException(e);
    }
  }

??其實newStream方法也是非常的簡單,重點還是在findHealthyConnection方法,我們來看看。

  private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
      int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
      boolean doExtensiveHealthChecks) throws IOException {
    while (true) {
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
          pingIntervalMillis, connectionRetryEnabled);

      // If this is a brand new connection, we can skip the extensive health checks.
      synchronized (connectionPool) {
        if (candidate.successCount == 0) {
          return candidate;
        }
      }

      // Do a (potentially slow) check to confirm that the pooled connection is still good. If it
      // isn't, take it out of the pool and start again.
      if (!candidate.isHealthy(doExtensiveHealthChecks)) {
        noNewStreams();
        continue;
      }

      return candidate;
    }
  }

?? findHealthyConnection方法的作用是找到一個健康的連接,在findHealthyConnection方法里面,我們可以看到,不斷調用findConnection方法來找到一個連接,然后判斷當前這個連接是否是健康,如果是健康的,那么就返回,否則就重新尋找。
?? 這里的健康是一個宏觀的概念,那什么表示不健康呢?如果一個連接沒有關閉或者相關的流沒有關閉都表示該連接是不健康的。
??我們再來看看findConnection方法,由于findConnection方法過長,這里就不全部貼出來了。簡單的解釋一下這個方法整個執行流程。
??findConnection方法主要分為兩步:

  1. 找到一個可以使用的Connection
  2. 調用Connectionconnect方法進行連接。

??先來看看第一步。

      // Attempt to use an already-allocated connection. We need to be careful here because our
      // already-allocated connection may have been restricted from creating new streams.
      releasedConnection = this.connection;
      toClose = releaseIfNoNewStreams();
      if (this.connection != null) {
        // We had an already-allocated connection and it's good.
        result = this.connection;
        releasedConnection = null;
      }
      if (!reportedAcquired) {
        // If the connection was never reported acquired, don't report it as released!
        releasedConnection = null;
      }

      if (result == null) {
        // Attempt to get a connection from the pool.
        Internal.instance.get(connectionPool, address, this, null);
        if (connection != null) {
          foundPooledConnection = true;
          result = connection;
        } else {
          selectedRoute = route;
        }
      }
    }

??尋找一個可以使用的Connection過程看上去還是比較簡單的。總體上來說,先嘗試復用當前StreamAllocationconnection,如果可以復用的話,直接拿來用;否則的話,就去連接池 connectionPool里面去取得一個可以使用的連接。
??第二步操作就是調用Connectionconnect方法。這連接方法里面就涉及到非常多的東西,包括連接方式(socket連接還是隧道連接)等等。這里不進行展開了。
??最后在findConnection方法里面,我們可以又把獲得的連接放回了連接池中去了:

    synchronized (connectionPool) {
      reportedAcquired = true;

      // Pool the connection.
      Internal.instance.put(connectionPool, result);

      // If another multiplexed connection to the same address was created concurrently, then
      // release this connection and acquire that one.
      if (result.isMultiplexed()) {
        socket = Internal.instance.deduplicate(connectionPool, address, this);
        result = connection;
      }
    }

6. CallServerInterceptor

??CallServerInterceptorOkHttp中的5大攔截器最后一個攔截器。CallServerInterceptor攔截器的作用在上面做了一個簡單的介紹,在這里將結合代碼來說明CallServerInterceptor到底做了那些事情。
??CallServerInterceptor的執行過程,我們可以分為兩步

  1. 往網絡請求中寫入數據,包括寫入頭部數據和body數據。
  2. 接收服務器返回的數據。

??具體的細節這里就不在詳細的解釋了,都是一些常規操作。

7. 總結

??InterceptorOkhttp里面比較核心的東西,同時也是比較復雜的東西,這里對攔截器做一個簡單的總結。

  1. OKhttp里面,5大攔截器構成一個鏈,從RealCallgetResponseWithInterceptorChain方法開始,5大攔截器開始執行。
  2. 5大攔截是通過RealInterceptorChain連接起來的,它們之間調用過程一定熟悉。
  3. 5大攔截器,其中每個攔截的作用都需要熟悉。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375