FastKV:一個(gè)真的很快的KV存儲(chǔ)庫(kù)

一嘹裂、前言

KV存儲(chǔ)無(wú)論對(duì)于客戶(hù)端還是服務(wù)端都是重要的構(gòu)件妄壶。
對(duì)于Android客戶(hù)端而言,最常見(jiàn)的莫過(guò)于SDK提供的SharePreferences(以下簡(jiǎn)稱(chēng)SP)寄狼,但其低效率和ANR問(wèn)題飽受詬病丁寄。
官方后來(lái)又推出了基于Kotlin的DataStore泊愧,不過(guò)測(cè)試下來(lái)發(fā)現(xiàn)寫(xiě)入效率很低。
微信開(kāi)源了MMKV删咱,寫(xiě)入速度比前者高不少,但是讀取相對(duì)較慢摘能,同時(shí)也存在其他一些缺點(diǎn)续崖。

1.1 SP的不足

關(guān)于SP的缺點(diǎn)網(wǎng)上有不少討論袜刷,這里主要提兩個(gè)點(diǎn):

  • 保存速度較慢
    SP用內(nèi)存層用HashMap保存莺丑,磁盤(pán)層則是用的XML文件保存。
    每次更改梢莽,都需要將整個(gè)HashMap序列化為XML格式的報(bào)文然后整個(gè)寫(xiě)入文件萧豆。
    歸結(jié)其較慢的原因:
    1、不能增量寫(xiě)入昏名;
    2涮雷、序列化比較耗時(shí)。

  • 可以能會(huì)導(dǎo)致ANR

public void apply() {
    // ...省略無(wú)關(guān)代碼...
    QueuedWork.addFinisher(awaitCommit);
    Runnable postWriteRunnable = new Runnable() {
        @Override
        public void run() {
            awaitCommit.run();
            QueuedWork.removeFinisher(awaitCommit);
        }
    };
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
}
public void handleStopActivity(IBinder token, boolean show, int configChanges,
                               PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
    // ...省略無(wú)關(guān)代碼...
    // Make sure any pending writes are now committed.
    if (!r.isPreHoneycomb()) {
        QueuedWork.waitToFinish();
    }
}

Activity stop時(shí)會(huì)等待SP的寫(xiě)入任務(wù)轻局,如果SP的寫(xiě)入任務(wù)多且執(zhí)行慢的話(huà)洪鸭,可能會(huì)阻塞主線(xiàn)程較長(zhǎng)時(shí)間,輕則卡頓仑扑,重則ANR览爵。

1.2 MMKV的不足

  • 沒(méi)有類(lèi)型信息,不支持getAll
    MMKV的存儲(chǔ)用類(lèi)似于Protobuf的編碼方式镇饮,只存儲(chǔ)key和value本身蜓竹,沒(méi)有存類(lèi)型信息(Protobuf用tag標(biāo)記字段,信息更少)储藐。
    由于沒(méi)有記錄類(lèi)型信息俱济,MMKV無(wú)法自動(dòng)反序列化,也就無(wú)法實(shí)現(xiàn)getAll接口钙勃。

  • 讀取相對(duì)較慢
    SP在加載的時(shí)候已經(jīng)將value反序列化存在HashMap中了蛛碌,讀取的時(shí)候索引到之后就能直接引用了。
    而MMKV每次讀取時(shí)都需要重新解碼辖源,除了時(shí)間上的消耗之外蔚携,還需要每次都創(chuàng)建新的對(duì)象。
    不過(guò)這不是大問(wèn)題同木,相對(duì)SP沒(méi)有差很多浮梢。

  • 需要引入so, 增加包體積
    引入MMKV需要增加的體積還是不少的跛十,且不說(shuō)jar包和aidl文件彤路,光是一個(gè)arm64-v8a的so就有四百多K。

    雖然說(shuō)現(xiàn)在APP體積都不小洲尊,但畢竟增加體積對(duì)打包坞嘀、分發(fā)和安裝時(shí)間都多少有些影響丽涩。

  • 文件只增不減
    MMKV的擴(kuò)容策略還是比較激進(jìn)的继准,而且擴(kuò)容之后不會(huì)主動(dòng)trim size移必。
    比方說(shuō)崔泵,假如有一個(gè)大value憎瘸,讓其擴(kuò)容至1M含思,后面刪除該value含潘,后面即使觸發(fā)GC遏弱,哪怕有效內(nèi)容有幾K漱逸,文件大小還是保持在1M饰抒。

  • 可能會(huì)丟失數(shù)據(jù)
    前面的問(wèn)題總的來(lái)說(shuō)都不是什么“要緊”的問(wèn)題袋坑,但是這個(gè)丟失數(shù)據(jù)確實(shí)是硬傷婆誓。
    MMKV官方有這么一段表述:

    通過(guò) mmap 內(nèi)存映射文件洋幻,提供一段可供隨時(shí)寫(xiě)入的內(nèi)存塊文留,App 只管往里面寫(xiě)數(shù)據(jù)厂庇,由操作系統(tǒng)負(fù)責(zé)將內(nèi)存回寫(xiě)到文件权旷,不必?fù)?dān)心 crash 導(dǎo)致數(shù)據(jù)丟失拄氯。

