深入理解iOS App的啟動過程

前言

啟動時間是衡量應(yīng)用品質(zhì)的重要指標(biāo)。

本文首先會從原理上出發(fā)沥曹,講解iOS系統(tǒng)是如何啟動APP的髓帽,然后從main函數(shù)之前和main函數(shù)之后倆個角度去分析如何優(yōu)化啟動時間。

準(zhǔn)備知識

mach-O

哪些名詞指的是Mach-O

  • Executable 可執(zhí)行文件
  • Dylib 動態(tài)庫
  • Bundle 無法被連接的動態(tài)庫驳阎,只能通過dlopen()加載
  • Image 指的是Executable抗愁,Dylib或者Bundle的一種,文中會多次使用Image這個名詞呵晚。
  • Framework 動態(tài)庫和對應(yīng)的頭文件和資源文件的集合

Apple出品的操作系統(tǒng)的可執(zhí)行文件格式幾乎都是mach-O蜘腌,iOS當(dāng)然也不例外。mach-o可以大致分為三部分:


mach-o的結(jié)構(gòu)
  • Header頭部饵隙,包含可以執(zhí)行的CPU架構(gòu)撮珠,比如x86,arm64
  • Load commands 加載命令金矛,包含文件的組織架構(gòu)和在虛擬內(nèi)存中的布局方式
  • Data芯急,數(shù)據(jù),包含load commands中需要的各個段(segment)的數(shù)據(jù)驶俊,每個Segment的大小都是Page的整數(shù)倍娶耍。

我們用MachOView打開Demo工程的可以執(zhí)行文件,來驗(yàn)證下mach-o的文件布局:

MachOView中的文件布局

圖中分析的mach-o文件來源于PullToRefreshKit饼酿。這是一個純Swift的編寫的工程榕酒。
那么Data部分又包含那些segment呢胚膊?絕大多數(shù)mach-o包括以下三個階段(支持用戶自定義Segment,但是很少使用)

  • __TEXT代碼段想鹰,只讀澜掩,包含函數(shù),和只讀的字符串杖挣,上圖中類似__TEXT,__text的都是代碼段
  • __Data數(shù)據(jù)段肩榕,讀寫,包括可讀寫的全局變量等惩妇,__DATA,__data都是數(shù)據(jù)段
  • __LINKEDIT包含了方法和變量的元數(shù)據(jù)(位置株汉,偏移量),以及代碼簽名等信息歌殃。
    關(guān)于mach-o更多細(xì)節(jié)乔妈,可以看看文檔:《Mac OS X ABI Mach-O File Format Reference

dyld

dyld的全稱是dynamic loader,它的作用是加載一個進(jìn)程所需要的image氓皱,dyld是開源的路召。

Virtual Memory

虛擬內(nèi)存是在物理內(nèi)存上建立的一個邏輯地址空間,它向上(應(yīng)用)提供了一個連續(xù)的邏輯地址空間波材,向下隱藏了物理內(nèi)存的細(xì)節(jié)股淡。
虛擬內(nèi)存使得邏輯地址可以沒有實(shí)際的物理地址,也可以讓多個邏輯地址對應(yīng)到一個物理地址廷区。
虛擬內(nèi)存被劃分為一個個大小相同的Page(64位系統(tǒng)上是16KB)唯灵,提高管理和讀寫的效率。 Page又分為只讀和讀寫的Page隙轻。

虛擬內(nèi)存是建立在物理內(nèi)存和進(jìn)程之間的中間層埠帕。在iOS上,當(dāng)內(nèi)存不足的時候玖绿,會嘗試釋放那些只讀的Page敛瓷,因?yàn)橹蛔x的Page在下次被訪問的時候,可以再從磁盤讀取斑匪。如果沒有可用內(nèi)存呐籽,會通知在后臺的App(也就是在這個時候收到了memory warning),如果在這之后仍然沒有可用內(nèi)存秤标,則會殺死在后臺的App绝淡。

Page fault

