Android 優(yōu)雅地處理后臺返回的騷數(shù)據(jù)

前言

Retrofit 是目前主流的網(wǎng)絡(luò)請求框架,不少用過的小伙伴會遇到這樣的問題嚼摩,絕大部分接口測試都正常久脯,就個別接口尤其是返回失敗信息時報了個奇怪的錯誤信息,而看了自己的代碼邏輯也沒什么問題挂捅。別的接口都是一樣的寫,卻沒出現(xiàn)這樣的情況堂湖,可是后臺人員看了也說不關(guān)他們的事闲先。剛遇到時會比較懵,有些人不知道什么原因也就無從下手无蜂。

問題原因

排查問題也很簡單伺糠,把信息百度一下,會發(fā)現(xiàn)是解析異常酱讶。那就先看下后臺返回了什么退盯,用 PostMan 請求一下查看返回結(jié)果彼乌,發(fā)現(xiàn)是類似下面這樣的:

{
  "code": 500,
  "msg": "登錄失敗",
  "data": ""
}

也可能是這樣的:

{
  "code": 500,
  "msg": "登錄失敗",
  "data": 0
}

或者是這樣的:

{
  "code": 500,
  "msg": "登錄失敗",
  "data": []
}

仔細(xì)觀察后突然恍然大悟泻肯,這不是坑爹嗎渊迁?后臺這樣返回解析肯定有問題呀,我要將 data 解析成一個對象灶挟,而后臺返回的是一個空字符串琉朽、整形或空數(shù)組,肯定解析報錯稚铣。

嗯箱叁,這就是后臺的問題,是后臺寫得不“規(guī)范”惕医,所以就跑過去和后臺理論讓他們改。如果后臺是比較好說話,肯配合改還好說浇借。但有些可能是比較“倔強(qiáng)”的性格读慎,可能會說,“這很簡單呀峡钓,知道是失敗狀態(tài)不解析 data 不就好了妓笙?”,或者說能岩,“為什么 iOS 可以寞宫,你這邊卻不行?你們 Android 有問題就不能自己處理掉嗎拉鹃?”辈赋。如果遇到這樣的同事就會比較尷尬。

其實(shí)就算后臺能根據(jù)我們要求改膏燕,但也不是長遠(yuǎn)之計炭庙。后臺人員變動或自己換個環(huán)境可能還是會遇到同樣的情況,每次都和后臺溝通配合改也麻煩煌寇,而且沒準(zhǔn)就剛好遇到“倔強(qiáng)”不肯改的焕蹄。

是后臺人員寫得不規(guī)范嗎?我個人認(rèn)為并不是阀溶,因?yàn)椴]有約定俗成的規(guī)范要這么寫腻脏,其實(shí)只是后臺人員不知道這么返回數(shù)據(jù)會對 Retrofit 的解析有影響,不知道這么寫對 Android 不太友好银锻。后臺人員也沒有錯永品,我們所覺得的“規(guī)范”沒人告訴過他呀』魑常可以通過溝通解決問題鼎姐,不過也建議自己把問題處理了,一勞永逸。

解決方案

既然是解析報錯了炕桨,那么在 Gson 解析成對象之前饭尝,先驗(yàn)證狀態(tài)碼,判斷是錯誤的情況就拋出異常献宫,這樣就不進(jìn)行后續(xù)的 Gson 解析操作去解析 data钥平,也就沒問題了。

最先想到的當(dāng)然是從解析的地方入手姊途,而 Retrofit 能進(jìn)行 Gson 解析是配置了一個 Gson 轉(zhuǎn)換器涉瘾。

retrofit = Retrofit.Builder()
  // 其它配置
  .addConverterFactory(GsonConverterFactory.create())
  .build()

所以我們修改 GsonConverterFactory 不就好了。

自定義 GsonConverterFactory 處理返回結(jié)果

試一下會發(fā)現(xiàn)并不能直接繼承 GsonConverterFactory 重載修改相關(guān)方法捷兰,因?yàn)樵擃愑昧?final 修飾立叛。所以只好把 GsonConverterFactory 源碼復(fù)制出來改,其中關(guān)聯(lián)的兩個類 GsonRequestBodyConverter 和 GsonResponseBodyConverter 也要復(fù)制修改贡茅。下面給出的是 Kotlin 版本的示例囚巴。

