Room & Kotlin 符號(hào)的處理

△ 圖片來(lái)自 Unsplash 由 Marc Reichelt 提供

△ 圖片來(lái)自 Unsplash 由 Marc Reichelt 提供

Jetpack Room 庫(kù)在 SQLite 上提供了一個(gè)抽象層蝇率,能夠在沒(méi)有任何樣板代碼的情況下同辣,提供編譯時(shí)驗(yàn)證 SQL 查詢(xún)的能力紧唱。它通過(guò)處理代碼注解和生成 Java 源代碼的方式健无,實(shí)現(xiàn)上述行為圆米。

注解處理器非常強(qiáng)大,但它們會(huì)增加構(gòu)建時(shí)間蓝厌。這對(duì)于用 Java 寫(xiě)的代碼來(lái)說(shuō)通常是可以接受的角雷,但對(duì)于 Kotlin 而言,編譯時(shí)間消耗會(huì)非常明顯爆雹,這是因?yàn)?Kotlin 沒(méi)有一個(gè)內(nèi)置的注解處理管道停蕉。相反,它通過(guò) Kotlin 代碼生成了存根 Java 代碼來(lái)支持注解處理器钙态,然后將其輸送到 Java 編譯器中進(jìn)行處理慧起。

由于并不是所有 Kotlin 源代碼中的內(nèi)容都能用 Java 表示,因此有些信息會(huì)在這種轉(zhuǎn)換中丟失册倒。同樣蚓挤,Kotlin 是一種多平臺(tái)語(yǔ)言,但 KAPT 只在面向 Java 字節(jié)碼的情況下生效驻子。

認(rèn)識(shí) Kotlin 符號(hào)處理

隨著注解處理器在 Android 上的廣泛使用灿意,KAPT 成為了編譯時(shí)的性能瓶頸。為了解決這個(gè)問(wèn)題崇呵,Google Kotlin 編譯器團(tuán)隊(duì)開(kāi)始研究一個(gè)替代方案缤剧,來(lái)為 Kotlin 提供一流的注解處理支持。當(dāng)這個(gè)項(xiàng)目誕生之初域慷,我們非常激動(dòng)鞭执,因?yàn)樗鼘椭?Room 更好地支持 Kotlin司顿。從 Room 2.4 開(kāi)始芒粹,它對(duì) KSP 有了實(shí)驗(yàn)性的支持兄纺,我們發(fā)現(xiàn)編譯速度提高了 2 倍,特別是在全量編譯的情況下化漆。

本文內(nèi)容重點(diǎn)不在注解的處理估脆、Room 或者 KSP。而在于重點(diǎn)介紹我們?cè)跒?Room 添加 KSP 支持時(shí)所面臨的挑戰(zhàn)和所做的權(quán)衡座云。為了理解本文您并不需要了解 Room 或者 KSP疙赠,但必須熟悉注解處理。

注意: 我們?cè)?KSP 發(fā)布穩(wěn)定版之前就開(kāi)始使用它了朦拖。因此圃阳,尚不確定之前做的一些決策是否適用于現(xiàn)在。

本篇文章旨在讓注解處理器的作者們?cè)跒轫?xiàng)目添加 KSP 支持前璧帝,充分了解需要注意的問(wèn)題捍岳。

Room 工作原理簡(jiǎn)介

Room 的注解處理分為兩個(gè)步驟。有一些 "Processor" 類(lèi)睬隶,它們遍歷用戶(hù)的代碼锣夹,驗(yàn)證并提取必要的信息到 "值對(duì)象" 中。這些值對(duì)象被送到 "Writer" 類(lèi)中苏潜,這些類(lèi)將它們轉(zhuǎn)換為代碼银萍。和其他諸多的注解處理器一樣,Room 非常依賴(lài) Auto-Commonjavax.lang.model 包 (Java 注解處理 API 包) 中頻繁引用的類(lèi)恤左。

為了支持 KSP贴唇,我們有三種選擇:

  1. 復(fù)制 JavaAP 和 KSP 的每個(gè) "Processor" 類(lèi),它們會(huì)有相同的值對(duì)象作為輸出飞袋,我們可以將其輸入到 Writer 中戳气;
  2. 在 KSP/Java AP 之上創(chuàng)建一個(gè)抽象層,以便處理器擁有一個(gè)基于該抽象層的實(shí)現(xiàn)授嘀;
  3. 用 KSP 代替 JavaAP物咳,并要求開(kāi)發(fā)者也使用 KSP 來(lái)處理 Java 代碼。