在應(yīng)用執(zhí)行的時候,它被分配的邏輯地址空間都是可以訪問的苍姜,當(dāng)應(yīng)用訪問一個邏輯Page牢酵,而在對應(yīng)的物理內(nèi)存中并不存在的時候,這時候就發(fā)生了一次Page fault衙猪。當(dāng)Page fault發(fā)生的時候馍乙,會中斷當(dāng)前的程序布近,在物理內(nèi)存中尋找一個可用的Page,然后從磁盤中讀取數(shù)據(jù)到物理內(nèi)存丝格,接著繼續(xù)執(zhí)行當(dāng)前程序撑瞧。

Dirty Page & Clean Page

  • 如果一個Page可以從磁盤上重新生成,那么這Page稱為Clear Page
  • 如果一個Page包含了進(jìn)程相關(guān)信息显蝌,那么這個Page稱為Dirty Page

像代碼段這種只讀的Page就是Clean Page预伺。而數(shù)據(jù)段(__DATA)這種讀寫的Page,當(dāng)寫數(shù)據(jù)發(fā)生的時候曼尊,會觸發(fā)CO(Copy on write)酬诀,也就是寫時復(fù)制,Page會被標(biāo)記成Dirty骆撇,同時會被復(fù)制瞒御。

想要了解更多細(xì)節(jié),可以閱讀文檔:Memory Usage Performance Guidelines

啟動過程

使用dyld2啟動應(yīng)用的過程如圖:

iOS應(yīng)用啟動過程

大致的過程如下:

  1. 加載dyld到App進(jìn)程
  2. 加載動態(tài)庫(包括所依賴的所有動態(tài)庫)
  3. Rebase
  4. Bind
  5. 初始化Objective C Runtime
  6. 其它的初始化代碼

加載動態(tài)庫

dyld會首先讀取mach-o文件的Header和load commands神郊。
接著就知道了這個可執(zhí)行文件依賴的動態(tài)庫肴裙。例如加載動態(tài)庫A到內(nèi)存,接著檢查A所依賴的動態(tài)庫涌乳,就這樣的遞歸加載蜻懦,直到所有的動態(tài)庫加載完畢。通常一個App所依賴的動態(tài)庫在100-400個左右爷怀,其中大多數(shù)都是系統(tǒng)的動態(tài)庫阻肩,它們會被緩存到dyld shared cache,這樣讀取的效率會很高运授。

查看mach-o文件所依賴的動態(tài)庫,可以通過MachOView的圖形化界面(展開Load Command就能看到)乔煞,也可以通過命令行otool吁朦。

192:Desktop Leo$ otool -L demo 
demo:
    @rpath/PullToRefreshKit.framework/PullToRefreshKit (compatibility version 1.0.0, current version 1.0.0)
    /System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 1444.12.0)
    /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
    @rpath/libswiftCore.dylib (compatibility version 1.0.0, current version 900.0.65)
    @rpath/libswiftCoreAudio.dylib (compatibility version 1.0.0, current version 900.0.65)
    //...

Rebase && Bind

這里先來講講為什么要Rebase?

有兩種主要的技術(shù)來保證應(yīng)用的安全:ASLR和Code Sign渡贾。

ASLR的全稱是Address space layout randomization逗宜,翻譯過來就是“地址空間布局隨機(jī)化”。App被啟動的時候空骚,程序會被影射到邏輯的地址空間纺讲,這個邏輯的地址空間有一個起始地址,而ASLR技術(shù)使得這個起始地址是隨機(jī)的囤屹。如果是固定的熬甚,那么黑客很容易就可以由起始地址+偏移量找到函數(shù)的地址。

Code Sign相信大多數(shù)開發(fā)者都知曉肋坚,這里要提一點(diǎn)的是乡括,在進(jìn)行Code sign的時候肃廓,加密哈希不是針對于整個文件,而是針對于每一個Page的诲泌。這就保證了在dyld進(jìn)行加載的時候盲赊,可以對每一個page進(jìn)行獨(dú)立的驗(yàn)證。

mach-o中有很多符號敷扫,有指向當(dāng)前mach-o的哀蘑,也有指向其他dylib的,比如printf葵第。那么绘迁,在運(yùn)行時,代碼如何準(zhǔn)確的找到printf的地址呢羹幸?