class MyGsonConverterFactory private constructor(private val gson: Gson) : Converter.Factory() {

  override fun responseBodyConverter(
    type: Type, annotations: Array<Annotation>,
    retrofit: Retrofit
  ): Converter<ResponseBody, *> {
    val adapter = gson.getAdapter(TypeToken.get(type))
    return MyGsonResponseBodyConverter(gson, adapter)
  }

  override fun requestBodyConverter(
    type: Type,
    parameterAnnotations: Array<Annotation>,
    methodAnnotations: Array<Annotation>,
    retrofit: Retrofit
  ): Converter<*, RequestBody> {
    val adapter = gson.getAdapter(TypeToken.get(type))
    return MyGsonRequestBodyConverter(gson, adapter)
  }

  companion object {
    @JvmStatic
    fun create(): MyGsonConverterFactory {
      return create(Gson())
    }

    @JvmStatic
    fun create(gson: Gson?): MyGsonConverterFactory {
      if (gson == null) throw NullPointerException("gson == null")
      return MyGsonConverterFactory(gson)
    }
  }
}
class MyGsonRequestBodyConverter<T>(
  private val gson: Gson,
  private val adapter: TypeAdapter<T>
) :
  Converter<T, RequestBody> {

  @Throws(IOException::class)
  override fun convert(value: T): RequestBody {
    val buffer = Buffer()
    val writer = OutputStreamWriter(buffer.outputStream(), UTF_8)
    val jsonWriter = gson.newJsonWriter(writer)
    adapter.write(jsonWriter, value)
    jsonWriter.close()
    return buffer.readByteString().toRequestBody(MEDIA_TYPE)
  }

  companion object {
    private val MEDIA_TYPE = "application/json; charset=UTF-8".toMediaType()
    private val UTF_8 = Charset.forName("UTF-8")
  }
}
class MyGsonResponseBodyConverter<T>(
  private val gson: Gson,
  private val adapter: TypeAdapter<T>
) : Converter<ResponseBody, T> {

  @Throws(IOException::class)
  override fun convert(value: ResponseBody): T {
  
    // 在這里通過 value 拿到 json 字符串進(jìn)行解析
    // 判斷狀態(tài)碼是失敗的情況,就拋出異常
    
    val jsonReader = gson.newJsonReader(value.charStream())
    value.use {
      val result = adapter.read(jsonReader)
      if (jsonReader.peek() != JsonToken.END_DOCUMENT) {
        throw JsonIOException("JSON document was not fully consumed.")
      }
      return result
    }
  }
}

上面三個類中只需要修改 GsonResponseBodyConverter 的代碼友扰,因?yàn)槭窃谶@個類解析數(shù)據(jù)彤叉。可以在上面有注釋的地方加入自己的處理村怪。到底加什么代碼秽浇,看完后面的內(nèi)容就知道了。

雖然得到了我們想要的效果甚负,但總感覺并不是很優(yōu)雅柬焕,因?yàn)檫@只是在 gson 解析之前增加一些判斷,而為此多寫了很多和源碼重復(fù)的代碼梭域。還有這是針對 Retrofit 進(jìn)行處理的斑举,如果公司用的是自己封裝的 OkHttp 請求工具,就沒法用這個方案了病涨。

觀察一下發(fā)現(xiàn)其實(shí)只是對一個 ResponseBody 對象進(jìn)行解析判斷狀態(tài)碼富玷,就是說只需要得到個 ResponseBody 對象而已。那么還有什么辦法能在 gson 解析之前拿到 ResponseBody 呢既穆?

自定義攔截器處理返回結(jié)果

很容易會想到用攔截器赎懦,按道理來說是應(yīng)該是可行的,通過攔截器處理也不局限于使用 Retrofit幻工,用 OkHttp 的也能處理励两。

想法很美好,但是實(shí)際操作起來并沒有想象中的簡單囊颅。剛開始可能會想到用 response.body().string() 讀出 json 字符串当悔。

public abstract class ResponseBodyInterceptor implements Interceptor {
  @NotNull
  @Override
  public Response intercept(@NotNull Chain chain) throws IOException {
    Response response = chain.proceed(chain.request());
    String json = response.body().string();
    // 對 json 進(jìn)行解析判斷狀態(tài)碼是失敗的情況就拋出異常
    return response;
  }
}

