官方也無力回天媒抠?Android SharedPreferences的設(shè)計與實現(xiàn)

起源

就在前幾日饰剥,有幸拜讀到 HiDhl文章坠宴,繼騰訊開源類似功能的MMKV之后粉私,Google官方維護的 Jetpack DataStore 組件橫空出世——這是否意味著無論是騰訊三方還是Google官方的角度,SharedPreferences都徹底告別了這個時代壁顶?

無論是MMKV的支持者還是DataStore的擁躉珠洗,SharedPreferences似乎都不值一提;值得深思的是若专,筆者通過面試或者其它方式许蓖,和一些同行交流時,卻遇到了以下的情形:

在談及SharedPreferencesMMKV调衰,大多數(shù)人都能對前者的 缺陷膊爪,以及后者性能上若干 數(shù)量級的優(yōu)勢 娓娓道來;但是嚎莉,在針對前者的短板進行細節(jié)化的討論時米酬,往往卻得不到更深入性的結(jié)果,簡單列舉幾個問題如下:

  • SharedPreferences是如何保證線程安全的趋箩,其內(nèi)部的實現(xiàn)用到了哪些鎖赃额?
  • 進程不安全是否會導(dǎo)致數(shù)據(jù)丟失?
  • 數(shù)據(jù)丟失時叫确,其最終的屏障——文件備份機制是如何實現(xiàn)的跳芳?
  • 如何實現(xiàn)進程安全的SharedPreferences

除此之外启妹,站在 設(shè)計者的角度 上筛严,還有一些與架構(gòu)相關(guān),且同樣值得思考的問題:

  • 為什么SharedPreferences會有這些缺陷饶米,如何對這些缺陷做改進的嘗試桨啃?
  • 為什么不惜推倒重來车胡,推出新的DataStore組件來代替前者?
  • Google工程師掣肘照瘾,時隔今日匈棘,這些缺陷依然存在的最根本性原因是什么?

而想要解除這些潛藏在內(nèi)心最深處的困惑析命,就不得不從SharedPreferences本身的設(shè)計與實現(xiàn)講起了主卫。

本文大綱如下:

一、SharedPreferences的前世今生

我們知道鹃愤,就在不久前2019年的Google I/O大會上簇搅,官方推出了Jetpack Security組件,旨在保證文件和SharedPreferences的安全性软吐,SharedPreferences的包裝類瘩将,EncryptedSharedPreferences隆重登場。

不僅如此凹耙,Android 8.0前后的源碼中姿现,SharedPreferences內(nèi)部的實現(xiàn)也略有不同。由此可見肖抱,Android官方一直在盡力“拯救”SharedPreferences备典。

因此,在毅然決然拋棄SharedPreferences投奔新的解決方案之前意述,我們有必要重新認識一下它提佣。

1、設(shè)計與實現(xiàn):建立基本結(jié)構(gòu)

SharedPreferencesAndroid平臺上 輕量級的存儲類欲险,用來保存App的各種配置信息镐依,其本質(zhì)是一個以 鍵值對key-value)的方式保存數(shù)據(jù)的xml文件匹涮,其保存在/data/data/shared_prefs目錄下天试。

對于21世紀初,那個Android系統(tǒng)誕生的時代而言然低,使用xml文件保存應(yīng)用輕量級的數(shù)據(jù)絕對是一個不錯的主意喜每。那個時代的json才剛剛出生不久,雖然也漸漸成為了主流的 輕量級數(shù)據(jù)交換格式 雳攘,但是其更多的優(yōu)勢還是在于 可讀性带兜,這也是筆者猜測沒有使用json而使用xml保存的原因之一。

現(xiàn)在我們?yōu)檫@個 輕量級的存儲類 建立了最基礎(chǔ)的模型吨灭,通過xml中的鍵值對刚照,將對應(yīng)的數(shù)據(jù)保存到本地的文件中。這樣喧兄,每次讀取數(shù)據(jù)時无畔,通過解析xml文件啊楚,得到指定key對應(yīng)的value;每次更新數(shù)據(jù)浑彰,也通過文件中key更新對應(yīng)的value恭理。

2、讀操作的優(yōu)化