mach-o中采用了PIC技術(shù)脊髓,全稱是Position Independ code。當(dāng)你的程序要調(diào)用printf的時候栅受,會先在__DATA段中建立一個指針指向printf将硝,在通過這個指針實(shí)現(xiàn)間接調(diào)用。dyld這時候需要做一些fix-up工作屏镊,即幫助應(yīng)用程序找到這些符號的實(shí)際地址依疼。主要包括兩部分

  • Rebase 修正內(nèi)部(指向當(dāng)前mach-o文件)的指針指向
  • Bind 修正外部指針指向
Rebase && Bind

之所以需要Rebase,是因?yàn)閯倓偺岬降腁SLR使得地址隨機(jī)化而芥,導(dǎo)致起始地址不固定律罢,另外由于Code Sign,導(dǎo)致不能直接修改Image棍丐。Rebase的時候只需要增加對應(yīng)的偏移量即可误辑。待Rebase的數(shù)據(jù)都存放在__LINKEDIT中。
可以通過MachOView查看:Dynamic Loader Info -> Rebase Info

192:Desktop Leo$ xcrun dyldinfo -bind demo 
bind information:
segment section          address        type    addend dylib            symbol
__DATA  __got            0x10003C038    pointer      0 PullToRefreshKit __T016PullToRefreshKit07DefaultC4LeftC9textLabelSo7UILabelCvWvd
__DATA  __got            0x10003C040    pointer      0 PullToRefreshKit __T016PullToRefreshKit07DefaultC5RightC9textLabelSo7UILabelCvWvd
__DATA  __got            0x10003C048    pointer      0 PullToRefreshKit __T016PullToRefreshKit07DefaultC6FooterC9textLabelSo7UILabelCvWvd
__DATA  __got            0x10003C050    pointer      0 PullToRefreshKit __T016PullToRefreshKit07DefaultC6HeaderC7spinnerSo23UIActivityIndicatorViewCvWvd
//...

Rebase解決了內(nèi)部的符號引用問題歌逢,而外部的符號引用則是由Bind解決巾钉。在解決Bind的時候,是根據(jù)字符串匹配的方式查找符號表秘案,所以這個過程相對于Rebase來說是略慢的砰苍。

同樣,也可以通過xcrun dyldinfo來查看Bind的信息阱高,比如我們查看bind信息中赚导,包含UITableView的部分:

192:Desktop Leo$ xcrun dyldinfo -bind demo | grep UITableView
__DATA  __objc_classrefs 0x100041940    pointer      0 UIKit            _OBJC_CLASS_$_UITableView
__DATA  __objc_classrefs 0x1000418B0    pointer      0 UIKit            _OBJC_CLASS_$_UITableViewCell
__DATA  __objc_data      0x100041AC0    pointer      0 UIKit            _OBJC_CLASS_$_UITableViewController
__DATA  __objc_data      0x100041BE8    pointer      0 UIKit            _OBJC_CLASS_$_UITableViewController
__DATA  __objc_data      0x100042348    pointer      0 UIKit            _OBJC_CLASS_$_UITableViewController
__DATA  __objc_data      0x100042718    pointer      0 UIKit            _OBJC_CLASS_$_UITableViewController
__DATA  __data           0x100042998    pointer      0 UIKit            _OBJC_METACLASS_$_UITableViewController
__DATA  __data           0x100042A28    pointer      0 UIKit            _OBJC_METACLASS_$_UITableViewController
__DATA  __data           0x100042F10    pointer      0 UIKit            _OBJC_METACLASS_$_UITableViewController
__DATA  __data           0x1000431A8    pointer      0 UIKit            _OBJC_METACLASS_$_UITableViewController

Objective C

Objective C是動態(tài)語言,所以在執(zhí)行main函數(shù)之前赤惊,需要把類的信息注冊到一個全局的Table中吼旧。同時,Objective C支持Category荐捻,在初始化的時候黍少,也會把Category中的方法注冊到對應(yīng)的類中寡夹,同時會唯一Selector,這也是為什么當(dāng)你的Cagegory實(shí)現(xiàn)了類中同名的方法后厂置,類中的方法會被覆蓋菩掏。