看著好像沒問題傅瞻,但是嘗試后發(fā)現(xiàn),狀態(tài)碼是失敗的情況確實(shí)沒毛病盲憎,然而狀態(tài)碼是正確的情況卻有問題了嗅骄。

為什么會這樣子?有興趣的可以看下這篇文章《為何 response.body().string() 只能調(diào)用一次焙畔?》。簡單總結(jié)一下就是考慮到應(yīng)用重復(fù)讀取數(shù)據(jù)的可能性很小串远,所以將其設(shè)計為一次性流宏多,讀取后即關(guān)閉并釋放資源。我們在攔截器里用通常的 Response 使用方法會把資源釋放了澡罚,后續(xù)解析沒有資源了就會有問題伸但。

那該怎么辦呢?自己對 Response 的使用又不熟悉留搔,怎么知道該怎么讀數(shù)據(jù)不影響后續(xù)的操作更胖。可以參考源碼呀隔显,OkHttp 也是用了一些攔截器處理響應(yīng)數(shù)據(jù)却妨,它卻沒有釋放掉資源。

這里就不用大家去看源碼研究怎么寫的了括眠,我直接封裝好一個工具類提供大家使用彪标,已經(jīng)把響應(yīng)數(shù)據(jù)的字符串得到了,大家可以直接編寫自己的業(yè)務(wù)代碼掷豺,拷貝下面的類使用即可捞烟。

abstract class ResponseBodyInterceptor : Interceptor {

  override fun intercept(chain: Interceptor.Chain): Response {
    val request = chain.request()
    val url = request.url.toString()
    val response = chain.proceed(request)
    response.body?.let { responseBody ->
      val contentLength = responseBody.contentLength()
      val source = responseBody.source()
      source.request(Long.MAX_VALUE)
      var buffer = source.buffer

      if ("gzip".equals(response.headers["Content-Encoding"], ignoreCase = true)) {
        GzipSource(buffer.clone()).use { gzippedResponseBody ->
          buffer = Buffer()
          buffer.writeAll(gzippedResponseBody)
        }
      }

      val contentType = responseBody.contentType()
      val charset: Charset =
        contentType?.charset(StandardCharsets.UTF_8) ?: StandardCharsets.UTF_8
      if (contentLength != 0L) {
        return intercept(response,url, buffer.clone().readString(charset))
      }
    }
    return response
  }

  abstract fun intercept(response: Response, url: String, body: String): Response
}

由于 OkHttp 源碼已經(jīng)用 Kotlin 語言重寫了,所以只有個 Kotlin 版本的当船。但是可能還有很多人還沒有用 Kotlin 寫項(xiàng)目题画,所以個人又手動翻譯了一個 Java 版本的,方便大家使用德频,同樣拷貝使用即可苍息。

public abstract class ResponseBodyInterceptor implements Interceptor {

  @NotNull
  @Override
  public Response intercept(@NotNull Chain chain) throws IOException {
    Request request = chain.request();
    String url = request.url().toString();
    Response response = chain.proceed(request);
    ResponseBody responseBody = response.body();
    if (responseBody != null) {
      long contentLength = responseBody.contentLength();
      BufferedSource source = responseBody.source();
      source.request(Long.MAX_VALUE);
      Buffer buffer = source.getBuffer();

      if ("gzip".equals(response.headers().get("Content-Encoding"))) {
        GzipSource gzippedResponseBody = new GzipSource(buffer.clone());
        buffer = new Buffer();
        buffer.writeAll(gzippedResponseBody);
      }

      MediaType contentType = responseBody.contentType();
      Charset charset;
      if (contentType == null || contentType.charset(StandardCharsets.UTF_8) == null) {
        charset = StandardCharsets.UTF_8;
      } else {
        charset = contentType.charset(StandardCharsets.UTF_8);
      }

      if (charset != null && contentLength != 0L) {
        return intercept(response,url, buffer.clone().readString(charset));
      }
    }
    return response;
  }

  abstract Response intercept(@NotNull Response response,String url, String body);
}

