從官方方式進行拓展的App啟動時間監(jiān)控

這是一篇 WWDC 2020 Session 406 的學(xué)習(xí)筆記阻肩,從原理到實踐講述了如何優(yōu)化 App 的啟動時間。

App 運行理論

  • main() 執(zhí)行前發(fā)生的事
  • Mach-O 格式
  • 虛擬內(nèi)存基礎(chǔ)
  • Mach-O 二進制的加載

理論速成

Mach-O 術(shù)語

Mach-O 是針對不同運行時可執(zhí)行文件的文件類型。

文件類型:

  • main() 執(zhí)行前發(fā)生的事
  • Mach-O 格式
  • 虛擬內(nèi)存基礎(chǔ)
  • Mach-O 二進制的加載

理論速成

Mach-O 術(shù)語

Mach-O 是針對不同運行時可執(zhí)行文件的文件類型。

文件類型:

  • Executable: 應(yīng)用的主要二進制
  • Dylib: 動態(tài)鏈接庫(又稱 DSO 或 DLL)
  • Bundle: 不能被鏈接的 Dylib诱贿,只能在運行時使用 dlopen() 加載芍殖,可當(dāng)做 macOS 的插件。

Image: executable外厂,dylib 或 bundle
Framework: 包含 Dylib 以及資源文件和頭文件的文件夾

Mach-O 鏡像文件

Mach-O 被劃分成一些 segement冕象,每個 segement 又被劃分成一些 section。

segment 的名字都是大寫的汁蝶,且空間大小為頁的整數(shù)渐扮。頁的大小跟硬件有關(guān),在 arm64 架構(gòu)一頁是 16KB掖棉,其余為 4KB墓律。

section 雖然沒有整數(shù)倍頁大小的限制,但是 section 之間不會有重疊幔亥。

幾乎所有 Mach-O 都包含這三個段(segment): __TEXT,__DATA__LINKEDIT

  • __TEXT 包含 Mach header耻讽,被執(zhí)行的代碼和只讀常量(如C 字符串)。只讀可執(zhí)行(r-x)紫谷。
  • __DATA 包含全局變量齐饮,靜態(tài)變量等捐寥。可讀寫(rw-)祖驱。
  • __LINKEDIT 包含了加載程序的『元數(shù)據(jù)』握恳,比如函數(shù)的名稱和地址。只讀(r–)捺僻。

Mach-O Universal 文件

FAT 二進制文件乡洼,將多種架構(gòu)的 Mach-O 文件合并而成。它通過 Fat Header 來記錄不同架構(gòu)在文件中的偏移量匕坯,F(xiàn)at Header 占一頁的空間束昵。

按分頁來存儲這些 segement 和 header 會浪費空間,但這有利于虛擬內(nèi)存的實現(xiàn)葛峻。

虛擬內(nèi)存

虛擬內(nèi)存就是一層間接尋址(indirection)锹雏。軟件工程中有句格言就是任何問題都能通過添加一個間接層來解決。虛擬內(nèi)存解決的是管理所有進程使用物理 RAM 的問題术奖。通過添加間接層來讓每個進程使用邏輯地址空間礁遵,它可以映射到 RAM 上的某個物理頁上。這種映射不是一對一的采记,邏輯地址可能映射不到 RAM 上佣耐,也可能有多個邏輯地址映射到同一個物理 RAM 上。針對第一種情況唧龄,當(dāng)進程要存儲邏輯地址內(nèi)容時會觸發(fā) page fault兼砖;第二種情況就是多進程共享內(nèi)存。

對于文件可以不用一次性讀入整個文件既棺,可以使用分頁映射(mmap())的方式讀取讽挟。也就是把文件某個片段映射到進程邏輯內(nèi)存的某個頁上。當(dāng)某個想要讀取的頁沒有在內(nèi)存中援制,就會觸發(fā) page fault戏挡,內(nèi)核只會讀入那一頁,實現(xiàn)文件的懶加載晨仑。