另外爽彤,由于iOS開發(fā)時基于Cocoa Touch的叁怪,所以絕大多數(shù)的類起始都是系統(tǒng)類,所以大多數(shù)的Runtime初始化起始在Rebase和Bind中已經(jīng)完成楣富。

Initializers

接下來就是必要的初始化部分了访忿,主要包括幾部分:

  • +load方法瞧栗。
  • C/C++靜態(tài)初始化對象和標(biāo)記為attribute(constructor)的方法

這里要提一點(diǎn)的就是,+load方法已經(jīng)被棄用了海铆,如果你用Swift開發(fā)迹恐,你會發(fā)現(xiàn)根本無法去寫這樣一個方法,官方的建議是實(shí)用initialize卧斟。區(qū)別就是殴边,load是在類裝載的時候執(zhí)行,而initialize是在類第一次收到message前調(diào)用珍语。

dyld3

上文的講解是dyld2的加載方式锤岸。而最新的是dyld3加載方式略有不同:

dyld2和dyld3的區(qū)別

dyld2是純粹的in-process,也就是在程序進(jìn)程內(nèi)執(zhí)行的板乙,也就意味著只有當(dāng)應(yīng)用程序被啟動的時候是偷,dyld2才能開始執(zhí)行任務(wù)。

dyld3則是部分out-of-process募逞,部分in-process蛋铆。圖中,虛線之上的部分是out-of-process的放接,在App下載安裝和版本更新的時候會去執(zhí)行戒职,out-of-process會做如下事情:

  • 分析Mach-o Headers
  • 分析依賴的動態(tài)庫
  • 查找需要Rebase & Bind之類的符號
  • 把上述結(jié)果寫入緩存

這樣,在應(yīng)用啟動的時候透乾,就可以直接從緩存中讀取數(shù)據(jù),加快加載速度磕秤。

啟動時間

冷啟動 VS 熱啟動

如果你剛剛啟動過App乳乌,這時候App的啟動所需要的數(shù)據(jù)仍然在緩存中,再次啟動的時候稱為熱啟動市咆。如果設(shè)備剛剛重啟汉操,然后啟動App,這時候稱為冷啟動蒙兰。

啟動時間在小于400ms是最佳的磷瘤,因?yàn)閺狞c(diǎn)擊圖標(biāo)到顯示Launch Screen芒篷,到Launch Screen消失這段時間是400ms。啟動時間不可以大于20s采缚,否則會被系統(tǒng)殺掉针炉。

在Xcode中,可以通過設(shè)置環(huán)境變量來查看App的啟動時間扳抽,DYLD_PRINT_STATISTICS和DYLD_PRINT_STATISTICS_DETAILS篡帕。

image.png
Total pre-main time:  43.00 milliseconds (100.0%)
         dylib loading time:  19.01 milliseconds (44.2%)
        rebase/binding time:   1.77 milliseconds (4.1%)
            ObjC setup time:   3.98 milliseconds (9.2%)
           initializer time:  18.17 milliseconds (42.2%)
           slowest intializers :
             libSystem.B.dylib :   2.56 milliseconds (5.9%)
   libBacktraceRecording.dylib :   3.00 milliseconds (6.9%)
    libMainThreadChecker.dylib :   8.26 milliseconds (19.2%)
                       ModelIO :   1.37 milliseconds (3.1%)

對于這個libMainThreadChecker.dylib估計(jì)很多同學(xué)會有點(diǎn)陌生,這是XCode 9新增的動態(tài)庫贸呢,用來做主線成檢查的镰烧。

優(yōu)化啟動時間

啟動時間這個名詞,不同的人有不同的定義楞陷。在我看來怔鳖,

啟動時間是用戶點(diǎn)擊App圖標(biāo),到第一個界面展示的時間固蛾。

以main函數(shù)作為分水嶺结执,啟動時間其實(shí)包括了兩部分:main函數(shù)之前和main函數(shù)到第一個界面的viewDidAppear:。所以魏铅,優(yōu)化也是從兩個方面進(jìn)行的昌犹,個人建議優(yōu)先優(yōu)化后者,因?yàn)榻^大多數(shù)App的瓶頸在自己的代碼里览芳。