主要是拿到 source 再獲得 buffer,然后通過 buffer 去讀出字符串壹置。說下其中的一段 gzip 相關(guān)的代碼档叔,為什么需要有這段代碼的處理,自己看源碼的話可能會漏掉蒸绩。這是因?yàn)?OkHttp 請求時會添加支持 gzip 壓縮的預(yù)處理衙四,所以如果響應(yīng)的數(shù)據(jù)是 gzip 編碼的,需要對 gzip 壓縮數(shù)據(jù)解包再去讀數(shù)據(jù)患亿。

好了廢話不多說传蹈,到底這個工具類怎么用押逼,其實(shí)和攔截器一樣使用,繼承我封裝好的 ResponseBodyInterceptor 類惦界,在重寫方法里加上自己需要的業(yè)務(wù)處理代碼挑格,body 參數(shù)就是我們想要的 json 字符串?dāng)?shù)據(jù),可以進(jìn)行解析判斷狀態(tài)碼是失敗情況并拋出異常沾歪。下面給一個簡單的解析例子參考漂彤,json 結(jié)構(gòu)是文章開頭給出的例子,這里假設(shè)狀態(tài)碼不是 200 都拋出一個自定義異常灾搏。

class HandleErrorInterceptor : ResponseBodyInterceptor() {

  override fun intercept(response: Response, body: String): Response {
    var jsonObject: JSONObject? = null
    try {
      jsonObject = JSONObject(body)
    } catch (e: Exception) {
      e.printStackTrace()
    }
    if (jsonObject != null) {
      if (jsonObject.optInt("code", -1) != 200 && jsonObject.has("msg")) {
        throw ApiException(jsonObject.getString("msg"))
      }
    }
    return response
  }
}

然后在 OkHttpClient 中添加該攔截器就可以了挫望。

val okHttpClient = OkHttpClient.Builder()
  // 其它配置
  .addInterceptor(HandleErrorInterceptor())
  .build()

萬一后臺返回的是更騷的數(shù)據(jù)呢?

本人目前只遇到過失敗時 data 類型不一致的情況狂窑,下面是一些小伙伴反饋的媳板,如果大家有遇到類似或更騷的,都建議和后臺溝通改成返回方便自己寫業(yè)務(wù)邏輯代碼的數(shù)據(jù)泉哈。實(shí)在溝通無果蛉幸,再參考下面的案例看下是否有幫助。

后面所給出的參考方案都是緩兵之計丛晦,不能根治問題奕纫。想徹底地解決只能和后臺人員溝通一套合適的規(guī)范。

數(shù)據(jù)需要去 msg 里取

有位小伙伴提到的:騷的時候數(shù)據(jù)還會去 msg 取烫沙。(大家都經(jīng)歷過了什么...)

還是強(qiáng)調(diào)一下建議讓后臺改若锁,實(shí)在沒辦法必須要這么做的話,再往下看斧吐。

假設(shè)返回的數(shù)據(jù)是下面這樣的:

{
  "code": 200,
  "msg": {
    "userId": 123456,
    "userName": "admin"
  }
}

通常 msg 返回的是個字符串又固,但這次居然是個對象,而且是我們需要得到的數(shù)據(jù)煤率。我們解析的實(shí)體類已經(jīng)定義了 msg 是字符串仰冠,當(dāng)然不可能因?yàn)橐粋€接口把 msg 改成泛型,所以我們需要偷偷地把數(shù)據(jù)改成我們想要得到的形式蝶糯。

{
  "code": 200,
  "msg": "登錄成功"
  "data": {
    "userId": 123456,
    "userName": "張三"
  }
}

那么該怎么操作呢洋只?代碼比較簡單,就不啰嗦了昼捍,記得要把該攔截器配置了识虚。

class HandleLoginInterceptor: ResponseBodyInterceptor() {

  override fun intercept(response: Response, url: String, body: String): Response {
    var jsonObject: JSONObject? = null
    try {
      jsonObject = JSONObject(body)
      if (url.contains("/login")) { // 當(dāng)請求的是登錄接口才處理
        if (jsonObject.getJSONObject("msg") != null) {
          jsonObject.put("data", jsonObject.getJSONObject("msg"))
          jsonObject.put("msg", "登錄成功")
        }
      }
    } catch (e: Exception) {
      e.printStackTrace()
    }

    val contentType = response.body?.contentType()
    val responseBody = jsonObject.toString().toResponseBody(contentType)
    return response.newBuilder().body(responseBody).build() // 重新生成響應(yīng)對象
  }
}