也就是說 Mach-O 文件中的 __TEXT 段可以映射到多個進程,并可以懶加載拆檬,且進程之間共享內(nèi)存洪己。__DATA 段是可讀寫的。這里使用到了 Copy-On-Write 技術(shù)竟贯,簡稱 COW答捕。也就是多個進程共享一頁內(nèi)存空間時,一旦有進程要做寫操作屑那,它會先將這頁內(nèi)存內(nèi)容復(fù)制一份出來拱镐,然后重新映射邏輯地址到新的 RAM 頁上艘款。也就是這個進程自己擁有了那頁內(nèi)存的拷貝。這就涉及到了 clean/dirty page 的概念沃琅。dirty page 含有進程自己的信息哗咆,而 clean page 可以被內(nèi)核重新生成(重新讀磁盤)。所以 dirty page 的代價大于 clean page益眉。

Mach-O 鏡像 加載

所以在多個進程加載 Mach-O 鏡像時 __TEXT__LINKEDIT 因為只讀晌柬,都是可以共享內(nèi)存的。而 __DATA 因為可讀寫郭脂,就會產(chǎn)生 dirty page年碘。當(dāng) dyld 執(zhí)行結(jié)束后,__LINKEDIT 就沒用了展鸡,對應(yīng)的內(nèi)存頁會被回收屿衅。

安全

ASLR(Address Space Layout Randomization):地址空間布局隨機化,鏡像會在隨機的地址上加載莹弊。這其實是一二十年前的舊技術(shù)了涤久。

代碼簽名:可能我們認(rèn)為 Xcode 會把整個文件都做加密 hash 并用做數(shù)字簽名。其實為了在運行時驗證 Mach-O 文件的簽名箱硕,并不是每次重復(fù)讀入整個文件拴竹,而是把每頁內(nèi)容都生成一個單獨的加密散列值,并存儲在 __LINKEDIT 中剧罩。這使得文件每頁的內(nèi)容都能及時被校驗確并保不被篡改栓拜。

exec()main()

exec() 是一個系統(tǒng)調(diào)用。系統(tǒng)內(nèi)核把應(yīng)用映射到新的地址空間惠昔,且每次起始位置都是隨機的(因為使用 ASLR)幕与。并將起始位置到 0x000000 這段范圍的進程權(quán)限都標(biāo)記為不可讀寫不可執(zhí)行。如果是 32 位進程镇防,這個范圍至少是 4KB啦鸣;對于 64 位進程則至少是 4GB。NULL 指針引用和指針截斷誤差都是會被它捕獲来氧。

dyld 加載 dylib 文件

Unix 的前二十年很安逸诫给,因為那時還沒有發(fā)明動態(tài)鏈接庫。有了動態(tài)鏈接庫后啦扬,一個用于加載鏈接庫的幫助程序被創(chuàng)建中狂。在蘋果的平臺里是 dyld,其他 Unix 系統(tǒng)也有 ld.so扑毡。 當(dāng)內(nèi)核完成映射進程的工作后會將名字為 dyld 的Mach-O 文件映射到進程中的隨機地址胃榕,它將 PC 寄存器設(shè)為 dyld 的地址并運行。dyld 在應(yīng)用進程中運行的工作是加載應(yīng)用依賴的所有動態(tài)鏈接庫瞄摊,準(zhǔn)備好運行所需的一切勋又,它擁有的權(quán)限跟應(yīng)用一樣苦掘。

下面的步驟構(gòu)成了 dyld 的時間線:

Load dylibs -> Rebase -> Bind -> ObjC -> Initializers

加載 Dylib

從主執(zhí)行文件的 header 獲取到需要加載的所依賴動態(tài)庫列表,而 header 早就被內(nèi)核映射過楔壤。然后它需要找到每個 dylib鹤啡,然后打開文件讀取文件起始位置,確保它是 Mach-O 文件挺邀。接著會找到代碼簽名并將其注冊到內(nèi)核揉忘。然后在 dylib 文件的每個 segment 上調(diào)用 mmap()。應(yīng)用所依賴的 dylib 文件可能會再依賴其他 dylib端铛,所以 dyld 所需要加載的是動態(tài)庫列表一個遞歸依賴的集合泣矛。一般應(yīng)用會加載 100 到 400 個 dylib 文件,但大部分都是系統(tǒng) dylib禾蚕,它們會被預(yù)先計算和緩存起來您朽,加載速度很快。