通過這樣的方式郭变,雖然我們建立了一個最簡單的 文件存儲系統(tǒng)颜价,但是性能實在不敢恭維,每次讀取一個key對應(yīng)的值都要重新對文件進行一次讀的操作诉濒?顯然需要盡量避免笨重的I/O操作周伦。

因此設(shè)計者針對讀操作進行了簡單的優(yōu)化,當SharedPreferences對象第一次通過Context.getSharedPreferences()進行初始化時未荒,對xml文件進行一次讀取横辆,并將文件內(nèi)所有內(nèi)容(即所有的鍵值對)緩到內(nèi)存的一個Map中,這樣茄猫,接下來所有的讀操作狈蚤,只需要從這個Map中取就可以了:

final class SharedPreferencesImpl implements SharedPreferences {
  private final File mFile;             // 對應(yīng)的xml文件
  private Map<String, Object> mMap;     // Map中緩存了xml文件中所有的鍵值對
}
復(fù)制代碼

讀者不禁會有疑問,雖然節(jié)省了I/O的操作划纽,但另一個視角分析脆侮,當xml中數(shù)據(jù)量過大時,這種 內(nèi)存緩存機制 是否會產(chǎn)生 高內(nèi)存占用 的風險勇劣?

這也正是很多開發(fā)者詬病SharedPreferences的原因之一靖避,那么,從事物的兩面性上來看比默,高內(nèi)存占用 真的是設(shè)計者的問題嗎幻捏?

不盡然,因為SharedPreferences的設(shè)計初衷是數(shù)據(jù)的 輕量級存儲 命咐,對于類似應(yīng)用的簡單的配置項(比如一個boolean或者int類型)篡九,即使很多也并不會對內(nèi)存有過高的占用;而對于復(fù)雜的數(shù)據(jù)(比如復(fù)雜對象序列化后的字符串)醋奠,開發(fā)者更應(yīng)該使用類似Room這樣的解決方案榛臼,而非一股腦存儲到SharedPreferences中。

因此窜司,相對于「SharedPreferences會導(dǎo)致內(nèi)存使用過高」的說法沛善,筆者更傾向于更客觀的進行總結(jié):

雖然 內(nèi)存緩存機制 表面上看起來好像是一種 空間換時間 的權(quán)衡,實際上規(guī)避了短時間內(nèi)頻繁的I/O操作對性能產(chǎn)生的影響塞祈,而通過良好的代碼規(guī)范金刁,也能夠避免該機制可能會導(dǎo)致內(nèi)存占用過高的副作用,所以這種設(shè)計是 值得肯定 的。

3尤蛮、寫操作的優(yōu)化

針對寫操作漠秋,設(shè)計者同樣設(shè)計了一系列的接口,以達到優(yōu)化性能的目的抵屿。

我們知道對鍵值對進行更新是通過mSharedPreferences.edit().putString().commit()進行操作的——edit()是什么庆锦,commit()又是什么,為什么不單純的設(shè)計初mSharedPreferences.putString()這樣的接口轧葛?

設(shè)計者希望搂抒,在復(fù)雜的業(yè)務(wù)中,有時候一次操作會導(dǎo)致多個鍵值對的更新尿扯,這時求晶,與其多次更新文件,我們更傾向?qū)⑦@些更新 合并到一次寫操作 中衷笋,以達到性能的優(yōu)化芳杏。

因此,對于SharedPreferences的寫操作辟宗,設(shè)計者抽象出了一個Editor類爵赵,不管某次操作通過若干次調(diào)用putXXX()方法,更新了幾個xml中的鍵值對泊脐,只有調(diào)用了commit()方法空幻,最終才會真正寫入文件:

// 簡單的業(yè)務(wù),一次更新一個鍵值對
sharedPreferences.edit().putString().commit();

// 復(fù)雜的業(yè)務(wù)容客,一次更新多個鍵值對秕铛,仍然只進行一次IO操作(文件的寫入)
Editor editor = sharedPreferences.edit();
editor.putString();
editor.putBoolean().putInt();
editor.commit();   // commit()才會更新文件
復(fù)制代碼

了解到這一點,讀者應(yīng)該明白缩挑,通過簡單粗暴的封裝但两,以達到類似SPUtils.putXXX()這種所謂代碼量的節(jié)省,從而忽略了Editor.commit()的設(shè)計理念和使用場景供置,往往是不可取的谨湘,從設(shè)計上來講,這甚至是一種 倒退 士袄。