這個(gè)表述對(duì)一半不對(duì)一半译柏。
如果數(shù)據(jù)完成寫(xiě)入到內(nèi)存塊鄙麦,如果系統(tǒng)不崩潰,即使進(jìn)程崩潰恨胚,系統(tǒng)也會(huì)將buffer刷入磁盤(pán)寒波;
但是如果在刷入磁盤(pán)之前發(fā)生系統(tǒng)崩潰或者斷電等俄烁,數(shù)據(jù)就丟失了页屠,不過(guò)這種情況發(fā)生的概率不大矛双;
另一種情況是數(shù)據(jù)寫(xiě)一半的時(shí)候進(jìn)程崩潰或者被殺死蟆豫,然后系統(tǒng)會(huì)將已寫(xiě)入的部分刷入磁盤(pán),再次打開(kāi)時(shí)文件可能就不完整了帮辟。
例如玩焰,MMKV在剩余空間不足時(shí)會(huì)回收無(wú)效的空間昔园,如果這期間進(jìn)程中斷默刚,數(shù)據(jù)可能會(huì)不完整澜搅。
MMKV官方的說(shuō)明可以佐證:

CRC校驗(yàn)失敗之后勉躺,MMKV有兩種應(yīng)對(duì)策略:直接丟棄所有數(shù)據(jù),或者嘗試讀取數(shù)據(jù)(用戶(hù)可以在初始化時(shí)設(shè)定)概说。
嘗試讀取數(shù)據(jù)不一定能恢復(fù)數(shù)據(jù)糖赔,甚至可能會(huì)讀到一些錯(cuò)誤的數(shù)據(jù)放典,得看運(yùn)氣壳影。

這個(gè)過(guò)程是比較容易復(fù)現(xiàn)的,下面是其中一種復(fù)現(xiàn)路徑:

  1. 新增和刪除若干key-value
    得到數(shù)據(jù)如下:
  1. 插入一個(gè)大字符串掺栅,觸發(fā)擴(kuò)容氧卧,擴(kuò)容前會(huì)觸發(fā)垃圾回收

  2. 斷點(diǎn)打在執(zhí)行memmove的循環(huán)中沙绝,執(zhí)行一部分memmove, 然后在手機(jī)上殺死進(jìn)程


  3. 再次打開(kāi)APP,數(shù)據(jù)丟失

相比之下谬以,SP雖然低效为黎,但至少有相應(yīng)的機(jī)制確保數(shù)據(jù)完整性,頂多可能會(huì)丟失最新的update炕檩;
而MMKV則有可能會(huì)丟失整個(gè)文件的數(shù)據(jù)笛质。

二、FastKV

在總結(jié)了之前的經(jīng)驗(yàn)和感悟之后敲霍,筆者實(shí)現(xiàn)了一個(gè)高效且可靠的版本柴我,且將其命名為: FastKV艘儒。

2.1 特性