Fix-ups

在加載所有的動態(tài)鏈接庫之后换淆,它們只是處在相互獨立的狀態(tài)哗总,需要將它們綁定起來,這就是 Fix-ups倍试。代碼簽名使得我們不能修改指令讯屈,那樣就不能讓一個 dylib 的調(diào)用另一個 dylib。這時需要加很多間接層县习。

現(xiàn)代 code-gen 被叫做動態(tài) PIC(Position Independent Code)涮母,意味著代碼可以被加載到間接的地址上。當(dāng)調(diào)用發(fā)生時躁愿,code-gen 實際上會在 __DATA 段中創(chuàng)建一個指向被調(diào)用者的指針叛本,然后加載指針并跳轉(zhuǎn)過去。

所以 dyld 做的事情就是修正(fix-up)指針和數(shù)據(jù)彤钟。Fix-up 有兩種類型来候,rebasing 和 binding。

Rebasing 和 Binding

Rebasing:在鏡像內(nèi)部調(diào)整指針的指向
Binding:將指針指向鏡像外部的內(nèi)容

可以通過命令行查看 rebase 和 bind 等信息:


xcrun dyldinfo -rebase -bind -lazy_bind zcs.app/zcs

通過這個命令可以查看所有的 Fix-up逸雹。rebase营搅,bind,weak_bind梆砸,lazy_bind 都存儲在 __LINKEDIT 段中剧防,并可通過 LC_DYLD_INFO_ONLY 查看各種信息的偏移量和大小。

建議用 MachOView 查看更加方便直觀辫樱。

dyld 源碼層面簡要介紹下 Rebasing 和 Binding 的流程。

ImageLoader 是一個用于加載可執(zhí)行文件的基類俊庇,它負(fù)責(zé)鏈接鏡像狮暑,但不關(guān)心具體文件格式鸡挠,因為這些都交給子類去實現(xiàn)。每個可執(zhí)行文件都會對應(yīng)一個 ImageLoader 實例搬男。ImageLoaderMachO 是用于加載 Mach-O 格式文件的 ImageLoader 子類拣展,而 ImageLoaderMachOClassicImageLoaderMachOCompressed 都繼承于 ImageLoaderMachO,分別用于加載那些 __LINKEDIT 段為傳統(tǒng)格式和壓縮格式的 Mach-O 文件缔逛。

因為 dylib 之間有依賴關(guān)系备埃,所以 ImageLoader 中的好多操作都是沿著依賴鏈遞歸操作的,Rebasing 和 Binding 也不例外褐奴,分別對應(yīng)著 recursiveRebase()recursiveBind() 這兩個方法按脚。因為是遞歸,所以會自底向上地分別調(diào)用 doRebase()doBind() 方法敦冬,這樣被依賴的 dylib 總是先于依賴它的 dylib 執(zhí)行 Rebasing 和 Binding辅搬。傳入 doRebase()doBind() 的參數(shù)包含一個 LinkContext 上下文,存儲了可執(zhí)行文件的一堆狀態(tài)和相關(guān)的函數(shù)脖旱。

在 Rebasing 和 Binding 前會判斷是否已經(jīng) Prebinding堪遂。如果已經(jīng)進行過預(yù)綁定(Prebinding),那就不需要 Rebasing 和 Binding 這些 Fix-up 流程了萌庆,因為已經(jīng)在預(yù)先綁定的地址加載好了溶褪。

ImageLoaderMachO 實例不使用預(yù)綁定會有四個原因:

  1. Mach-O Header 中 MH_PREBOUND 標(biāo)志位為 0
  2. 鏡像加載地址有偏移(這個后面會講到)
  3. 依賴的庫有變化
  4. 鏡像使用 flat-namespace,預(yù)綁定的一部分會被忽略
  5. LinkContext 的環(huán)境變量禁止了預(yù)綁定