另外一個值得思考的角度是悲关,本質(zhì)上文件的I/O是一個非常重的操作,直接放在主線程中的commit()方法某些場景下會導(dǎo)致ANR(比如數(shù)據(jù)量過大)娄柳,因此更合理的方式是應(yīng)該將其放入子線程執(zhí)行。

因此設(shè)計者還為Editor提供了一個apply()方法艘绍,用于異步執(zhí)行文件數(shù)據(jù)的同步赤拒,并推薦開發(fā)者使用apply()而非commit()

看起來Editor+apply()方法對寫操作做了很大的優(yōu)化,但更多的問題隨之而來挎挖,比如子線程更新文件这敬,必然會引發(fā) 線程安全問題;此外蕉朵,apply()方法真的能夠像我們預(yù)期的一樣崔涂,能夠避免ANR嗎?答案是并不能始衅,這個我們后文再提冷蚂。

4、數(shù)據(jù)的更新 & 文件數(shù)量的權(quán)衡

隨著業(yè)務(wù)復(fù)雜度的上升汛闸,需要面對新的問題是蝙茶,xml文件中的數(shù)據(jù)量愈發(fā)龐大,一次文件的寫操作成本也愈發(fā)高昂诸老。

xml中數(shù)據(jù)是如何更新的隆夯?讀者可以簡單理解為 全量更新 ——通過上文,我們知道xml文件中的數(shù)據(jù)會緩存到內(nèi)存的mMap中别伏,每次在調(diào)用editor.putXXX()時蹄衷,實際上會將新的數(shù)據(jù)存入在mMap,當調(diào)用commit()apply()時厘肮,最終會將mMap的所有數(shù)據(jù)全量更新到xml文件里宦芦。

由此可見,xml中數(shù)據(jù)量的大小轴脐,的確會對 寫操作 的成本有一定的影響调卑,因此,設(shè)計者更建議將 不同業(yè)務(wù)模塊的數(shù)據(jù)分文件存儲 大咱,即根據(jù)業(yè)務(wù)將數(shù)據(jù)存放在不同的xml文件中恬涧。

因此,不同的xml文件應(yīng)該對應(yīng)不同的SharedPreferences對象碴巾,如果想要對某個xml文件進行操作怕篷,就通過傳不同的文件標識符,獲取對應(yīng)的SharedPreferences

@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
  // name參數(shù)就是文件名疏遏,通過不同文件名溉旋,獲取指定的SharedPreferences對象
}
復(fù)制代碼

因此,當xml文件過大時煮仇,應(yīng)該考慮根據(jù)業(yè)務(wù)劳跃,細分為若干個小的文件進行管理;但過多的小文件也會導(dǎo)致過多的SharedPreferences對象浙垫,不好管理且易混淆刨仑。實際開發(fā)中郑诺,開發(fā)者應(yīng)根據(jù)業(yè)務(wù)的需要進行對應(yīng)的平衡。

二杉武、線程安全問題

SharedPreferences是線程安全的嗎辙诞?

毫無疑問,SharedPreferences是線程安全的轻抱,但這只是對成品而言飞涂,對于我們目前的實現(xiàn),顯然還有一定的差距祈搜,如何保證線程安全呢较店?

——那,為了保證線程安全夭问,怎么著不得加個鎖吧泽西。

加個鎖?那是起步缰趋!3把鎖捧杉,你還別嫌多。你得研究開發(fā)寫代碼時的心理秘血,舍得往代碼里吭哧吭哧加鎖的開發(fā)味抖,壓根不在乎再加2把。

1灰粮、保證復(fù)雜流程代碼的可讀性

為了保證SharedPreferences是線程安全的仔涩,Google的設(shè)計者一共使用了3把鎖:

final class SharedPreferencesImpl implements SharedPreferences {
  // 1、使用注釋標記鎖的順序
  // Lock ordering rules:
  //  - acquire SharedPreferencesImpl.mLock before EditorImpl.mLock
  //  - acquire mWritingToDiskLock before EditorImpl.mLock