FastKV有以下特性:

  1. 讀寫(xiě)速度快
    • FastKV采用二進(jìn)制編碼彤悔,編碼后的體積相對(duì)XML等文本編碼要小很多。
    • 增量編碼:FastKV記錄了各個(gè)key-value相對(duì)文件的偏移量,更新數(shù)據(jù)時(shí)敞斋,可以直接在對(duì)應(yīng)的位置寫(xiě)入數(shù)據(jù)截汪。
    • 默認(rèn)用mmap的方式記錄數(shù)據(jù),更新數(shù)據(jù)時(shí)直接寫(xiě)入到內(nèi)存即可植捎,沒(méi)有IO阻塞衙解。
  2. 支持多種寫(xiě)入模式
    • 除了mmap這種非阻塞的寫(xiě)入方式,F(xiàn)astKV也支持常規(guī)的阻塞式寫(xiě)入方式焰枢,
      并且支持同步阻塞和異步阻塞(分別類(lèi)似于SharePreferences的commit和apply)蚓峦。
  3. 支持多種類(lèi)型
    • 支持常用的boolean/int/float/long/double/String等基礎(chǔ)類(lèi)型。
    • 支持ByteArray (byte[])济锄。
    • 支持存儲(chǔ)自定義對(duì)象暑椰。
    • 內(nèi)置Set<String>的編碼器 (為了方便兼容SharePreferences)。
  4. 支持多進(jìn)程
    • 項(xiàng)目提供了支持多進(jìn)程的存儲(chǔ)類(lèi)(MPFastKV)。
    • 支持監(jiān)聽(tīng)文件內(nèi)容變化妇穴,其中一個(gè)進(jìn)程修改文件瞒滴,所有進(jìn)程皆可感知。
  5. 方便易用
    • FastKV提供了了豐富的API接口琼蚯,開(kāi)箱即用峦睡。
    • 提供的接口其中包括getAll()和putAll()方法粱快,
      所以遷移SharePreferences等框架的數(shù)據(jù)到FastKV很方便,當(dāng)然蓄坏,遷移FastKV的數(shù)據(jù)到其他框架也很方便渔彰。
  6. 穩(wěn)定可靠
    • 通過(guò)double-write等方法確保數(shù)據(jù)的完整性。
    • 在API拋IO異常時(shí)自動(dòng)降級(jí)處理汞斧。
  7. 代碼精簡(jiǎn)
    • FastKV由純Java實(shí)現(xiàn),編譯成jar包后體積只有幾十K啡邑。

2.2 實(shí)現(xiàn)原理

2.2.1 編碼

文件的布局:

[data_len | checksum | key-value | key-value|....]

  • data_len: 占4字節(jié), 記錄所有key-value所占字節(jié)數(shù)。
  • checksum: 占8字節(jié)耘子,記錄key-value部分的checksum。

key-value的數(shù)據(jù)布局:

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| delete_flag | external_flag | type  | key_len | key_content |  value  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     1bit    |      1bit     | 6bits |  1 byte |             |         |
  • delete_flag :標(biāo)記當(dāng)前key-value是否刪除弓摘。

  • external_flag: 標(biāo)記value部分是否寫(xiě)到額外的文件渊啰。
    注:對(duì)于數(shù)據(jù)量比較大的value,放在主文件一者占用內(nèi)存杆煞,二者會(huì)影響其他key-value的訪問(wèn)性能凳寺,因此闻书,單獨(dú)用一個(gè)文件來(lái)保存該value, 并在主文件中記錄其文件名晃择。

  • type: value類(lèi)型宫屠,目前支持boolean/int/float/long/double/String/ByteArray以及自定義對(duì)象创葡。

  • key_len: 記錄key的長(zhǎng)度,key_len本身占1字節(jié),所以支持key的最大長(zhǎng)度為255菇晃。

  • key_content: key的內(nèi)容本身册倒,utf8編碼。

  • value: 基礎(chǔ)類(lèi)型的value, 直接編碼(little-end)磺送;
    其他類(lèi)型驻子,先記錄長(zhǎng)度(用varint編碼),再記錄內(nèi)容估灿。
    String采用UTF-8編碼崇呵,ByteArray無(wú)需編碼,自定義對(duì)象實(shí)現(xiàn)Encoder接口馅袁,分別在Encoder的encode/decode方法中序列化和反序列化域慷。