ImageLoaderMachOdoRebase() 做的事情大致如下:

  1. 如果使用預(yù)綁定践险,fgImagesWithUsedPrebinding 計數(shù)加一猿妈,并 return;否則進入第二步
  2. 如果 MH_PREBOUND 標(biāo)志位為 1(也就是可以預(yù)綁定但沒使用),且鏡像在共享內(nèi)存中捏境,重置上下文中所有的 lazy pointer于游。(如果鏡像在共享內(nèi)存中,稍后會在 Binding 過程中綁定垫言,所以無需重置)
  3. 如果鏡像加載地址偏移量為0贰剥,則無需 Rebasing,直接 return筷频;否則進入第四步
  4. 調(diào)用 rebase() 方法蚌成,這才是真正做 Rebasing 工作的方法。如果開啟 TEXT_RELOC_SUPPORT 宏凛捏,會允許 rebase() 方法對 __TEXT 段做寫操作來對其進行 Fix-up担忧。所以其實 __TEXT 只讀屬性并不是絕對的。

ImageLoaderMachOClassicImageLoaderMachOCompressed 分別實現(xiàn)了自己的 doRebase() 方法坯癣。實現(xiàn)邏輯大同小異瓶盛,同樣會判斷是否使用預(yù)綁定,并在真正的 Binding 工作時判斷 TEXT_RELOC_SUPPORT 宏來決定是否對 __TEXT 段做寫操作。最后都會調(diào)用 setupLazyPointerHandler 在鏡像中設(shè)置 dyld 的 entry point惩猫,放在最后調(diào)用是為了讓主可執(zhí)行文件設(shè)置好 __dyld__program_vars芝硬。

Rebasing

在過去,會把 dylib 加載到指定地址轧房,所有指針和數(shù)據(jù)對于代碼來說都是對的拌阴,dyld 就無需做任何 fix-up 了。如今用了 ASLR 后會將 dylib 加載到新的隨機地址(actual_address)奶镶,這個隨機的地址跟代碼和數(shù)據(jù)指向的舊地址(preferred_address)會有偏差迟赃,dyld 需要修正這個偏差(slide),做法就是將 dylib 內(nèi)部的指針地址都加上這個偏移量厂镇,偏移量的計算方法如下:

Slide = actual_address - preferred_address

然后就是重復(fù)不斷地對 __DATA 段中需要 rebase 的指針加上這個偏移量纤壁。這就又涉及到 page fault 和 COW。這可能會產(chǎn)生 I/O 瓶頸剪撬,但因為 rebase 的順序是按地址排列的摄乒,所以從內(nèi)核的角度來看這是個有次序的任務(wù),它會預(yù)先讀入數(shù)據(jù)残黑,減少 I/O 消耗馍佑。

Binding

Binding 是處理那些指向 dylib 外部的指針,它們實際上被符號(symbol)名稱綁定梨水,也就是個字符串拭荤。之前提到 __LINKEDIT 段中也存儲了需要 bind 的指針,以及指針需要指向的符號疫诽。dyld 需要找到 symbol 對應(yīng)的實現(xiàn)舅世,這需要很多計算,去符號表里查找奇徒。找到后會將內(nèi)容存儲到 __DATA 段中的那個指針中雏亚。Binding 看起來計算量比 Rebasing 更大,但其實需要的 I/O 操作很少摩钙,因為之前 Rebasing 已經(jīng)替 Binding 做過了罢低。

ObjC Runtime

Objective-C 中有很多數(shù)據(jù)結(jié)構(gòu)都是靠 Rebasing 和 Binding 來修正(fix-up)的,比如 Class 中指向超類的指針和指向方法的指針胖笛。

ObjC 是個動態(tài)語言网持,可以用類的名字來實例化一個類的對象。這意味著 ObjC Runtime 需要維護一張映射類名與類的全局表长踊。當(dāng)加載一個 dylib 時功舀,其定義的所有的類都需要被注冊到這個全局表中。

C++ 中有個問題叫做易碎的基類(fragile base class)身弊。ObjC 就沒有這個問題辟汰,因為會在加載時通過 fix-up 動態(tài)類中改變實例變量的偏移量列敲。

在 ObjC 中可以通過定義類別(Category)的方式改變一個類的方法。有時你想要添加方法的類在另一個 dylib 中莉擒,而不在你的鏡像中(也就是對系統(tǒng)或別人的類動刀)酿炸,這時也需要做些 fix-up。