選項(xiàng) C 實(shí)際上是不可行的蹄皱,因?yàn)樗鼤?huì)對(duì) Java 用戶(hù)造成嚴(yán)重的干擾览闰。隨著 Room 使用數(shù)量的增加,這種破壞性的改變是不可能的巷折。在 "A" 和 "B" 兩者之間压鉴,我們決定選擇 "B",因?yàn)樘幚砥骶哂邢喈?dāng)數(shù)量的業(yè)務(wù)邏輯锻拘,將其分解并非易事油吭。

認(rèn)識(shí) X-Processing

在 JavaAP 和 KSP 上創(chuàng)建一個(gè)通用的抽象并非易事击蹲。Kotlin 和 Java 可以互操作,但模式卻不相同婉宰,例如歌豺,Kotlin 中特殊類(lèi)的類(lèi)型如 Kotlin 的值類(lèi)或者 Java 中的靜態(tài)方法。此外心包,Java 類(lèi)中有字段和方法类咧,而 Kotlin 中有屬性和函數(shù)。

我們決定實(shí)現(xiàn) "Room 需要什么"蟹腾,而不是嘗試去追求完美的抽象痕惋。從字面意思來(lái)看,在 Room 中找到導(dǎo)入了 javax.lang.model 的每一個(gè)文件娃殖,并將其移動(dòng)到 X-Processing 的抽象中值戳。這樣一來(lái),TypeElement 變成了 XTypeElement炉爆,ExecutableElemen 變成了 XExecutableElemen 等等堕虹。

遺憾的是,javax.lang.model API 在 Room 中的應(yīng)用非常廣泛叶洞。一次性創(chuàng)建所有這些 X 類(lèi)鲫凶,會(huì)給審閱者帶來(lái)非常嚴(yán)重的心理負(fù)擔(dān)。因此衩辟,我們需要找到一種方法來(lái)迭代這一實(shí)現(xiàn)螟炫。

另一方面,我們需要證明這是可行的艺晴。所以我們首先對(duì)其做了 原型 設(shè)計(jì)昼钻,一旦驗(yàn)證這是一個(gè)合理的選擇,我們就用他們自己的測(cè)試 逐一重新實(shí)現(xiàn)了所有 X 類(lèi)封寞。

關(guān)于我說(shuō)的實(shí)現(xiàn) "Room 需要什么"然评,有一個(gè)很好的例子,我們可以在關(guān)于類(lèi)的字段 更改 中看到狈究。當(dāng) Room 處理一個(gè)類(lèi)的字段時(shí)碗淌,它總是對(duì)其所有的字段感興趣,包括父類(lèi)中的字段抖锥。所以我們?cè)趧?chuàng)建相應(yīng)的 X-Processing API 時(shí)亿眠,添加了獲取所有字段的能力。

interface XTypeElement {
  fun getAllFieldsIncludingPrivateSupers(): List<XVariableElement>
}

如果我們正在設(shè)計(jì)一個(gè)通用庫(kù)磅废,這樣可能永遠(yuǎn)不會(huì)通過(guò) API 審查纳像。但因?yàn)槲覀兊哪繕?biāo)只是 Room,并且它已經(jīng)有一個(gè)與 TypeElement 具有相同功能的輔助方法拯勉,所以復(fù)制它可以減少項(xiàng)目的風(fēng)險(xiǎn)竟趾。

一旦我們有了基本的 X-Processing API 和它們的測(cè)試方法憔购,下一步就是讓 Room 來(lái)調(diào)用這個(gè)抽象。這也是 "實(shí)現(xiàn) Room 所需要的東西" 獲得良好回報(bào)的地方岔帽。Room 在 javax.lang.model API 上已經(jīng)擁有了用于基本功能的擴(kuò)展函數(shù)/屬性 (例如獲取 TypeElement 的方法)玫鸟。我們首先更新了這些擴(kuò)展,使其看起來(lái)與 X-Processing API 類(lèi)似山卦,然后在 1 CL 中將 Room 遷移到 X-Processing鞋邑。

改進(jìn) API 可用性