  // 2粘舟、通過注解標記持有的是哪把鎖
  @GuardedBy("mLock")
  private Map<String, Object> mMap;

  @GuardedBy("mWritingToDiskLock")
  private long mDiskStateGeneration;

  public final class EditorImpl implements Editor {
    @GuardedBy("mEditorLock")
    private final Map<String, Object> mModified = new HashMap<>();
  }
}
復(fù)制代碼

對于這樣復(fù)雜的類而言熔脂,如何提高代碼的可讀性?SharedPreferencesImpl做了一個很好的示范:通過注釋明確寫明加鎖的順序柑肴,并為被加鎖的成員使用@GuardedBy注解霞揉。

對于簡單的 讀操作 而言,我們知道其原理是讀取內(nèi)存中mMap的值并返回晰骑,那么為了保證線程安全适秩,只需要加一把鎖保證mMap的線程安全即可:

public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}
復(fù)制代碼

那么,對于 寫操作 而言硕舆,我們也能夠通過一把鎖達到線程安全的目的嗎秽荞?

2、保證寫操作的線程安全

對于寫操作而言抚官,每次putXXX()并不能立即更新在mMap中扬跋,這是理所當然的,如果開發(fā)者沒有調(diào)用apply()方法耗式,那么這些數(shù)據(jù)的更新理所當然應(yīng)該被拋棄掉胁住,但是如果直接更新在mMap中趁猴,那么數(shù)據(jù)就難以恢復(fù)刊咳。

因此彪见,Editor本身也應(yīng)該持有一個mEditorMap對象,用于存儲數(shù)據(jù)的更新娱挨;只有當調(diào)用apply()時余指,才嘗試將mEditorMapmMap進行合并,以達到數(shù)據(jù)更新的目的跷坝。

因此酵镜,這里我們還需要另外一把鎖保證mEditorMap的線程安全,筆者認為柴钻,不和mMap公用同一把鎖的原因是淮韭,在apply()被調(diào)用之前,getXXXputXXX理應(yīng)是沒有沖突的贴届。

代碼實現(xiàn)參考如下:

public final class EditorImpl implements Editor {
  @Override
  public Editor putString(String key, String value) {
      synchronized (mEditorLock) {
          mEditorMap.put(key, value);
          return this;
      }
  }
}
復(fù)制代碼

而當真正需要執(zhí)行apply()進行寫操作時靠粪,mEditorMapmMap進行合并,這時必須通過2把鎖保證mEditorMapmMap的線程安全毫蚓,保證mMap最終能夠更新成功占键,最終向?qū)?yīng)的xml文件中進行更新。

文件的更新理所當然也需要加一把鎖:

// SharedPreferencesImpl.EditorImpl.enqueueDiskWrite()
synchronized (mWritingToDiskLock) {
    writeToFile(mcr, isFromSyncCommit);
}
復(fù)制代碼

最終元潘,我們一共通過使用了3把鎖畔乙,對整個寫操作的線程安全進行了保證。

篇幅限制翩概,本文不對源碼進行詳細引申牲距,有興趣的讀者可參考 SharedPreferencesImpl.EditorImpl 類的apply()源碼。

3钥庇、擺脫不掉的ANR

apply()方法設(shè)計的初衷是為了規(guī)避主線程的I/O操作導(dǎo)致ANR問題的產(chǎn)生牍鞠,那么,ANR的問題真得到了有效的解決嗎上沐?

并沒有皮服,在 字節(jié)跳動技術(shù)團隊這篇文章 中,明確說明了線上環(huán)境中参咙,相當一部分的ANR統(tǒng)計都來自于SharedPreference龄广,由此可見,apply()并沒有完全規(guī)避掉這個問題蕴侧,那么導(dǎo)致ANR的原因又是什么呢择同。

經(jīng)過我們的優(yōu)化,SharedPreferences的確是線程安全的净宵,apply()的內(nèi)部實現(xiàn)也的確將I/O操作交給了子線程敲才,可以說其本身是沒有問題的裹纳,而其原因歸根到底則是Android的另外一個機制。

apply()方法中紧武,首先會創(chuàng)建一個等待鎖剃氧,根據(jù)源碼版本的不同,最終更新文件的任務(wù)會交給QueuedWork.singleThreadExecutor()單個線程或者HandlerThread去執(zhí)行阻星,當文件更新完畢后會釋放鎖朋鞍。