ObjC 中的 selector 必須是唯一的涨冀。

Initializers

C++ 會為靜態(tài)創(chuàng)建的對象生成初始化器。而在 ObjC 中有個叫 +load 的方法麦萤,然而它被廢棄了鹿鳖,現(xiàn)在建議使用 +initialize。對比詳見:http://stackoverflow.com/questions/13326435/nsobject-load-and-initialize-what-do-they-do

現(xiàn)在有了主執(zhí)行文件壮莹,一堆 dylib翅帜,其依賴關(guān)系構(gòu)成了一張巨大的有向圖,那么執(zhí)行初始化器的順序是什么命满?自底向上涝滴!按照依賴關(guān)系,先加載葉子節(jié)點胶台,然后逐步向上加載中間節(jié)點歼疮,直至最后加載根節(jié)點。這種加載順序確保了安全性诈唬,加載某個 dylib 前韩脏,其所依賴的其余 dylib 文件肯定已經(jīng)被預(yù)先加載。

最后 dyld 會調(diào)用 main() 函數(shù)铸磅。main() 會調(diào)用 UIApplicationMain()赡矢。

改善啟動時間

從點擊 App 圖標(biāo)到加載 App 閃屏之間會有個動畫,我們希望 App 啟動速度比這個動畫更快阅仔。雖然不同設(shè)備上 App 啟動速度不一樣吹散,但啟動時間最好控制在 400ms。需要注意的是啟動時間一旦超過 20s八酒,系統(tǒng)會認(rèn)為發(fā)生了死循環(huán)并殺掉 App 進程空民。當(dāng)然啟動時間最好以 App 所支持的最低配置設(shè)備為準(zhǔn)。直到 applicationWillFinishLaunching 被調(diào)動丘跌,App 才啟動結(jié)束袭景。

測量啟動時間

Warm launch: App 和數(shù)據(jù)已經(jīng)在內(nèi)存中
Cold launch: App 不在內(nèi)核緩沖存儲器中

冷啟動(Cold launch)耗時才是我們需要測量的重要數(shù)據(jù),為了準(zhǔn)確測量冷啟動耗時闭树,測量前需要重啟設(shè)備耸棒。在 main() 方法執(zhí)行前測量是很難的,好在 dyld 提供了內(nèi)建的測量方法:在 Xcode 中 Edit scheme -> Run -> Auguments 將環(huán)境變量 DYLD_PRINT_STATISTICS 設(shè)為 1报辱∮胙辏控制臺輸出的內(nèi)容如下:

Total pre-main time: 228.41 milliseconds (100.0%)
         dylib loading time:  82.35 milliseconds (36.0%)
        rebase/binding time:   6.12 milliseconds (2.6%)
            ObjC setup time:   7.82 milliseconds (3.4%)
           initializer time: 132.02 milliseconds (57.8%)
           slowest intializers :
             libSystem.B.dylib : 122.07 milliseconds (53.4%)
                CoreFoundation :   5.59 milliseconds (2.4%)

優(yōu)化啟動時間

可以針對 App 啟動前的每個步驟進行相應(yīng)的優(yōu)化工作。

加載 Dylib

之前提到過加載系統(tǒng)的 dylib 很快,因為有優(yōu)化幅疼。但加載內(nèi)嵌(embedded)的 dylib 文件很占時間米奸,所以盡可能把多個內(nèi)嵌 dylib 合并成一個來加載,或者使用 static archive爽篷。使用 dlopen() 來在運行時懶加載是不建議的悴晰,這么做可能會帶來一些問題,并且總的開銷更大逐工。

Rebase/Binding

之前提過 Rebaing 消耗了大量時間在 I/O 上铡溪,而在之后的 Binding 就不怎么需要 I/O 了,而是將時間耗費在計算上泪喊。所以這兩個步驟的耗時是混在一起的棕硫。