保留類(lèi)似 JavaAP 的 API 并不意味著我們不能改進(jìn)任何東西。在將 Room 遷移到 X-Processing 之后账蓉,我們又實(shí)現(xiàn)了一系列的 API 改進(jìn)。

例如逾一,Room 多次調(diào)用 MoreElement/MoreTypes铸本,以便在不同的 javax.lang.model 類(lèi)型 (例如 MoreElements.asType) 之間進(jìn)行轉(zhuǎn)換。相關(guān)調(diào)用通常如下所示:

val element: Element ...
if (MoreElements.isType(element)) {
  val typeElement:TypeElement = MoreElements.asType(element)
}

我們把所有的調(diào)用放到了 Kotlin contracts 中遵堵,這樣一來(lái)就可以寫(xiě)成:

val element: XElement ...
if (element.isTypeElement()) {
  // 編譯器識(shí)別到元素是一個(gè) XTypeElement
}

另一個(gè)很好的例子是在一個(gè) TypeElement 中找尋方法箱玷。通常在 JavaAP 中,您需要調(diào)用 ElementFilter 類(lèi)來(lái)獲取 TypeElement 中的方法陌宿。與此相反锡足,我們直接將其設(shè)為 XTypeElement 中的一個(gè)屬性。

// 前
val methods = ElementFilter.methodsIn(typeElement.enclosedElements)
// 后
val methods = typeElement.declaredMethods

最后一個(gè)例子壳坪,這也可能是我最喜歡的例子之一舶得,就是可分配性。在 JavaAP 中爽蝴,如果您要檢查給定的 TypeMirror 是否可以由另一個(gè) TypeMirror 賦值沐批,則需要調(diào)用 Types.isAssignable

val type1: TypeMirror ...
val type2: TypeMirror ...
if (typeUtils.isAssignable(type1, type2)) {
  ...
}

這段代碼真的很難讀懂蝎亚,因?yàn)槟踔翢o(wú)法猜到它是否驗(yàn)證了類(lèi)型 1 可以由類(lèi)型 2 指定九孩,亦或是完全相反的結(jié)果。我們已經(jīng)有一個(gè)擴(kuò)展函數(shù)如下:

fun TypeMirror.isAssignableFrom(
  types: Types,
  otherType: TypeMirror
): Boolean

在 X-Processing 中发框,我們能夠?qū)⑵滢D(zhuǎn)換為 XType 上的常規(guī)函數(shù)躺彬,如下方所示:

interface XType {
  fun isAssignableFrom(other: XType): Boolean
}

為 X-Processing 實(shí)現(xiàn) KSP 后端

這些 X-Processing 接口每個(gè)都有自己的測(cè)試套件。我們編寫(xiě)它們并非是用來(lái)測(cè)試 AutoCommon 或者 JavaAP 的梅惯,相反宪拥,編寫(xiě)它們是為了在有了它們的 KSP 實(shí)現(xiàn)時(shí),我們就可以運(yùn)行測(cè)試用例來(lái)驗(yàn)證它是否符合 Room 的預(yù)期个唧。

由于最初的 X-Processing API 是按照 avax.lang.model 建模江解,它們并非每次都適用于 KSP,所以我們也改進(jìn)了這些 API徙歼,以便在需要時(shí)為 Kotlin 提供更好的支持犁河。

這樣產(chǎn)生了一個(gè)新問(wèn)題”钫恚現(xiàn)有的 Room 代碼庫(kù)是為了處理 Java 源代碼而寫(xiě)的。當(dāng)應(yīng)用是由 Kotlin 編寫(xiě)時(shí)桨螺,Room 只能識(shí)別該 Kotlin 在 Java 存根中的樣子宾符。我們決定在 X-Processing 的 KSP 實(shí)現(xiàn)中保持類(lèi)似行為。

例如灭翔,Kotlin 中的 suspend 函數(shù)在編譯時(shí)生成如下簽名:

// kotlin
suspend fun foo(bar:Bar):Baz
// java
Object foo(bar:Bar, Continuation<? extends Baz>)

為保持相同的行為魏烫,KSP 中的 XMethodElement 實(shí)現(xiàn)為 suspend 方法合成了一個(gè)新參數(shù),以及新的返回類(lèi)型肝箱。(KspMethodElement.kt)