但當Activity.onStop()以及Service處理onStop等相關(guān)方法時,則會執(zhí)行 QueuedWork.waitToFinish()等待所有的等待鎖釋放妥箕,因此如果SharedPreferences一直沒有完成更新任務(wù)滥酥,有可能會導(dǎo)致卡在主線程,最終超時導(dǎo)致ANR畦幢。

什么情況下SharedPreferences會一直沒有完成任務(wù)呢坎吻? 比如太頻繁無節(jié)制的apply(),導(dǎo)致任務(wù)過多宇葱,這也側(cè)面說明了SPUtils.putXXX()這種粗暴的設(shè)計的弊端瘦真。

Google為何這么設(shè)計呢?字節(jié)跳動技術(shù)團隊的這篇文章中做出了如下猜測:

無論是 commit 還是 apply 都會產(chǎn)生 ANR贝搁,但從 Android 之初到目前 Android8.0吗氏,Google 一直沒有修復(fù)此 bug,我們貿(mào)然處理會產(chǎn)生什么問題呢雷逆。Google 在 Activity 和 Service 調(diào)用 onStop 之前阻塞主線程來處理 SP弦讽,我們能猜到的唯一原因是盡可能的保證數(shù)據(jù)的持久化。因為如果在運行過程中產(chǎn)生了 crash膀哲,也會導(dǎo)致 SP 未持久化往产,持久化本身是 IO 操作,也會失敗某宪。

如此看來仿村,導(dǎo)致這種缺陷的原因,其設(shè)計也的確是有自身的考量的兴喂,好在 這篇文章 末尾也提出了一個折衷的解決方案蔼囊,有興趣的讀者可以了解一下,本文不贅述衣迷。

三畏鼓、進程安全問題

1、如何保證進程安全

SharedPreferences是否進程安全呢壶谒?讓我們打開SharedPreferences的源碼云矫,看一下最頂部類的注釋:

/**
 * ...
 * This class does not support use across multiple processes.
 * ...
 */
public interface SharedPreferences {
  // ...
}
復(fù)制代碼

由此,由于沒有使用跨進程的鎖汗菜,SharedPreferences是進程不安全的让禀,在跨進程頻繁讀寫會有數(shù)據(jù)丟失的可能挑社,這顯然不符合我們的期望。

那么巡揍,如何保證SharedPreferences進程的安全呢?

實現(xiàn)思路很多痛阻,比如使用文件鎖,保證每次只有一個進程在訪問這個文件吼肥;或者對于Android開發(fā)而言录平,ContentProvider作為官方倡導(dǎo)的跨進程組件麻车,其它進程通過定制的ContentProvider用于訪問SharedPreferences缀皱,同樣可以保證SharedPreferences的進程安全;等等动猬。

篇幅原因啤斗,對實現(xiàn)有興趣的讀者,可以參考 百度 或文章末尾的 參考資料赁咙。

2钮莲、文件損壞 & 備份機制

SharedPreferences再次迎來了新的挑戰(zhàn)。

由于不可預(yù)知的原因(比如內(nèi)核崩潰或者系統(tǒng)突然斷電)彼水,xml文件的 寫操作 異常中止崔拥,Android系統(tǒng)本身的文件系統(tǒng)雖然有很多保護措施,但依然會有數(shù)據(jù)丟失或者文件損壞的情況凤覆。

作為設(shè)計者链瓦,如何規(guī)避這樣的問題呢?答案是對文件進行備份盯桦,SharedPreferences的寫入操作正式執(zhí)行之前慈俯,首先會對文件進行備份,將初始文件重命名為增加了一個.bak后綴的備份文件:

// 嘗試寫入文件
private void writeToFile(...) {
  if (!backupFileExists) {
      !mFile.renameTo(mBackupFile);
  }
}
復(fù)制代碼

這之后拥峦,嘗試對文件進行寫入操作贴膘,寫入成功時,則將備份文件刪除:

// 寫入成功略号,立即刪除存在的備份文件
// Writing was successful, delete the backup file if there is one.
mBackupFile.delete();
復(fù)制代碼