Main函數(shù)之后

我們首先來分析下斜姥,從main函數(shù)開始執(zhí)行,到你的第一個界面顯示沧竟,這期間一般會做哪些事情铸敏。

  • 執(zhí)行AppDelegate的代理方法,主要是didFinishLaunchingWithOptions
  • 初始化Window悟泵,初始化基礎(chǔ)的ViewController結(jié)構(gòu)(一般是UINavigationController+UITabViewController)
  • 獲取數(shù)據(jù)(Local DB/Network)杈笔,展示給用戶。

UIViewController

延遲初始化那些不必要的UIViewController糕非。

比如網(wǎng)易新聞:

image.png

在啟動的時候只需要初始化首頁的頭條頁面即可蒙具。像“要聞”,“我的”等頁面朽肥,則延遲加載禁筏,即啟動的時候只是一個UIViewController作為占位符給TabController,等到用戶點(diǎn)擊了再去進(jìn)行真正的數(shù)據(jù)和視圖的初始化工作衡招。

AppDelegate

通常我們會在AppDelegate的代理方法里進(jìn)行初始化工作篱昔,主要包括了兩個方法:

  • didFinishLaunchingWithOptions
  • applicationDidBecomeActive

優(yōu)化這些初始化的核心思想就是:

能延遲初始化的盡量延遲初始化,不能延遲初始化的盡量放到后臺初始化。

這些工作主要可以分為幾類:

  • 三方SDK初始化州刽,比如Crash統(tǒng)計(jì); 像分享之類的空执,可以等到第一次調(diào)用再出初始化。
  • 初始化某些基礎(chǔ)服務(wù)穗椅,比如WatchDog辨绊,遠(yuǎn)程參數(shù)。
  • 啟動相關(guān)日志房待,日志往往涉及到DB操作邢羔,一定要放到后臺去做
  • 業(yè)務(wù)方初始化,這個交由每個業(yè)務(wù)自己去控制初始化時間桑孩。

對于didFinishLaunchingWithOptions的代碼拜鹤,建議按照以下的方式進(jìn)行劃分:

@interface AppDelegate ()
//業(yè)務(wù)方需要的生命周期回調(diào)
@property (strong, nonatomic) NSArray<id<UIApplicationDelegate>> * eventQueues;
//主框架負(fù)責(zé)的生命周期回調(diào)
@property (strong, nonatomic) id<UIApplicationDelegate> basicDelegate;
@end

然后,你會得到一個非常干凈的AppDelegate文件:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    for (id<UIApplicationDelegate> delegate in self.eventQueues) {
        [delegate application:application didFinishLaunchingWithOptions:launchOptions];
    }
    return [self.basicDelegate application:application didFinishLaunchingWithOptions:launchOptions];
}

由于對這些初始化進(jìn)行了分組流椒,在開發(fā)期就可以很容易的控制每一個業(yè)務(wù)的初始化時間:

CFTimeInterval startTime = CACurrentMediaTime();
//執(zhí)行方法
CFTimeInterval endTime = CACurrentMediaTime();

用Time Profiler找到元兇

Time Profiler在分析時間占用上非常強(qiáng)大敏簿。實(shí)用的時候注意三點(diǎn)

  • 在打包模式下分析(一般是Release),這樣和線上環(huán)境一樣。
  • 記得開啟dsym宣虾,不然無法查看到具體的函數(shù)調(diào)用堆棧
  • 分析性能差的設(shè)備惯裕,對于支持iOS 8的,一般分析iphone 4s或者iphone 5绣硝。

一個典型的分析界面如下:

image.png

幾點(diǎn)要注意:

  1. 分析啟動時間蜻势,一般只關(guān)心主線程
  2. 選擇Hide System Libraries和Invert Call Tree,這樣我們能專注于自己的代碼
  3. 右側(cè)可以看到詳細(xì)的調(diào)用堆棧信息

在某一行上雙擊鹉胖,我們可以進(jìn)入到代碼預(yù)覽界面握玛,去看看實(shí)際每一行占用了多少時間:

image.png

小結(jié)