注意: 這樣做效果很好哄褒,因?yàn)?Room 生成的是 Java 代碼,即使在 KSP 中也是如此煌张。當(dāng)我們添加對(duì) Kotlin 代碼生成的支持時(shí)呐赡,可能會(huì)引起一些變化。

另一個(gè)例子與屬性有關(guān)骏融。Kotlin 屬性也可能具有基于其簽名的合成 getter/setter (訪問(wèn)器)链嘀。由于 Room 期望找到這些訪問(wèn)器作為方法 (參見(jiàn): KspTypeElement.kt),因此 XTypeElement 實(shí)現(xiàn)了這些合成方法档玻。

注意 : 我們已有計(jì)劃更改 XTypeElement API 以提供屬性而非字段怀泊,因?yàn)檫@才是 Room 真正想要獲取的內(nèi)容。正如您現(xiàn)在猜到的那樣误趴,我們決定 "暫時(shí)" 不這樣做來(lái)減少 Room 的修改霹琼。希望有一天我們能夠做到這一點(diǎn),當(dāng)我們這樣做時(shí)冤留,XTypeElement 的 JavaAP 實(shí)現(xiàn)將會(huì)把方法和字段作為屬性捆綁在一起碧囊。

在為 X-Processing 添加 KSP 實(shí)現(xiàn)時(shí),最后一個(gè)有趣的問(wèn)題是 API 耦合纤怒。這些處理器的 API 經(jīng)常相互訪問(wèn)糯而,因此如果不實(shí)現(xiàn) XField / XMethod,就不能在 KSP 中實(shí)現(xiàn) XTypeElement泊窘,而 XField / XMethod 本身又引用了 XType 等等熄驼。在添加這些 KSP 實(shí)現(xiàn)的同時(shí),我們?yōu)樗鼈兊膶?shí)現(xiàn)部分寫(xiě)了單獨(dú)的測(cè)試用例烘豹。當(dāng) KSP 的實(shí)現(xiàn)變得更加完整時(shí)瓜贾,我們逐漸通過(guò) KSP 后端啟動(dòng)全部的 X-Processing 測(cè)試。

需要注意的是携悯,在此階段我們只在 X-Processing 項(xiàng)目中運(yùn)行測(cè)試祭芦,所以即使我們知道測(cè)試的內(nèi)容沒(méi)問(wèn)題,我們也無(wú)法保證所有的 Room 測(cè)試都能通過(guò) (也稱(chēng)之為單元測(cè)試 vs 集成測(cè)試)憔鬼。我們需要通過(guò)一種方法來(lái)使用 KSP 后端運(yùn)行所有的 Room 測(cè)試龟劲,"X-Processing-Testing" 就應(yīng)運(yùn)而生胃夏。

認(rèn)識(shí) X-Processing-Testing

注解處理器的編寫(xiě)包含 20% 的處理器代碼和 80% 的測(cè)試代碼。您需要考慮到各種可能的開(kāi)發(fā)者錯(cuò)誤昌跌,并確保如實(shí)報(bào)告錯(cuò)誤消息仰禀。為了編寫(xiě)這些測(cè)試,Room 已經(jīng)提供一個(gè)輔助方法如下:

fun runTest(
  vararg javaFileObjects: JavaFileObject,
  process: (TestInvocation) -> Unit
): CompilationResult

runTest 在底層使用了 Google Compile Testing 庫(kù)蚕愤,并允許我們簡(jiǎn)單地對(duì)處理器進(jìn)行單元測(cè)試答恶。它合成了一個(gè) Java 注解處理器并在其中調(diào)用了處理器提供的 process 方法。

val entitySource : JavaFileObject //示例 @Entity 注釋類(lèi)
val result = runTest(entitySource) { invocation ->
  val element = invocation.processingEnv.findElement("Subject")
  val entityValueObject = EntityProcessor(...).process(element)
  // 斷言 entityValueObject
}
// 斷言結(jié)果是否有誤萍诱,警告等

糟糕的是悬嗓,Google Compile Testing 僅支持 Java 源代碼。為了測(cè)試 Kotlin 我們需要另一個(gè)庫(kù)砂沛,幸運(yùn)的是有 Kotlin Compile Testing烫扼,它允許我們編寫(xiě)針對(duì) Kotlin 的測(cè)試,而且我們?yōu)樵搸?kù)貢獻(xiàn)了對(duì) KSP 支持碍庵。