反之刑峡,若因異常情況(比如進程被殺)導(dǎo)致寫入失敗,進程再次啟動后玄柠,若發(fā)現(xiàn)存在備份文件突梦,則將備份文件重名為源文件,原本未完成寫入的文件就直接丟棄:

// 從磁盤初始化加載時執(zhí)行
private void loadFromDisk() {
    synchronized (mLock) {
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }
  }
復(fù)制代碼

現(xiàn)在随闪,通過文件備份機制阳似,我們能夠保證數(shù)據(jù)只會丟失最后的更新,而之前成功保存的數(shù)據(jù)依然能夠有效铐伴。

四撮奏、小結(jié)

綜合來看俏讹,SharedPreferences那些一直被關(guān)注的問題,從設(shè)計的角度來看畜吊,都是有其自身考量的泽疆。

我們可以看到,雖然SharedPreferences其整體是比較完善的玲献,但是為什么相比較MMKVJetpack DataStore殉疼,其性能依然有明顯的落差呢?

這個原因更加綜合且復(fù)雜捌年,即使筆者也還是處于淺顯的了解層面瓢娜,比如后兩者在其數(shù)據(jù)序列化方面都選用了更先進的protobuf協(xié)議,MMKV自身的數(shù)據(jù)的 增量更新 機制等等礼预,有機會的話會另起新的一篇進行分享眠砾。

反過頭來,相對于對組件之間單純進行 不好 的定義托酸,筆者更認為通過辯證的方式去看待和學(xué)習它們褒颈,相信即使是SharedPreferences,學(xué)習下來依然能夠有所收獲励堡。

作者:卻把清梅嗅
鏈接:https://juejin.im/post/6884505736836022280

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末谷丸,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子应结,更是在濱河造成了極大的恐慌刨疼,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,589評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件摊趾,死亡現(xiàn)場離奇詭異币狠,居然都是意外死亡,警方通過查閱死者的電腦和手機砾层,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評論 3 396
  • 文/潘曉璐 我一進店門漩绵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人肛炮,你說我怎么就攤上這事止吐。” “怎么了侨糟?”我有些...
    開封第一講書人閱讀 165,933評論 0 356
  • 文/不壞的土叔 我叫張陵碍扔,是天一觀的道長。 經(jīng)常有香客問我秕重,道長不同,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,976評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮二拐,結(jié)果婚禮上服鹅,老公的妹妹穿的比我還像新娘。我一直安慰自己百新,他們只是感情好企软,可當我...
    茶點故事閱讀 67,999評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著饭望,像睡著了一般仗哨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上铅辞,一...
    開封第一講書人閱讀 51,775評論 1 307
  • 那天厌漂,我揣著相機與錄音,去河邊找鬼巷挥。 笑死桩卵,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的倍宾。 我是一名探鬼主播,決...
    沈念sama閱讀 40,474評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼胜嗓,長吁一口氣:“原來是場噩夢啊……” “哼高职!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起辞州,我...
    開封第一講書人閱讀 39,359評論 0 276
  • 序言:老撾萬榮一對情侶失蹤怔锌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后变过,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體埃元,經(jīng)...
    沈念sama閱讀 45,854評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,007評論 3 338
  • 正文 我和宋清朗相戀三年媚狰,在試婚紗的時候發(fā)現(xiàn)自己被綠了岛杀。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,146評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡崭孤,死狀恐怖类嗤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情辨宠,我是刑警寧澤遗锣,帶...
    沈念sama閱讀 35,826評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站嗤形,受9級特大地震影響精偿,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,484評論 3 331
  • 文/蒙蒙 一笔咽、第九天 我趴在偏房一處隱蔽的房頂上張望墓阀。 院中可真熱鬧,春花似錦拓轻、人聲如沸斯撮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽勿锅。三九已至,卻和暖如春枣氧,著一層夾襖步出監(jiān)牢的瞬間溢十,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評論 1 272
  • 我被黑心中介騙來泰國打工达吞, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留张弛,地道東北人。 一個月前我還...
    沈念sama閱讀 48,420評論 3 373
  • 正文 我出身青樓酪劫,卻偏偏與公主長得像吞鸭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子覆糟,可洞房花燭夜當晚...
    茶點故事閱讀 45,107評論 2 356