如果用 Java 的話,是這樣來重新生成響應(yīng)對象妒茬。

MediaType contentType = response.body().contentType();
ResponseBody responseBody = ResponseBody.create(jsonObject.toString(), contentType);
return response.newBuilder().body(responseBody).build(); 

數(shù)據(jù)多和數(shù)據(jù)少返回的類型不一樣

又有位小伙伴說道:數(shù)據(jù)少給你返回 JSONObject担锤,數(shù)據(jù)多給你返回 JSONArray,數(shù)據(jù)沒有給你返回 “null”乍钻,null肛循,“”铭腕。(這真的不會被打嗎...)

再強(qiáng)調(diào)一次,建議讓后臺改多糠。如果硬要這么做累舷,再參考下面思路。

小伙伴沒給具體的例子夹孔,這里我自己假設(shè)數(shù)據(jù)的幾種情況被盈。

{
  "code": 200,
  "msg": "",
  "data": "null"
}

{
  "code": 200,
  "msg": "",
  "data": {
    "key1": "value1",
    "key2": "value2"
  }
}

{
  "code": 200,
  "msg": "",
  "data": [
    {
      "key1": "value1",
      "key2": "value2"
    },
    {
      "key1": "value3",
      "key2": "value4"
    }
  ]
}

data 的類型會有多種,我們直接請求的話搭伤,應(yīng)該只能將 data 定義成 String只怎,然后解析判斷到底是哪種情況,再寫邏輯代碼闷畸,這樣處理起來麻煩很多尝盼。個人建議用攔截器手動將 data 統(tǒng)一轉(zhuǎn)成 JSONArray 的形式吞滞,這樣 data 類型只有一種佑菩,處理起來更加方便,代碼邏輯也更清晰裁赠。

{
  "code": 200,
  "msg": "",
  "data": []
}

{
  "code": 200,
  "msg": "",
  "data": [
    {
      "key1": "value1",
      "key2": "value2"
    }
  ]
}

{
  "code": 200,
  "msg": "",
  "data": [
    {
      "key1": "value1",
      "key2": "value2"
    },
    {
      "key1": "value3",
      "key2": "value4"
    }
  ]
}

具體的代碼就不給出了殿漠,實(shí)現(xiàn)是類似上一個例子,主要是提供思路給大家參考佩捞。

直接返回 http 狀態(tài)碼绞幌,響應(yīng)報文可能沒有或者不是 json

這是有兩位小伙伴說的情況:后臺直接返回 http 狀態(tài)碼,響應(yīng)報文為空一忱、null莲蜘、"null"、""帘营、[] 等這些數(shù)據(jù)票渠。

還是那句話,建議讓后臺改芬迄。如果不肯改问顷,其實(shí)這個處理起來也還好。

大概了解下后臺返回的 http 狀態(tài)碼是一個 600 以上的數(shù)字禀梳,一個狀態(tài)碼對應(yīng)著一個沒有返回數(shù)據(jù)的操作杜窄。響應(yīng)報文可能沒有,可能不是 json算途。

看起來像是不同類型的響應(yīng)報文塞耕,比數(shù)據(jù)類型不同更難處理。其實(shí)這比之前兩個例子簡單很多嘴瓤,因?yàn)椴挥每紤]讀數(shù)據(jù)荷科。具體處理是判斷一下狀態(tài)碼是多少唯咬,然后拋出對應(yīng)的自定義異常,請求時對該的異常進(jìn)行處理畏浆。響應(yīng)報文都是些“空代表”處理起來好像挺麻煩胆胰,但我們沒必要去管,拋了異常就不會進(jìn)行解析刻获。

class HandleHttpCodeInterceptor : ResponseBodyInterceptor() {

  override fun intercept(response: Response, url: String, body: String): Response {
    when (response.code) {
      600,601,602 -> {
        throw ApiException(response.code, "msg")
      }
      else -> {
      }
    }
    return response
  }
}

在 header 里取 data 數(shù)據(jù)

居然還有這種騷操作蜀涨,漲見識了...

建議先讓后臺改。后臺不改自己再手動把 header 里的數(shù)據(jù)提取出來蝎毡,轉(zhuǎn)成自己想要的 json 數(shù)據(jù)厚柳。