注意 : 我們后來(lái)用 內(nèi)部實(shí)現(xiàn) 替換了 Kotlin Compile Testing,以簡(jiǎn)化 AndroidX Repo 中的 Kotlin/KSP 更新悟狱。我們還添加了更好的斷言 API静浴,這需要我們對(duì) KCT 執(zhí)行 API 不兼容的修改操作。

作為能讓 KSP 運(yùn)行所有測(cè)試的最后一步挤渐,我們創(chuàng)建了以下測(cè)試 API:

fun runProcessorTest(
  sources: List<Source>,
  handler: (XTestInvocation) -> Unit
): Unit

這個(gè)和原始版本之間的主要區(qū)別在于苹享,它同時(shí)通過(guò) KSP 和 JavaAP (或 KAPT,取決于來(lái)源) 運(yùn)行測(cè)試浴麻。因?yàn)樗啻芜\(yùn)行測(cè)試且 KSP 和 JavaAP 兩者的判斷結(jié)果不同得问,因此無(wú)法返回單個(gè)結(jié)果。

因此软免,我們想到了一個(gè)辦法:

fun XTestInvocation.assertCompilationResult(
  assertion: (XCompilationResultSubject) -> Unit
}

每次編譯后宫纬,它都會(huì)調(diào)用結(jié)果斷言 (如果沒(méi)有失敗提示,則檢查編譯是否成功)膏萧。我們把每個(gè) Room 測(cè)試重構(gòu)為如下所示:

val entitySource : Source //示例 @Entity 注釋類(lèi)
runProcessorTest(listOf(entitySource)) { invocation ->
  // 該代碼塊運(yùn)行兩次漓骚,一次使用 JavaAP/KAPT,一次使用 KSP
  val element = invocation.processingEnv.findElement("Subject")
  val entityValueObject = EntityProcessor(...).process(element)
  //  斷言 entityValueObject
  invocation.assertCompilationResult {
    // 結(jié)果被斷言為是否有 error榛泛,warning 等
    hasWarningContaining("...")
  }
}

接下來(lái)的事情就很簡(jiǎn)單了蝌蹂。將每個(gè) Room 的編譯測(cè)試遷移到新的 API,一旦發(fā)現(xiàn)新的 KSP / X-Processing 錯(cuò)誤曹锨,就會(huì)上報(bào)孤个,然后實(shí)施臨時(shí)解決方案;這一動(dòng)作反復(fù)進(jìn)行沛简。由于 KSP 正在大力開(kāi)發(fā)中齐鲤,我們確實(shí)遇到了很多 bug斥废。每一次我們都會(huì)上報(bào) bug,從 Room 源鏈接到它佳遂,然后繼續(xù)前進(jìn) (或者進(jìn)行修復(fù))营袜。每當(dāng) KSP 發(fā)布之后,我們都會(huì)搜索代碼庫(kù)來(lái)找到已修復(fù)的問(wèn)題丑罪,刪除臨時(shí)解決方案并啟動(dòng)測(cè)試荚板。

一旦編譯測(cè)試覆蓋情況較好,我們?cè)谙乱徊骄蜁?huì)使用 KSP 運(yùn)行 Room 的 集成測(cè)試吩屹。這些是實(shí)際的 Android 測(cè)試應(yīng)用毁腿,也會(huì)在運(yùn)行時(shí)測(cè)試其行為。幸運(yùn)的是奸鸯,Android 支持 Gradle 變體凡伊,因此使用 KSP 和 KAPT 來(lái)運(yùn)行我們 Kotlin 集成測(cè)試 便相當(dāng)容易。

下一步

將 KSP 支持添加到 Room 只是第一步〔炼埽現(xiàn)在嘲驾,我們需要更新 Room 來(lái)使用它。例如迹卢,Room 中的所有類(lèi)型檢查都忽略了 nullability辽故,因?yàn)?code>javax.lang.model 的 TypeMirror 并不理解 nullability。因此腐碱,當(dāng)調(diào)用您的 Kotlin 代碼時(shí)誊垢,Room 有時(shí)會(huì)在運(yùn)行時(shí)觸發(fā) NullPointerException。有了 KSP症见,這些檢查現(xiàn)在可在 Room 中創(chuàng)建新的 KSP bug (例如 b/193437407)喂走。我們已經(jīng)添加了一些臨時(shí)解決方案,但理想情況下谋作,我們?nèi)韵M?改進(jìn) Room 以正確處理這些情況芋肠。