之前說過可以從查看 __DATA 段中需要修正(fix-up)的指針,所以減少指針數(shù)量才會減少這部分工作的耗時袒啼。對于 ObjC 來說就是減少 Class,selectorcategory 這些元數(shù)據(jù)的數(shù)量哈扮。從編碼原則和設(shè)計模式之類的理論都會鼓勵大家多寫精致短小的類和方法,并將每部分方法獨立出一個類別蚓再,其實這會增加啟動時間滑肉。對于 C++ 來說需要減少虛方法,因為虛方法會創(chuàng)建 vtable对途,這也會在 __DATA 段中創(chuàng)建結(jié)構(gòu)赦邻。雖然 C++ 虛方法對啟動耗時的增加要比 ObjC 元數(shù)據(jù)要少,但依然不可忽視实檀。最后推薦使用 Swift 結(jié)構(gòu)體惶洲,它需要 fix-up 的內(nèi)容較少。

ObjC Setup

針對這步所能事情很少膳犹,幾乎都靠 Rebasing 和 Binding 步驟中減少所需 fix-up 內(nèi)容恬吕。因為前面的工作也會使得這步耗時減少。

Initializer

顯式初始化

  • 使用 +initialize 來替代 +load
  • 不要使用 __atribute__((constructor)) 將方法顯式標(biāo)記為初始化器须床,而是讓初始化方法調(diào)用時才執(zhí)行铐料。比如使用 dispatch_once(),pthread_once()std::once()。也就是在第一次使用時才初始化豺旬,推遲了一部分工作耗時钠惩。

隱式初始化

對于帶有復(fù)雜(non-trivial)構(gòu)造器的 C++ 靜態(tài)變量:

  1. 在調(diào)用的地方使用初始化器。
  2. 只用簡單值類型賦值(POD:Plain Old Data)族阅,這樣靜態(tài)鏈接器會預(yù)先計算 __DATA 中的數(shù)據(jù)篓跛,無需再進行 fix-up 工作。
  3. 使用編譯器 warning 標(biāo)志 -Wglobal-constructors 來發(fā)現(xiàn)隱式初始化代碼坦刀。
  4. 使用 Swift 重寫代碼愧沟,因為 Swift 已經(jīng)預(yù)先處理好了蔬咬,強力推薦。

不要在初始化方法中調(diào)用 dlopen()沐寺,對性能有影響林艘。因為 dyld 在 App 開始前運行,由于此時是單線程運行所以系統(tǒng)會取消加鎖混坞,但 dlopen() 開啟了多線程狐援,系統(tǒng)不得不加鎖,這就嚴(yán)重影響了性能拔第,還可能會造成死鎖以及產(chǎn)生未知的后果咕村。所以也不要在初始化器中創(chuàng)建線程。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蚊俺,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子逛万,更是在濱河造成了極大的恐慌泳猬,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宇植,死亡現(xiàn)場離奇詭異得封,居然都是意外死亡,警方通過查閱死者的電腦和手機指郁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門忙上,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人闲坎,你說我怎么就攤上這事疫粥。” “怎么了腰懂?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵梗逮,是天一觀的道長。 經(jīng)常有香客問我绣溜,道長慷彤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任怖喻,我火速辦了婚禮底哗,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘锚沸。我一直安慰自己跋选,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布咒吐。 她就那樣靜靜地躺著野建,像睡著了一般属划。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上候生,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天同眯,我揣著相機與錄音,去河邊找鬼唯鸭。 笑死须蜗,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的目溉。 我是一名探鬼主播明肮,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼缭付!你這毒婦竟也來了柿估?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤陷猫,失蹤者是張志新(化名)和其女友劉穎秫舌,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體绣檬,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡足陨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了娇未。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片墨缘。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖零抬,靈堂內(nèi)的尸體忽然破棺而出镊讼,到底是詐尸還是另有隱情,我是刑警寧澤媚值,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布狠毯,位于F島的核電站,受9級特大地震影響褥芒,放射性物質(zhì)發(fā)生泄漏嚼松。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一锰扶、第九天 我趴在偏房一處隱蔽的房頂上張望献酗。 院中可真熱鬧,春花似錦坷牛、人聲如沸罕偎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽颜及。三九已至甩苛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間俏站,已是汗流浹背讯蒲。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留肄扎,地道東北人墨林。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像犯祠,于是被迫代替她去往敵國和親旭等。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355

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