class ConvertDataInterceptor : ResponseBodyInterceptor() {

  override fun intercept(response: Response, url: String, body: String): Response {
    val json = "{\"code\": 200}" // 創(chuàng)建自己需要的數(shù)據(jù)結(jié)構(gòu)
    val jsonObject = JSONObject(json)
    jsonObject.put("data", response.headers["Data"]) // 將 header 里的數(shù)據(jù)設(shè)置到 json 里
    
    val contentType = response.body?.contentType()
    val responseBody = jsonObject.toString().toResponseBody(contentType)
    return response.newBuilder().body(responseBody).build() // 重新生成響應(yīng)對象
  }
}

總結(jié)

大家遇到這些情況建議先與后臺人員溝通。剛開始說的失敗時 data 類型不一致的情況有不少人遇到過沐兵,有需要的可以提前處理預(yù)防一下别垮。至于那些更騷的操作最好還是和后臺溝通一個合適的規(guī)范,實(shí)在溝通無果再參考文中部分案例的處理思路扎谎。

自定義 GsonConverter 與源碼有不少冗余代碼碳想,并不推薦。而且如果想對某個接口的結(jié)果進(jìn)行處理毁靶,不好拿到該地址胧奔。攔截器的方式難點(diǎn)主要是該怎么寫,所以封裝好了工具類供大家使用预吆。

文中提到了用攔截器將數(shù)據(jù)轉(zhuǎn)換成方便我們編寫邏輯的結(jié)構(gòu)龙填,并不是鼓勵大家?guī)秃笈_擦屁股。這種用法或許對某些復(fù)雜的接口來說會有奇效拐叉。

剛開始只是打算分享自己封裝好的類岩遗,說一下怎么使用來解決問題。不過后來還是花了很多篇幅詳細(xì)描述了我解決問題的整個心路歷程凤瘦,主要是見過太多人求助這類問題宿礁,所以就寫詳細(xì)一點(diǎn),后續(xù)如果還有人問就直接發(fā)文章過去廷粒,應(yīng)該能有效解決他的疑惑窘拯。另外如果公司用的請求框架即不是 Retrofit 也不是基于 OkHttp 封裝的框架的話,通過本文章的解決問題思路應(yīng)該也能尋找到相應(yīng)的解決方案坝茎。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末涤姊,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子嗤放,更是在濱河造成了極大的恐慌思喊,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件次酌,死亡現(xiàn)場離奇詭異恨课,居然都是意外死亡舆乔,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門剂公,熙熙樓的掌柜王于貴愁眉苦臉地迎上來希俩,“玉大人,你說我怎么就攤上這事纲辽⊙瘴洌” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵拖吼,是天一觀的道長鳞上。 經(jīng)常有香客問我,道長吊档,這世上最難降的妖魔是什么篙议? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮怠硼,結(jié)果婚禮上鬼贱,老公的妹妹穿的比我還像新娘。我一直安慰自己拒名,他們只是感情好吩愧,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布芋酌。 她就那樣靜靜地躺著增显,像睡著了一般。 火紅的嫁衣襯著肌膚如雪脐帝。 梳的紋絲不亂的頭發(fā)上同云,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機(jī)與錄音堵腹,去河邊找鬼炸站。 笑死,一個胖子當(dāng)著我的面吹牛疚顷,可吹牛的內(nèi)容都是我干的旱易。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼腿堤,長吁一口氣:“原來是場噩夢啊……” “哼阀坏!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起笆檀,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤忌堂,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后酗洒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體士修,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡枷遂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了棋嘲。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片酒唉。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖沸移,靈堂內(nèi)的尸體忽然破棺而出黔州,到底是詐尸還是另有隱情,我是刑警寧澤阔籽,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布流妻,位于F島的核電站,受9級特大地震影響笆制,放射性物質(zhì)發(fā)生泄漏绅这。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一在辆、第九天 我趴在偏房一處隱蔽的房頂上張望证薇。 院中可真熱鬧,春花似錦匆篓、人聲如沸浑度。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽箩张。三九已至,卻和暖如春窗市,著一層夾襖步出監(jiān)牢的瞬間先慷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工咨察, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留论熙,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓摄狱,卻偏偏與公主長得像脓诡,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子媒役,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344