不同的App在啟動的時候做的事情往往不同,但是優(yōu)化起來的核心思想無非就兩個:

  • 能延遲執(zhí)行的就延遲執(zhí)行甫菠。比如SDK的初始化挠铲,界面的創(chuàng)建。
  • 不能延遲執(zhí)行的寂诱,盡量放到后臺執(zhí)行拂苹。比如數(shù)據(jù)讀取,原始JSON數(shù)據(jù)轉(zhuǎn)對象痰洒,日志發(fā)送瓢棒。

Main函數(shù)之前

Main函數(shù)之前是iOS系統(tǒng)的工作,所以這部分的優(yōu)化往往更具有通用性丘喻。

dylibs

啟動的第一步是加載動態(tài)庫音羞,加載系統(tǒng)的動態(tài)庫使很快的,因?yàn)榭梢跃彺娌秩虞d內(nèi)嵌的動態(tài)庫速度較慢。所以舍肠,提高這一步的效率的關(guān)鍵是:減少動態(tài)庫的數(shù)量搀继。

  • 合并動態(tài)庫窘面,比如公司內(nèi)部由私有Pod建立了如下動態(tài)庫:XXTableView, XXHUD, XXLabel,強(qiáng)烈建議合并成一個XXUIKit來提高加載速度叽躯。

Rebase & Bind & Objective C Runtime

Rebase和Bind都是為了解決指針引用的問題财边。對于Objective C開發(fā)來說,主要的時間消耗在Class/Method的符號加載上点骑,所以常見的優(yōu)化方案是:

  • 減少__DATA段中的指針數(shù)量酣难。
  • 合并Category和功能類似的類。比如:UIView+Frame,UIView+AutoLayout…合并為一個
  • 刪除無用的方法和類黑滴。
  • 多用Swift Structs憨募,因?yàn)镾wfit Structs是靜態(tài)分發(fā)的。感興趣的同學(xué)可以看看我之前這篇文章:《Swift進(jìn)階之內(nèi)存模型和方法調(diào)度

Initializers

通常袁辈,我們會在+load方法中進(jìn)行method-swizzling菜谣,這也是Nshipster推薦的方式。

  • 用initialize替代load晚缩。不少同學(xué)喜歡用method-swizzling來實(shí)現(xiàn)AOP去做日志統(tǒng)計(jì)等內(nèi)容尾膊,強(qiáng)烈建議改為在initialize進(jìn)行初始化。
  • 減少__atribute__((constructor))的使用荞彼,而是在第一次訪問的時候才用dispatch_once等方式初始化冈敛。
  • 不要創(chuàng)建線程
  • 使用Swfit重寫代碼。

參考資料

原文地址:(https://blog.csdn.net/Hello_Hwc/article/details/78317863)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鸣皂,一起剝皮案震驚了整個濱河市抓谴,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌签夭,老刑警劉巖齐邦,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異第租,居然都是意外死亡措拇,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門慎宾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來丐吓,“玉大人,你說我怎么就攤上這事趟据∪纾” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵汹碱,是天一觀的道長粘衬。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么稚新? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任勘伺,我火速辦了婚禮,結(jié)果婚禮上褂删,老公的妹妹穿的比我還像新娘飞醉。我一直安慰自己,他們只是感情好屯阀,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布缅帘。 她就那樣靜靜地躺著,像睡著了一般难衰。 火紅的嫁衣襯著肌膚如雪钦无。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天召衔,我揣著相機(jī)與錄音铃诬,去河邊找鬼。 笑死苍凛,一個胖子當(dāng)著我的面吹牛趣席,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播醇蝴,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼宣肚,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了悠栓?” 一聲冷哼從身側(cè)響起霉涨,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎惭适,沒想到半個月后笙瑟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡癞志,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年往枷,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片凄杯。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡错洁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出戒突,到底是詐尸還是另有隱情屯碴,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布膊存,位于F島的核電站导而,受9級特大地震影響忱叭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜嗡载,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一窑多、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧洼滚,春花似錦、人聲如沸技潘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽享幽。三九已至铲掐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間值桩,已是汗流浹背摆霉。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留奔坟,地道東北人携栋。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像咳秉,于是被迫代替她去往敵國和親婉支。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

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