2.2.2 存儲(chǔ)

  • mmap
    為了提高寫(xiě)入性能,F(xiàn)astKV默認(rèn)采用mmap的方式寫(xiě)入汗销。
  • 降級(jí)
    當(dāng)mmap API發(fā)生IO異常時(shí)犹褒,降級(jí)到常規(guī)的blocking I/O,同時(shí)為了不影響當(dāng)前線(xiàn)程弛针,會(huì)將寫(xiě)入放到異步線(xiàn)程中執(zhí)行叠骑。
  • 數(shù)據(jù)完整性
    如果在寫(xiě)入一部分的過(guò)程中發(fā)生中斷(進(jìn)程或系統(tǒng)),則文件可能會(huì)不完整削茁。
    故此宙枷,需要用一些方法確保數(shù)據(jù)的完整性。
    當(dāng)用mmap的方式打開(kāi)時(shí)茧跋,F(xiàn)astKV采用double-write的方式:數(shù)據(jù)依次寫(xiě)入A/B兩個(gè)文件(如果寫(xiě)入A過(guò)程中崩潰慰丛,B仍是完整的,如果A完整寫(xiě)入了瘾杭,則B寫(xiě)入時(shí)崩潰也不要緊)诅病;
    加載數(shù)據(jù)時(shí),通過(guò)checksum、標(biāo)記睬隶、數(shù)據(jù)合法性檢驗(yàn)等方法驗(yàn)證文件是否完整锣夹,若其中一個(gè)文件是損壞的,則用完整的文件覆蓋之苏潜。
    double-write可以防止進(jìn)程崩潰后數(shù)據(jù)不完整银萍,但由于mmap是系統(tǒng)定時(shí)刷盤(pán),若在刷盤(pán)前系統(tǒng)崩潰或者斷電恤左,仍會(huì)丟失未落盤(pán)的更新(之前的數(shù)據(jù)還在)贴唇;對(duì)于非常重要的key-value,在寫(xiě)入后飞袋,可接著調(diào)用force()強(qiáng)制將臟頁(yè)刷盤(pán)戳气。
  • 更新策略(增/刪/改)
    新增:寫(xiě)入到數(shù)據(jù)的尾部。
    刪除:delete_flag設(shè)置為1巧鸭。
    修改:如果value部分的長(zhǎng)度和原來(lái)一樣瓶您,則直接寫(xiě)入原來(lái)的位置;
    否則纲仍,先寫(xiě)入key-value到數(shù)據(jù)尾部呀袱,再標(biāo)記原來(lái)位置的delete_flag為1(刪除),最后再更新文件的data_len和checksum郑叠。
  • gc/truncate
    刪除key-value時(shí)會(huì)收集信息(統(tǒng)計(jì)刪除的個(gè)數(shù)夜赵,以及所在位置,占用空間等)乡革。
    GC的觸發(fā)時(shí)機(jī):
    1寇僧、新增key-value時(shí)剩余空間不足,且已刪除的空間達(dá)到閾值沸版,且騰出刪除空間后足夠?qū)懭氘?dāng)前key-value, 則觸發(fā)GC嘁傀;
    2、刪除key-value時(shí)推穷,如果刪除空間達(dá)到閾值心包,或者刪除的key-value個(gè)數(shù)達(dá)到閾值,則觸發(fā)GC馒铃。
    GC后如果空閑的空間達(dá)到設(shè)定閾值,則觸發(fā)truncate(縮小文件大泻弁铩)区宇。
  • 多進(jìn)程支持
    FileLock實(shí)現(xiàn)進(jìn)程互斥,F(xiàn)ileObserver實(shí)現(xiàn)文件變更監(jiān)聽(tīng)值戳。
    A文件mmap寫(xiě)入议谷,內(nèi)存共享;B文件FileChannel寫(xiě)入堕虹,觸發(fā)FileObserver回調(diào)(寫(xiě)入mmap不會(huì)出觸發(fā)FileObserver回調(diào))卧晓。

2.3 使用方法

2.3.1 導(dǎo)入

dependencies {
    implementation 'io.github.billywei01:fastkv:2.1.4'
}

2.3.2 初始化

    FastKVConfig.setLogger(FastKVLogger)
    FastKVConfig.setExecutor(Dispatchers.Default.asExecutor())

初始化可以按需設(shè)置日志接口和Executor芬首。

2.3.3 基本用法

    // FastKV kv = new FastKV.Builder(path, name).build();
    FastKV kv = new FastKV.Builder(context, name).build();
    
    if(!kv.getBoolean("flag")){
        kv.putBoolean("flag" , true);
    }
    
    int count = kv.getInt("count");
    if(count < 10){
        kv.putInt("count" , count + 1);
    }