同樣,即使我們支持 KSP瓷们,Room 仍然只生成 Java 代碼业栅。這種限制使我們無(wú)法添加對(duì)某些 Kotlin 特性的支持,比如 Value Classes谬晕。希望在將來(lái)碘裕,我們還能對(duì)生成 Kotlin 代碼提供一些支持,以便在 Room 中為 Kotlin 提供一流的支持攒钳。接下來(lái)帮孔,也許更多 :)。

我能在我的項(xiàng)目上使用 X-Processing 嗎?

答案是還不能;至少與您使用任何其他 Jetpack 庫(kù)的方式不同文兢。如前文所述晤斩,我們只實(shí)現(xiàn)了 Room 需要的部分。編寫(xiě)一個(gè)真正的 Jetpack 庫(kù)有很大的投入姆坚,比如文檔澳泵、API 穩(wěn)定性、Codelabs 等兼呵,我們無(wú)法承擔(dān)這些工作兔辅。話(huà)雖如此,Dagger 和 Airbnb (Paris击喂、DeeplinkDispatch) 都開(kāi)始用 X-Processing 來(lái)支持 KSP (并貢獻(xiàn)了他們需要的東西??)维苔。也許有一天我們會(huì)把它從 Room 中分解出來(lái)。從技術(shù)層面上講懂昂,您仍然可以像使用 Google Maven 庫(kù) 一樣使用它介时,但是沒(méi)有 API 保證可以這樣做,因此您絕對(duì)應(yīng)該使用 shade 技術(shù)凌彬。

總結(jié)

我們?yōu)?Room 添加了 KSP 支持沸柔,這并非易事但絕對(duì)值得。如果您在維護(hù)注解處理器铲敛,請(qǐng)?zhí)砑訉?duì) KSP 的支持勉失,以提供更好的 Kotlin 開(kāi)發(fā)者體驗(yàn)。

特別感謝 Zac SweersEli Hart 審校這篇文章的早期版本原探,他們同時(shí)也是優(yōu)秀的 KSP 貢獻(xiàn)者。

更多資源

歡迎您 點(diǎn)擊這里 向我們提交反饋顽素,或分享您喜歡的內(nèi)容咽弦、發(fā)現(xiàn)的問(wèn)題。您的反饋對(duì)我們非常重要胁出,感謝您的支持型型!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市全蝶,隨后出現(xiàn)的幾起案子闹蒜,更是在濱河造成了極大的恐慌,老刑警劉巖抑淫,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件绷落,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡始苇,警方通過(guò)查閱死者的電腦和手機(jī)砌烁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人函喉,你說(shuō)我怎么就攤上這事避归。” “怎么了管呵?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵梳毙,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我捐下,道長(zhǎng)账锹,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任蔑担,我火速辦了婚禮牌废,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘啤握。我一直安慰自己鸟缕,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布排抬。 她就那樣靜靜地躺著懂从,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蹲蒲。 梳的紋絲不亂的頭發(fā)上番甩,一...
    開(kāi)封第一講書(shū)人閱讀 51,562評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音届搁,去河邊找鬼缘薛。 笑死,一個(gè)胖子當(dāng)著我的面吹牛卡睦,可吹牛的內(nèi)容都是我干的宴胧。 我是一名探鬼主播,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼表锻,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼恕齐!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起瞬逊,我...
    開(kāi)封第一講書(shū)人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤显歧,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后确镊,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體士骤,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年骚腥,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了敦间。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖廓块,靈堂內(nèi)的尸體忽然破棺而出厢绝,到底是詐尸還是另有隱情,我是刑警寧澤带猴,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布昔汉,位于F島的核電站,受9級(jí)特大地震影響拴清,放射性物質(zhì)發(fā)生泄漏靶病。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一口予、第九天 我趴在偏房一處隱蔽的房頂上張望娄周。 院中可真熱鬧,春花似錦沪停、人聲如沸煤辨。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)众辨。三九已至,卻和暖如春舷礼,著一層夾襖步出監(jiān)牢的瞬間鹃彻,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工妻献, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蛛株,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓育拨,卻偏偏與公主長(zhǎng)得像泳挥,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子至朗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

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