Builder的構(gòu)造可傳Context或者path。
如果傳Context的話(huà)逼裆,會(huì)在內(nèi)部目錄的'files'目錄下創(chuàng)建'fastkv'目錄來(lái)作為文件的保存路徑郁稍。

2.3.4 存儲(chǔ)自定義對(duì)象

FastKV.Encoder<?>[] encoders = new FastKV.Encoder[]{LongListEncoder.INSTANCE};  
FastKV kv = new FastKV.Builder(context, name).encoder(encoders).build();  
  
List<Long> list = new ArrayList<>();  
list.add(100L);  
list.add(200L);  
list.add(300L);  
kv.putObject("long_list", list, LongListEncoder.INSTANCE);  
  
List<Long> list2 = kv.getObject("long_list");  

除了支持基本類(lèi)型外,F(xiàn)astKV還支持寫(xiě)入對(duì)象胜宇,只需在構(gòu)建FastKV實(shí)例時(shí)傳入對(duì)象的編碼器即可耀怜。
編碼器為實(shí)現(xiàn)FastEncoder接口的對(duì)象。
上面LongListEncoder就實(shí)現(xiàn)了FastEncoder接口桐愉,代碼實(shí)現(xiàn)可參考:LongListEncoder

編碼對(duì)象涉及序列化/反序列化财破。
這里推薦筆者的另外一個(gè)框架:https://github.com/BillyWei01/Packable

2.3.5 數(shù)據(jù)加密

如需對(duì)數(shù)據(jù)進(jìn)行加密,在創(chuàng)建FastKV實(shí)例時(shí)傳入
FastCipher 的實(shí)現(xiàn)即可从诲。

FastKV kv = FastKV.Builder(path, name)  
.cipher(yourCihper)  
.build()  

項(xiàng)目中有舉例Cipher的實(shí)現(xiàn)左痢,可參考:AESCipher

2.3.6 遷移 SharePreferences 到 FastKV

FastKV實(shí)現(xiàn)了SharedPreferences接口,并且提供了遷移SP數(shù)據(jù)的方法系洛。
用法如下:

public class SpCase {
   public static final String NAME = "common_store";
   // 原本的獲取SP的方法
   // public static final SharedPreferences preferences = GlobalConfig.appContext.getSharedPreferences(NAME, Context.MODE_PRIVATE);
   
   // 導(dǎo)入原SP數(shù)據(jù)
   public static final SharedPreferences preferences = FastKV.adapt(AppContext.INSTANCE.getContext(), NAME);
}

2.3.7 遷移 MMKV 到 FastKV

由于MMKV沒(méi)有實(shí)現(xiàn) 'getAll' 接口俊性,所以無(wú)法像SharePreferences一樣一次性遷移。
但是可以封裝一個(gè)KV類(lèi)碎罚,創(chuàng)建 'getInt'磅废,'getString' ... 等方法,并在其中做適配處理荆烈。
可參考:MMKV2FastKV

2.3.8 多進(jìn)程

項(xiàng)目提供了支持多進(jìn)程的實(shí)現(xiàn):MPFastKV拯勉。
MPFastKV除了支持多進(jìn)程讀寫(xiě)之外,還實(shí)現(xiàn)了SharedPreferences的接口憔购,包括支持注冊(cè)O(shè)nSharedPreferenceChangeListener ;
其中一個(gè)進(jìn)程修改了數(shù)據(jù)宫峦,所有的進(jìn)程都會(huì)感知(通過(guò)OnSharedPreferenceChangeListener回調(diào))。
可參考 MultiProcessTestActivityTestService

需要提醒的是玫鸟,由于支持多進(jìn)程需要維護(hù)更多的狀態(tài)导绷,MPFastKV 的寫(xiě)入要比FastKV慢不少,
所以在不需要多進(jìn)程訪問(wèn)的情況下屎飘,盡量用FastKV妥曲。

2.3.9 Kotlin 委托

Kotlin是兼容Java的,所以Kotlin下也可以直接用FastKV或者SharedPreferences的API钦购。
此外檐盟,Kotlin還提供了“委托屬性”這一語(yǔ)法糖,可以用于改進(jìn)key-value API訪問(wèn)押桃。
可參考:KVData

三葵萎、 性能測(cè)試

  • 測(cè)試數(shù)據(jù):搜集APP中的SharePreferences匯總的部份key-value數(shù)據(jù)(經(jīng)過(guò)隨機(jī)混淆)得到總共六百多個(gè)key-value。
    分別截取其中一部分,構(gòu)造正態(tài)分布的輸入序列羡忘,進(jìn)行多次測(cè)試谎痢。
  • 測(cè)試機(jī)型:華為P30 Pro
  • 測(cè)試代碼:Benchmark

測(cè)試結(jié)果如下:

更新:

25 50 100 200 400 600
SP-commit 114 172 411 666 2556 5344
DataStore 231 625 1717 4421 7629 13639
SQLiteKV 192 382 1025 1565 4279 5034
SP-apply 3 9 35 118 344 516
MMKV 4 8 5 8 10 9
FastKV 3 6 4 6 8 10

查詢(xún):

25 50 100 200 400 600
SP-commit 1 3 2 1 2 3
DataStore 57 76 115 117 170 216
SQLiteKV 96 161 265 417 767 1038
SP-apply 0 1 0 1 3 3
MMKV 0 1 1 5 8 11
FastKV 0 1 1 3 3 1

每次執(zhí)行Benchmark獲取到的結(jié)果有所浮動(dòng),尤其是APP啟動(dòng)后執(zhí)行多次卷雕,部分KV會(huì)變快(JIT優(yōu)化)节猿。
以上數(shù)據(jù)是取APP冷啟動(dòng)后第一次Benchmark的數(shù)據(jù)。

四爽蝴、結(jié)語(yǔ)

本文探討了當(dāng)下Android平臺(tái)的各類(lèi)KV存儲(chǔ)方式沐批,提出并實(shí)現(xiàn)了一種新的存儲(chǔ)組件,著重解決了KV存儲(chǔ)的效率和數(shù)據(jù)可靠性問(wèn)題蝎亚。
目前代碼已上傳Github: https://github.com/BillyWei01/FastKV

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末九孩,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子发框,更是在濱河造成了極大的恐慌躺彬,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,718評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件梅惯,死亡現(xiàn)場(chǎng)離奇詭異宪拥,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)铣减,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)她君,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人葫哗,你說(shuō)我怎么就攤上這事缔刹。” “怎么了劣针?”我有些...
    開(kāi)封第一講書(shū)人閱讀 158,207評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵校镐,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我捺典,道長(zhǎng)鸟廓,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,755評(píng)論 1 284
  • 正文 為了忘掉前任襟己,我火速辦了婚禮引谜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘擎浴。我一直安慰自己煌张,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,862評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布退客。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪萌狂。 梳的紋絲不亂的頭發(fā)上档玻,一...
    開(kāi)封第一講書(shū)人閱讀 50,050評(píng)論 1 291
  • 那天,我揣著相機(jī)與錄音茫藏,去河邊找鬼误趴。 笑死,一個(gè)胖子當(dāng)著我的面吹牛务傲,可吹牛的內(nèi)容都是我干的凉当。 我是一名探鬼主播,決...
    沈念sama閱讀 39,136評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼售葡,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼看杭!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起挟伙,我...
    開(kāi)封第一講書(shū)人閱讀 37,882評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤楼雹,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后尖阔,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體贮缅,經(jīng)...
    沈念sama閱讀 44,330評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,651評(píng)論 2 327
  • 正文 我和宋清朗相戀三年介却,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了谴供。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,789評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡齿坷,死狀恐怖桂肌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情胃夏,我是刑警寧澤轴或,帶...
    沈念sama閱讀 34,477評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站仰禀,受9級(jí)特大地震影響照雁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜答恶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,135評(píng)論 3 317
  • 文/蒙蒙 一饺蚊、第九天 我趴在偏房一處隱蔽的房頂上張望纸泄。 院中可真熱鬧壳嚎,春花似錦桶良、人聲如沸狞甚。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,864評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)喊巍。三九已至路媚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間苗缩,已是汗流浹背饵蒂。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,099評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留酱讶,地道東北人退盯。 一個(gè)月前我還...
    沈念sama閱讀 46,598評(píng)論 2 362
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像泻肯,于是被迫代替她去往敵國(guó)和親渊迁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評(píng)論 2 351

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