iOS啟動時間優(yōu)化

背景

一個項目做的時間長了,啟動流程往往容易雜亂恨豁,庫也用的越來越多沽瞭,APP的啟動時間也會慢慢變長。本次將針對iOS APP的啟動時間優(yōu)化一波豆同。

通常針對一個技術(shù)點做優(yōu)化的時候番刊,都要先了解清楚這個技術(shù)點有哪些流程,優(yōu)化的方向往往是減少流程的數(shù)量影锈,以及減少每個流程的消耗芹务。

本次優(yōu)化從結(jié)果上來看蝉绷,main階段的優(yōu)化效果最顯著,尤其是啟動時的一些IO操作處理枣抱,對啟動時間的減少有很大作用熔吗。多線程啟動的設(shè)計和驗證最有意思,但是在實踐上由于我們業(yè)務(wù)本身的原因佳晶,只開了額外一個子線程來并行啟動桅狠,且僅在子線程做了少量的獨立操作,這個要根據(jù)不同的業(yè)務(wù)去具體分析了轿秧。

一般說來中跌,pre-main階段的定義為APP開始啟動到系統(tǒng)調(diào)用main函數(shù)這一段時間;main階段則代表從main函數(shù)入口到主UI框架的viewDidAppear函數(shù)調(diào)用的這一段時間菇篡。(本文后續(xù)main階段的時間統(tǒng)計都用viewDidAppear作為基準而非的applicationWillFinishLaunching

本文前半部分講原理(內(nèi)容基本是從網(wǎng)上借鑒/摘錄)晒他,后半部分講實踐,pre-main階段的原理比較難理解逸贾,不過實踐倒是根據(jù)結(jié)論直接做就好了陨仅。

App啟動過程

①解析Info.plist 
加載相關(guān)信息,例如閃屏
沙箱建立铝侵、權(quán)限檢查
②Mach-O加載 
如果是胖二進制文件灼伤,尋找合適當(dāng)前CPU架構(gòu)的部分
加載所有依賴的Mach-O文件(遞歸調(diào)用Mach-O加載的方法)
定位內(nèi)部、外部指針引用咪鲜,例如字符串狐赡、函數(shù)等
加載類擴展(Category)中的方法
C++靜態(tài)對象加載、調(diào)用ObjC的 +load 函數(shù)
執(zhí)行聲明為__attribute__((constructor))的C函數(shù)
③程序執(zhí)行 
調(diào)用main()
調(diào)用UIApplicationMain()
調(diào)用applicationWillFinishLaunching

注意:在上述過程中疟丙,attribute((constructor))的函數(shù)調(diào)用會在+load函數(shù)調(diào)用之后颖侄,親測正確。也可以自己建個工程測試一下享郊。

換成另一個說法就是:

App開始啟動后览祖,系統(tǒng)首先加載可執(zhí)行文件(自身App的所有.o文件的集合),然后加載動態(tài)鏈接器dyld炊琉,dyld是一個專門用來加載動態(tài)鏈接庫的庫展蒂。 執(zhí)行從dyld開始,dyld從可執(zhí)行文件的依賴開始, 遞歸加載所有的依賴動態(tài)鏈接庫苔咪。

動態(tài)鏈接庫包括:iOS 中用到的所有系統(tǒng) framework锰悼,加載OC runtime方法的libobjc,系統(tǒng)級別的libSystem团赏,例如libdispatch(GCD)和libsystem_blocks (Block)箕般。

可執(zhí)行文件的內(nèi)核流程

如圖,當(dāng)啟動一個應(yīng)用程序時舔清,系統(tǒng)最后會根據(jù)你的行為調(diào)用兩個函數(shù)丝里,fork和execve可柿。fork功能創(chuàng)建一個進程;execve功能加載和運行程序丙者。這里有多個不同的功能复斥,比如execl,execv和exect,每個功能提供了不同傳參和環(huán)境變量的方法到程序中械媒。在OSX中目锭,每個這些其他的exec路徑最終調(diào)用了內(nèi)核路徑execve。

image

1纷捞、執(zhí)行exec系統(tǒng)調(diào)用痢虹,一般都是這樣,用fork()函數(shù)新建立一個進程主儡,然后讓進程去執(zhí)行exec調(diào)用奖唯。我們知道,在fork()建立新進程之后糜值,父進程與子進程共享代碼段(TEXT)丰捷,但數(shù)據(jù)空間(DATA)是分開的,但父進程會把自己數(shù)據(jù)空間的內(nèi)容copy到子進程中去寂汇,還有上下文也會copy到子進程中去病往。

2、為了提高效率骄瓣,采用一種寫時copy的策略停巷,即創(chuàng)建子進程的時候,并不copy父進程的地址空間榕栏,父子進程擁有共同的地址空間畔勤,只有當(dāng)子進程需要寫入數(shù)據(jù)時(如向緩沖區(qū)寫入數(shù)據(jù)),這時候會復(fù)制地址空間扒磁,復(fù)制緩沖區(qū)到子進程中去庆揪。從而父子進程擁有獨立的地址空間。而對于fork()之后執(zhí)行exec之前渗磅,這種策略能夠很好的提高效率嚷硫,如果一開始就copy,那么exec之后始鱼,子進程(此處可以說是父進程也可以說是子進程,因為倆進程的數(shù)據(jù)此時是一樣的)的數(shù)據(jù)會被放棄脆贵,被新的進程所代替医清。見下圖:

image

啟動時間的分布,pre-main和main階段原理淺析

image
image

rebase修復(fù)的是指向當(dāng)前鏡像內(nèi)部的資源指針卖氨; 而bind指向的是鏡像外部的資源指針会烙。

rebase步驟先進行负懦,需要把鏡像讀入內(nèi)存,并以page為單位進行加密驗證柏腻,保證不會被篡改纸厉,所以這一步的瓶頸在IO。bind在其后進行五嫂,由于要查詢符號表颗品,來指向跨鏡像的資源,加上在rebase階段沃缘,鏡像已被讀入和加密驗證躯枢,所以這一步的瓶頸在于CPU計算。這兩個步驟在下面會詳細闡述槐臀。

pre-main過程

image

main過程

image

一些概念

什么是dyld?

動態(tài)鏈接庫的加載過程主要由dyld來完成锄蹂,dyld是蘋果的動態(tài)鏈接器。

系統(tǒng)先讀取App的可執(zhí)行文件(Mach-O文件)水慨,從里面獲得dyld的路徑得糜,然后加載dyld,dyld去初始化運行環(huán)境晰洒,開啟緩存策略掀亩,加載程序相關(guān)依賴庫(其中也包含我們的可執(zhí)行文件),并對這些庫進行鏈接欢顷,最后調(diào)用每個依賴庫的初始化方法槽棍,在這一步,runtime被初始化抬驴。當(dāng)所有依賴庫的初始化后撇簿,輪到最后一位(程序可執(zhí)行文件)進行初始化砸西,在這時runtime會對項目中所有類進行類結(jié)構(gòu)初始化,然后調(diào)用所有的load方法。最后dyld返回main函數(shù)地址菇存,main函數(shù)被調(diào)用,我們便來到了熟悉的程序入口芳来。

當(dāng)加載一個 Mach-O 文件 (一個可執(zhí)行文件或者一個庫) 時梆暮,動態(tài)鏈接器首先會檢查共享緩存看看是否存在其中,如果存在胧卤,那么就直接從共享緩存中拿出來使用唯绍。每一個進程都把這個共享緩存映射到了自己的地址空間中。這個方法大大優(yōu)化了 OS X 和 iOS 上程序的啟動時間枝誊。

image

問題:測試發(fā)現(xiàn)况芒,由于手機從開機后,連續(xù)兩次啟動同一個APP的pre-main實際時間的差值比較大叶撒,這一步可以在真機上復(fù)現(xiàn)绝骚,那么這兩次啟動pre-main的時間差值耐版,是跟系統(tǒng)的framework關(guān)系比較大,還是跟APP自身依賴的第三方framework關(guān)系比較大呢压汪?

回答:操作系統(tǒng)對于動態(tài)庫有一個共享的空間粪牲,在這個空間被填滿,或者沒有其他機制來清理這一塊的內(nèi)存之前止剖,動態(tài)庫被加載到內(nèi)存后就一直存在腺阳。所以,問題中開機后連續(xù)啟動同一個APP兩次的pre-main時間的差值滴须,可以認為是動態(tài)庫被第一次加載后緩存到內(nèi)存造成的舌狗,時間上也肯定是第二次比第一次快。比如有一些系統(tǒng)的動態(tài)庫扔水,操作系統(tǒng)還暫時沒用到痛侍,但是你的APP用到了,在第一次啟動APP就會加載到內(nèi)存魔市,第二次就直接拿內(nèi)存里的主届。你自己APP用到的動態(tài)庫也類似,只不過APP自己的動態(tài)庫只能共享給自己的Extension待德,而不能給別的進程君丁,進程有相互獨立的地址空間,而且你的APP是用戶態(tài)将宪,而不是內(nèi)核態(tài)绘闷,不能像系統(tǒng)的動態(tài)庫那樣被所有進程訪問。詳情見《現(xiàn)代操作系統(tǒng)》较坛。

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–)渊涝。

ASLR(Address Space Layout Randomization):地址空間布局隨機化慎璧,鏡像會在隨機的地址上加載。

傳統(tǒng)方式下跨释,進程每次啟動采用的都是固定可預(yù)見的方式胸私,這意味著一個給定的程序在給定的架構(gòu)上的進程初始虛擬內(nèi)存都是基本一致的,而且在進程正常運行的生命周期中鳖谈,內(nèi)存中的地址分布具有非常強的可預(yù)測性岁疼,這給了黑客很大的施展空間(代碼注入,重寫內(nèi)存)缆娃;

如果采用ASLR捷绒,進程每次啟動,地址空間都會被簡單地隨機化贯要,但是只是偏移暖侨,不是攪亂。大體布局——程序文本崇渗、數(shù)據(jù)和庫是一樣的字逗,但是具體的地址都不同了,可以阻擋黑客對地址的猜測 宅广。

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

關(guān)于虛擬內(nèi)存

我們開發(fā)者開發(fā)過程中所接觸到的內(nèi)存均為虛擬內(nèi)存套腹,虛擬內(nèi)存使App認為它擁有連續(xù)的可用的內(nèi)存(一個連續(xù)完整的地址空間),這是系統(tǒng)給我們的饋贈资铡,而實際上电禀,它通常是分布在多個物理內(nèi)存碎片,系統(tǒng)的虛擬內(nèi)存空間映射vm_map負責(zé)虛擬內(nèi)存和物理內(nèi)存的映射關(guān)系笤休。

ARM處理器64bit的架構(gòu)情況下尖飞,也就是0x000000000 - 0xFFFFFFFFF,每個16進制數(shù)是4位,即2的36次冪政基,就是64GB贞铣,即App最大的虛擬內(nèi)存空間為64GB。

共享動態(tài)庫其實就是共享的物理內(nèi)存中的那份動態(tài)庫沮明,App虛擬內(nèi)存中的共享動態(tài)庫并未真實分配物理內(nèi)存辕坝,使用時虛擬內(nèi)存會訪問同一份物理內(nèi)存達到共享動態(tài)庫的目的,iPhone7 PLUS(之前的產(chǎn)品最大為2GB)的物理內(nèi)存RAM也只有3GB荐健,那么超過3GB的物理內(nèi)存如何處理呢酱畅,系統(tǒng)會使用一部分硬盤空間ROM來充當(dāng)內(nèi)存使用,在需要時進行數(shù)據(jù)交換江场,當(dāng)然磁盤的數(shù)據(jù)交換是遠遠慢于物理內(nèi)存的纺酸,這也是我們內(nèi)存過載時,App卡頓的原因之一址否。

系統(tǒng)使用動態(tài)鏈接有幾點好處:

代碼共用:很多程序都動態(tài)鏈接了這些 lib餐蔬,但它們在內(nèi)存和磁盤中中只有一份。

易于維護:由于被依賴的 lib 是程序執(zhí)行時才鏈接的在张,所以這些 lib 很容易做更新用含,比如libSystem.dylib 是 libSystem.B.dylib 的替身,哪天想升級直接換成libSystem.C.dylib 然后再替換替身就行了帮匾。

減少可執(zhí)行文件體積:相比靜態(tài)鏈接啄骇,動態(tài)鏈接在編譯時不需要打進去,所以可執(zhí)行文件的體積要小很多瘟斜。

image

上圖中缸夹,TEXT段兩個進程共用,DATA段每個進程各一份螺句。

下面開始詳細分析pre-main的各個階段

加載 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ù)先計算和緩存起來惦银,加載速度很快咆课。

加載系統(tǒng)的 dylib 很快末誓,因為有優(yōu)化(因為操作系統(tǒng)自己要用部分framework所以在操作系統(tǒng)開機后就已經(jīng)加載到內(nèi)存了)。但加載內(nèi)嵌(embedded)的 dylib 文件很占時間书蚪,所以盡可能把多個內(nèi)嵌 dylib 合并成一個來加載喇澡,或者使用 static archive。使用 dlopen() 來在運行時懶加載是不建議的善炫,這么做可能會帶來一些問題撩幽,并且總的開銷更大库继。

在每個動態(tài)庫的加載過程中箩艺, dyld需要:

①分析所依賴的動態(tài)庫
②找到動態(tài)庫的mach-o文件
③打開文件
④驗證文件
⑤在系統(tǒng)核心注冊文件簽名
⑥對動態(tài)庫的每一個segment調(diào)用mmap()

針對這一步驟的優(yōu)化有:

①減少非系統(tǒng)庫的依賴;
②使用靜態(tài)庫而不是動態(tài)庫宪萄;
③合并非系統(tǒng)動態(tài)庫為一個動態(tài)庫艺谆;

在加載所有的動態(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语泽。

Rebase

Rebasing:在鏡像內(nèi)部調(diào)整指針的指向,針對mach-o在加載到內(nèi)存中不是固定的首地址(ASLR)這一現(xiàn)象做數(shù)據(jù)修正的過程视卢;

由于ASLR(address space layout randomization)的存在踱卵,可執(zhí)行文件和動態(tài)鏈接庫在虛擬內(nèi)存中的加載地址每次啟動都不固定,所以需要這2步來修復(fù)鏡像中的資源指針据过,來指向正確的地址惋砂。 rebase修復(fù)的是指向當(dāng)前鏡像內(nèi)部的資源指針; 而bind指向的是鏡像外部的資源指針蝶俱。

在iOS4.3前會把dylib加載到指定地址班利,所有指針和數(shù)據(jù)對于代碼來說都是固定的,dyld 就無需做rebase/binding了榨呆。

iOS4.3后引入了 ASLR 罗标,dylib會被加載到隨機地址庸队,這個隨機的地址跟代碼和數(shù)據(jù)指向的舊地址會有偏差,dyld 需要修正這個偏差闯割,做法就是將 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 消耗。

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

rebase步驟先進行淹朋,需要把鏡像讀入內(nèi)存,并以page為單位進行加密驗證钉答,保證不會被篡改础芍,所以這一步的瓶頸在IO。bind在其后進行数尿,由于要查詢符號表仑性,來指向跨鏡像的資源,加上在rebase階段砌创,鏡像已被讀入和加密驗證虏缸,所以這一步的瓶頸在于CPU計算。

Binding

Binding:將指針指向鏡像外部的內(nèi)容嫩实,binding就是將這個二進制調(diào)用的外部符號進行綁定的過程刽辙。比如我們objc代碼中需要使用到NSObject, 即符號OBJC_CLASS$_NSObject,但是這個符號又不在我們的二進制中甲献,在系統(tǒng)庫 Foundation.framework中宰缤,因此就需要binding這個操作將對應(yīng)關(guān)系綁定到一起;

lazyBinding就是在加載動態(tài)庫的時候不會立即binding, 當(dāng)時當(dāng)?shù)谝淮握{(diào)用這個方法的時候再實施binding晃洒。 做到的方法也很簡單: 通過dyld_stub_binder這個符號來做慨灭。lazyBinding的方法第一次會調(diào)用到dyld_stub_binder, 然后dyld_stub_binder負責(zé)找到真實的方法,并且將地址bind到樁上球及,下一次就不用再bind了氧骤。

Binding 是處理那些指向 dylib 外部的指針,它們實際上被符號(symbol)名稱綁定吃引,也就是個字符串筹陵。__LINKEDIT段中也存儲了需要 bind 的指針刽锤,以及指針需要指向的符號。dyld 需要找到 symbol 對應(yīng)的實現(xiàn)朦佩,這需要很多計算并思,去符號表里查找。找到后會將內(nèi)容存儲到__DATA 段中的那個指針中语稠。Binding 看起來計算量比 Rebasing 更大宋彼,但其實需要的 I/O 操作很少,Binding的時間主要是耗費在計算上仙畦,因為IO操作之前 Rebasing 已經(jīng)替 Binding 做過了输涕,所以這兩個步驟的耗時是混在一起的。

可以從查看__DATA 段中需要修正(fix-up)的指針议泵,所以減少指針數(shù)量才會減少這部分工作的耗時占贫。對于 ObjC 來說就是減少 Class,selector 和 category 這些元數(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)容較少凡蜻。

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

Rebase&&Binding該階段的優(yōu)化關(guān)鍵在于減少__DATA segment中的指針數(shù)量划栓。我們可以優(yōu)化的點有:

①減少Objc類數(shù)量, 減少selector數(shù)量条获,把未使用的類和函數(shù)都可以刪掉 
②減少C++虛函數(shù)數(shù)量
③轉(zhuǎn)而使用swift stuct(其實本質(zhì)上就是為了減少符號的數(shù)量忠荞,使用swift語言來開發(fā)?)

未使用類的掃描,可以利用linkmap文件和otool工機具反編譯APP的可進行二進制文件得出一個大概的結(jié)果帅掘,但是不算非常精確委煤,掃描出來后需要手動一個個確認。掃描原理大致是classlist和classref兩者的差值修档,所有的類和使用了的類的差值就是未使用的類啦碧绞。因為未使用的類主要優(yōu)化的是pre-main的時間,根據(jù)測試我們的工程pre-main時間并不長吱窝,所以本次并沒有針對這一塊做優(yōu)化讥邻。(TODO:寫腳本來驗證這一點)寓免。

ObjC SetUp

主要做以下幾件事來完成Objc Setup:

①讀取二進制文件的 DATA 段內(nèi)容,找到與 objc 相關(guān)的信息
②注冊 Objc 類计维,ObjC Runtime 需要維護一張映射類名與類的全局表袜香。當(dāng)加載一個 dylib 時,其定義的所有的類都需要被注冊到這個全局表中鲫惶; 
③讀取 protocol 以及 category 的信息蜈首,把category的定義插入方法列表 (category registration), 
④確保 selector 的唯一性 

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 必須是唯一的想帅。

由于之前2步驟的優(yōu)化场靴,這一步實際上沒有什么可做的。幾乎都靠 Rebasing 和 Binding 步驟中減少所需 fix-up 內(nèi)容港准。因為前面的工作也會使得這步耗時減少旨剥。

Initializers

以上三步屬于靜態(tài)調(diào)整,都是在修改__DATA segment中的內(nèi)容浅缸,而這里則開始動態(tài)調(diào)整轨帜,開始在堆和棧中寫入內(nèi)容。 工作主要有:

1疗杉、Objc的+load()函數(shù)
2阵谚、C++的構(gòu)造函數(shù)屬性函數(shù) 形如attribute((constructor)) void DoSomeInitializationWork()
3、非基本類型的C++靜態(tài)全局變量的創(chuàng)建(通常是類或結(jié)構(gòu)體)(non-trivial initializer) 比如一個全局靜態(tài)結(jié)構(gòu)體的構(gòu)建烟具,如果在構(gòu)造函數(shù)中有繁重的工作梢什,那么會拖慢啟動速度

Objc的load函數(shù)和C++的靜態(tài)構(gòu)造函數(shù)采用由底向上的方式執(zhí)行,來保證每個執(zhí)行的方法朝聋,都可以找到所依賴的動態(tài)庫

1嗡午、dyld開始將程序二進制文件初始化
2、交由ImageLoader讀取image冀痕,其中包含了我們的類荔睹、方法等各種符號
3狸演、由于runtime向dyld綁定了回調(diào),當(dāng)image加載到內(nèi)存后僻他,dyld會通知runtime進行處理
4宵距、runtime接手后調(diào)用mapimages做解析和處理,接下來loadimages中調(diào)用 callloadmethods方法吨拗,遍歷所有加載進來的Class满哪,按繼承層級依次調(diào)用Class的+load方法和其 Category的+load方法

整個事件由dyld主導(dǎo),完成運行環(huán)境的初始化后劝篷,配合ImageLoader 將二進制文件按格式加載到內(nèi)存哨鸭,動態(tài)鏈接依賴庫,并由runtime負責(zé)加載成objc 定義的結(jié)構(gòu)娇妓,所有初始化工作結(jié)束后像鸡,dyld調(diào)用真正的main函數(shù)

C++ 會為靜態(tài)創(chuàng)建的對象生成初始化器。而在 ObjC 中有個叫 +load 的方法哈恰,然而它被廢棄了只估,現(xiàn)在建議使用 +initialize。對比詳見StackOverflow的一個連接;

這一步可以做的優(yōu)化有:

①使用 +initialize 來替代 +load
②不要使用 atribute((constructor)) 將方法顯式標記為初始化器蕊蝗,而是讓初始化方法調(diào)用時才執(zhí)行仅乓。
比如使用 dispatch_once(),pthread_once() 或 std::once()。也就是在第一次使用時才初始化蓬戚,推遲了一部分工作耗時。
也盡量不要用到C++的靜態(tài)對象宾抓。

從效率上來說子漩,在+load 和+initialize里執(zhí)行同樣的代碼,效率是一樣的石洗,即使有差距幢泼,也不會差距太大。 但所有的+load 方法都在啟動的時候調(diào)用讲衫,方法多了就會嚴重影響啟動速度了缕棵。 就說我們項目中,有200個左右+load方法涉兽,一共耗時大概1s 左右招驴,這塊就會嚴重影響到用戶感知了。 而+initialize方法是在對應(yīng) Class 第一次使用的時候調(diào)用枷畏,這是一個懶加載的方法别厘,理想情況下,這200個+load方法都使用+initialize來代替拥诡,將耗時分攤到用戶使用過程中触趴,每個方法平均耗時只有5ms氮发,用戶完全可以無感知。 因為load是在啟動的時候調(diào)用冗懦,而initialize是在類首次被使用的時候調(diào)用爽冕,不過當(dāng)你把load中的邏輯移到initialize中時候,一定要注意initialize的重復(fù)調(diào)用問題披蕉,能用dispatch_once()來完成的颈畸,就盡量不要用到load方法。

如果程序剛剛被運行過嚣艇,那么程序的代碼會被dyld緩存承冰,因此即使殺掉進程再次重啟加載時間也會相對快一點,如果長時間沒有啟動或者當(dāng)前dyld的緩存已經(jīng)被其他應(yīng)用占據(jù)食零,那么這次啟動所花費的時間就要長一點困乒,這就分別是熱啟動和冷啟動的概念。下文中的啟動時間統(tǒng)計贰谣,均統(tǒng)計的是第二次啟動后的數(shù)據(jù)娜搂。(具體dyld緩存的是動態(tài)庫而不是APP的可執(zhí)行代碼,緩存的時間取決于內(nèi)核是否會將其丟棄吱抚,跟操作系統(tǒng)的頁面置換機制或內(nèi)存清理機制有關(guān))

見下圖百宇,出處是這里

image

其實在我們APP的實踐過程中也會遇到類似的事情,只不過我只統(tǒng)計了第二次啟動后的時間秘豹,也就是定義中的熱啟動時間携御。

注:

通過在工程的scheme中添加環(huán)境變量DYLD_PRINT_STATISTICS,設(shè)置值為1既绕,App啟動加載時Xcode的控制臺就會有pre-main各個階段的詳細耗時輸出啄刹。但是DYLD_PRINT_STATISTICS變量打印時間是iOS10以后才支持的功能,所以需要用iOS10系統(tǒng)及以上的機器來做測試凄贩。

pre-main階段耗時的影響因素:

動態(tài)庫加載越多誓军,啟動越慢。
ObjC類越多疲扎,函數(shù)越多昵时,啟動越慢。
可執(zhí)行文件越大啟動越慢椒丧。
C的constructor函數(shù)越多壹甥,啟動越慢。
C++靜態(tài)對象越多瓜挽,啟動越慢盹廷。
ObjC的+load越多,啟動越慢。
整體上pre-main階段的優(yōu)化有:
①減少依賴不必要的庫俄占,不管是動態(tài)庫還是靜態(tài)庫管怠;如果可以的話,把動態(tài)庫改造成靜態(tài)庫缸榄;
如果必須依賴動態(tài)庫渤弛,則把多個非系統(tǒng)的動態(tài)庫合并成一個動態(tài)庫;
②檢查下 framework應(yīng)當(dāng)設(shè)為optional和required甚带,
如果該framework在當(dāng)前App支持的所有iOS系統(tǒng)版本都存在她肯,那么就設(shè)為required,否則就設(shè)為optional鹰贵,
因為optional會有些額外的檢查晴氨; 
③合并或者刪減一些OC類和函數(shù);
關(guān)于清理項目中沒用到的類碉输,使用工具AppCode代碼檢查功能籽前,查到當(dāng)前項目中沒有用到的類(也可以用根據(jù)linkmap文件來分析,但是準確度不算很高)敷钾;
有一個叫做[FUI](https://github.com/dblock/fui)的開源項目能很好的分析出不再使用的類枝哄,準確率非常高,唯一的問題是它處理不了動態(tài)庫和靜態(tài)庫里提供的類阻荒,也處理不了C++的類模板挠锥。
④刪減一些無用的靜態(tài)變量,
⑤刪減沒有被調(diào)用到或者已經(jīng)廢棄的方法侨赡,
方法見http://stackoverflow.com/questions/35233564/how-to-find-unused-code-in-xcode-7
和https://developer.Apple.com/library/ios/documentation/ToolsLanguages/Conceptual/Xcode_Overview/CheckingCodeCoverage.html蓖租。
⑥將不必須在+load方法中做的事情延遲到+initialize中,盡量不要用C++虛函數(shù)(創(chuàng)建虛函數(shù)表有開銷)
⑦類和方法名不要太長:iOS每個類和方法名都在__cstring段里都存了相應(yīng)的字符串值羊壹,所以類和方法名的長短也是對可執(zhí)行文件大小是有影響的菜秦;
因還是object-c的動態(tài)特性,因為需要通過類/方法名反射找到這個類/方法進行調(diào)用舶掖,object-c對象模型會把類/方法名字符串都保存下來浅浮;
⑧用dispatch_once()代替所有的 attribute((constructor)) 函數(shù)奠货、C++靜態(tài)對象初始化、ObjC的+load函數(shù)入挣;
⑨在設(shè)計師可接受的范圍內(nèi)壓縮圖片的大小嚣州,會有意外收獲鲫售。
壓縮圖片為什么能加快啟動速度呢?因為啟動的時候大大小小的圖片加載個十來二十個是很正常的该肴,
圖片小了情竹,IO操作量就小了,啟動當(dāng)然就會快了匀哄,比較靠譜的壓縮算法是TinyPNG秦效。

我們的實踐

統(tǒng)計了各個庫所占的size(使用之前做安裝包size優(yōu)化的一個腳本)雏蛮,基本上一個公共庫越大,類越多阱州,啟動時在pre-main階段所需要的時間也越多挑秉。

所以去掉了Realm,DiffMatchPatch源碼庫苔货,以及AlicloudHttpDNS犀概,BCConnectorBundl,F(xiàn)eedBack夜惭,SGMain和SecurityGuardSDK幾個庫姻灶;

結(jié)果如下:

靜態(tài)庫,少了7M左右:

image

第三方framework(其實也是靜態(tài)庫诈茧,只是腳本會分開統(tǒng)計而已)产喉,少了1M左右:

image

我們使用cocoapodbs并沒有設(shè)置use_frameworks,所以pod管理的有源碼的第三方庫都是靜態(tài)庫的形式若皱,而framework形式的靜態(tài)庫基本都是第三方公司提供的服務(wù)镊叁,上圖可以看到,size占比最大的還是阿里和騰訊兩家的SDK走触,比如阿里的推送和騰訊的直播和IM晦譬。

上圖在統(tǒng)計中,AliCloudHttpDNS的可執(zhí)行文件在Mac的Finder下的大小大概是10M互广,AlicloudUtils是3.4M敛腌,UTMini是16M,而UTDID只有1.6M惫皱。依賴關(guān)系上像樊,AliCloudHttpDNS依賴AlicloudUtils,而AlicloudUtils依賴UTMini和UTDID旅敷,UTMini依賴UTDID生棍。

上圖中在統(tǒng)計上,應(yīng)該是有所AlicloudUtils在左右兩個圖中size差值過大媳谁,應(yīng)該是依賴關(guān)系中UTMini導(dǎo)致的統(tǒng)計偏差涂滴。兩邊幾個庫加起來的差值大概是200kb,這也應(yīng)該是AlicloudHttpDNS這個庫所占的size大小晴音。

main階段

總體原則無非就是減少啟動的時候的步驟柔纵,以及每一步驟的時間消耗。

main階段的優(yōu)化大致有如下幾個點:

①減少啟動初始化的流程锤躁,能懶加載的就懶加載搁料,能放后臺初始化的就放后臺,
能夠延時初始化的就延時,不要卡主線程的啟動時間郭计,已經(jīng)下線的業(yè)務(wù)直接刪掉霸琴; 
②優(yōu)化代碼邏輯,去除一些非必要的邏輯和代碼拣宏,減少每個流程所消耗的時間沈贝; 
③啟動階段使用多線程來進行初始化,把CPU的性能盡量發(fā)揮出來勋乾; 
④使用純代碼而不是xib或者storyboard來進行UI框架的搭建宋下,尤其是主UI框架比如TabBarController這種,
盡量避免使用xib和storyboard辑莫,因為xib和storyboard也還是要解析成代碼來渲染頁面学歧,多了一些步驟; 

這里重點講一下多線程啟動設(shè)計的原理

首先各吨,iPhone有雙核枝笨,除了維持操作系統(tǒng)運轉(zhuǎn)和后臺進程(包括電話短信等守護進程),在打開APP時揭蜒,猜想雙核中的另一個核應(yīng)該有余力來幫忙分擔(dān)啟動的任務(wù)【待驗證】横浑;

其次,iPhone7開始使用A10屉更,iPhone8開始使用A11處理器徙融,根據(jù)維基百科的定義,A10 CPU包括兩枚高性能核心及兩枚低功耗核心瑰谜,A11則包括兩個高性能核心和四個高能效核心欺冀,而且相比A10,A11兩個性能核心的速度提升了25%萨脑,四個能效核心的速度提升了70%隐轩。而且蘋果還準備了第二代性能控制器,因此可以同時發(fā)揮六個核心的全部威力渤早,性能提升最高可達70%职车,多線程能力可見一斑。

多線程測試結(jié)果如下圖:

image

結(jié)論如下:

1.純算法的CPU運算鹊杖,指定計算的總數(shù)量提鸟,對于iPhone6和iPhone X來說,
把計算量平均分配到多線程比全部放在主線程執(zhí)行要快;
2.iPhone6三個線程跟兩個線程的總體耗時總體一致仅淑,甚至要多一點點,
所以對于iPhone6來說用兩個線程來做啟動設(shè)計足夠了胸哥;
3.iPhone X三個線程的耗時要比兩個線程短一些涯竟,但是差值已經(jīng)不算太大了;
四個線程跟三個線程的總體一致,偶爾能比三個線程快一點點庐船;

綜上银酬,利用多個線程來加速啟動時間的設(shè)計,是合理的筐钟。

但是多線程啟動的設(shè)計有幾個需要注意的點:

1.黑屏問題揩瞪;
2.用狀態(tài)機來設(shè)計,每個狀態(tài)機有2或3個線程在跑不同的任務(wù)篓冲,所有線程任務(wù)都完成后李破,進入到下一個狀態(tài),方便擴展壹将;
3.線程编凸ィ活問題,以及用完后要銷毀诽俯;
4.資源競爭或線程同步造成卡死的問題妇菱。

針對第一點,多線程跑初始化任務(wù)的時候暴区,可能主線程會有空閑等待子線程的階段闯团,而主線程一旦空閑,iOS系統(tǒng)的啟動畫面就會消失仙粱,如果此時APP尚未初始化完全房交,則可能會黑屏。為了避免黑屏缰盏,我們需要一個假的啟動畫面涌萤,在剛開始跑初始化任務(wù)時,就生成這個啟動畫面口猜,啟動過程完全結(jié)束后再去掉负溪。或者當(dāng)一個狀態(tài)機里的主線程跑完時檢查下是否所有線程都執(zhí)行完任務(wù)了济炎,如果沒有則去生成這個假的初始化頁面川抡,避免黑屏(我們采用后一種方式)。

第二點用狀態(tài)機來設(shè)計啟動须尚,每個狀態(tài)跑兩個或者多個線程崖堤,單個狀態(tài)里每個線程的耗時是不一樣的,跑完一個狀態(tài)再繼續(xù)下一個狀態(tài)耐床,可以多次測試去動態(tài)調(diào)整分派到各個線程里的任務(wù)密幔。

第三點線程保活則跟runloop有關(guān)撩轰,每個線程啟動后胯甩,跑完一個狀態(tài)昧廷,不能立馬就回收,不然下一個狀態(tài)的子線程就永遠不會執(zhí)行了偎箫;另外就是木柬,APP初始化完成后,線程要注意回收淹办。

第四點跟具體的業(yè)務(wù)有關(guān)眉枕,只要不是一個線程去做初始化,就有可能遇到線程間死鎖的問題怜森,比如下面采坑記錄里就有提到一個例子速挑。

我們在實踐中大概做了以下的幾點:

1.把啟動時RN包的刪除和拷貝操作,僅在APP安裝后第一次啟動時才做塔插,之后的啟動不再做這操作梗摇,
而是等到網(wǎng)絡(luò)請求RN數(shù)據(jù)回來,根據(jù)是否需要更新RN包的判斷想许,再去做這些IO操作從而避免啟動的耗時伶授。
iPhone5C能節(jié)省1.4s;
2.OSS token的獲取不是一個需要在啟動的時候必須要做的操作流纹,放到子線程去處理糜烹,大部分時候是節(jié)省10-15ms,偶爾能去到50ms漱凝;
3.去掉啟動狀態(tài)機里的原有定位服務(wù)疮蹦,原來SSZLocationManager的定位服務(wù)因為內(nèi)部依賴高德的SDK,
需要初始化SDK茸炒,iPhone5C大概耗時100ms愕乎。同時SSZLocationManager這個類代碼保留,
但是APP的工程去除對其的依賴壁公;
4.打點統(tǒng)計模塊里的定位服務(wù)權(quán)限請求改成異步感论,大概有50ms;
5.阿里百川的Feedback紊册,在網(wǎng)校并沒有使用比肄,直接去掉其初始化流程,大概5ms左右囊陡;
6.友盟的分享服務(wù)芳绩,沒有必要在啟動的時候去初始化,初始化任務(wù)丟到后臺線程解決撞反,大概600-800ms妥色;
7.狀態(tài)機跑完后的啟動內(nèi)存統(tǒng)計,放到后臺去做遏片,大概50ms垛膝;
8.UserAgentManager里對于webview是否為UIWebview的判斷鳍侣,以前是新創(chuàng)建一個對象使用對象方法來判斷,
修改為直接使用類方法吼拥,避免創(chuàng)建對象的消耗,節(jié)省約200ms线衫;
9.阿里云的HTTPDNS已經(jīng)沒有再使用了凿可,所以這里也可以直接去掉。大概20-40ms授账;
10.SSZAppConfig里把網(wǎng)絡(luò)請求放到后臺線程枯跑,這樣子就可以配合啟動狀態(tài)機把該任務(wù)放到子線程進行初始化了,
否則子線程消耗的時間太長了白热;
11.采用兩個線程來進行啟動流程的初始化敛助,用狀態(tài)機來控制狀態(tài)的變化。
但是要針對業(yè)務(wù)區(qū)分開屋确,并不是把一部分業(yè)務(wù)拆分到子線程纳击,就可以讓整體的啟動速度更快;
因為如果子線程有一些操作是要在主線程做的攻臀,有可能會出現(xiàn)等待主線程空閑再繼續(xù)的情況焕数;
或者當(dāng)兩個線程的耗時操作都是IO時,拆開到兩個線程刨啸,并不一定比單個線程去做IO操作要快堡赔。
12.主UI框架tabBarController的viewDidLoad函數(shù)里,去掉一些不必要的函數(shù)調(diào)用设联。
13.NSUserDefaults的synchronize函數(shù)盡量不要在啟動流程中去調(diào)用善已,統(tǒng)一在APP進入后臺,
willTerminate和完全進入前臺后把數(shù)據(jù)落地离例;

因為我們的項目用到了React Native技術(shù)(簡稱RN)换团,所以會有RN包的拷貝和更新這一操作,之前的邏輯是每次啟動都從bundle里拷貝一次到Document的指定目錄粘招,本次優(yōu)化修正為除了安裝后第一次啟動拷貝啥寇,其他時候不在做這個拷貝操作,不過RN包熱更新的覆蓋操作洒扎,還是每次都要做檢查的辑甜,如果有需要則執(zhí)行更新操作

其中遇到幾個坑:

①并不是什么任務(wù)都適合放子線程,有些任務(wù)在主線程大概10ms袍冷,放到子線程需要幾百ms磷醋,因為某些任務(wù)內(nèi)部可能會用到UIKit的api,又或者某些操作是需要提交到主線程去執(zhí)行的胡诗,關(guān)鍵是要搞清楚這個任務(wù)里邊究竟做了啥邓线,有些SDK并不見得適合放到子線程去初始化淌友,需要具體情況具體去測試和分析。

②AccountManager初始化導(dǎo)致的主線程卡死骇陈,子線程的任務(wù)依賴AccountManager震庭,主線程也依賴,當(dāng)子線程比主線程先調(diào)用時你雌,造成了主線程卡死器联,其原因是子線程會提交一個同步阻塞的操作到主線程,而此時主線程被dipatch_one的鎖鎖住了婿崭,所以造成子線程不能返回拨拓,主線程也無法繼續(xù)執(zhí)行。調(diào)試的時候還會用到符號斷點和LLDB去打印函數(shù)入?yún)ⅲㄒ话闶莚0-r3之間的寄存器)的值氓栈。

③RN包的拷貝檢查除了是否第一次打開APP之外渣磷,還要注意RN版本如果升級時,需要用新的包強制覆蓋掉舊的包授瘦,否則js代碼會一直得不到更新醋界。

實際優(yōu)化效果對比

由于只是去掉了幾個靜態(tài)庫,而且本來pre-main階段的耗時就不長奥务,基本在200ms-500ms左右物独,所以pre-main階段優(yōu)化前后效果并不明顯,有時候還沒有前后測試的誤差大氯葬。挡篓。。

main的階段的優(yōu)化效果還是很明顯的:

iPhone5C iOS10.3.3系統(tǒng)優(yōu)化前main階段的時間消耗為4秒左右帚称,優(yōu)化后基本在1.8秒內(nèi)官研;

iPhone7 iOS10.3.3系統(tǒng)優(yōu)化前main階段的時間消耗為1.1秒左右,優(yōu)化后基本在600ms內(nèi)闯睹;

iPhoneX iOS11.3.1系統(tǒng)優(yōu)化前main階段的時間消耗基本在1.5秒以上戏羽,優(yōu)化后在1秒內(nèi);

可以看到楼吃,同樣arm64架構(gòu)的機器始花,main階段是iPhone7比iPhoneX更快,說明就操作系統(tǒng)來說孩锡,iOS11.3要比iOS10.3慢不少酷宵;

詳細測試數(shù)據(jù)見下圖

image
image
image

上圖中iPhone5C為啥前后測試pre-main時間差值那么大?而且是優(yōu)化后的值比優(yōu)化前還要大躬窜?我也不知道浇垦,大概機器自己才知道吧。荣挨。男韧。

注意:

1.關(guān)于冷啟動和熱啟動朴摊,業(yè)界對冷啟動的定義沒有問題,普遍認為是手機開機后第一次啟動某個APP此虑,但是對熱啟動有不同的看法甚纲,有些人認為是按下home鍵把APP掛到后臺,之后點擊APP的icon再拉回來到前臺算是熱啟動朦前,也有些人認為是手機開機后在短時間內(nèi)第二次啟動APP(殺掉進程重啟)算是熱啟動(此時dyld會對部分APP的數(shù)據(jù)和庫進行緩存贩疙,所以比第一次啟動要快)。筆者認為APP從后臺拉起到前臺的時間沒啥研究的意義况既,而即使是短時間內(nèi)第二次啟動APP,啟動時間也是很重要的组民,所以在統(tǒng)計啟動時間時棒仍,筆者會傾向于后一種說法,不過具體怎么定義還是看個人吧臭胜,知道其中的區(qū)別就好莫其。

2.關(guān)于如何區(qū)分framework是靜態(tài)庫還是動態(tài)庫見這里。原理就是在終端使用指令file耸三,輸出如果是ar archive就是靜態(tài)庫乱陡,如果是動態(tài)庫則會輸出dynamically linked相關(guān)信息。

特別鳴謝

在做啟動優(yōu)化的過程中仪壮,得到了很多朋友們的幫助和支持憨颠。借鑒了淮哥狀態(tài)機的設(shè)計思路,同時也感謝singro大神的指點积锅,感謝劉金哥教我玩LLDB爽彤,感謝長元兄對于動態(tài)庫和靜態(tài)庫的指教,感謝森哥的鞭策和精神鼓舞缚陷,以及展少爺在整個過程中的技術(shù)支持适篙,引導(dǎo)和不耐其煩的解釋,再次謝謝大家箫爷,愛你們喲??嚷节!

參考鏈接:

1.優(yōu)化 App 的啟動時間;

2.iOS啟動優(yōu)化;

3.iOSApp啟動性能優(yōu)化;

4.今日頭條iOS客戶端啟動速度優(yōu)化;

5.XNU、dyld源碼分析Mach-O和動態(tài)庫的加載過程(上);

6.靜態(tài)庫虎锚,動態(tài)庫和framework;

7.iOS 靜態(tài)庫硫痰,動態(tài)庫與 Framework;

轉(zhuǎn)載:http://www.zoomfeng.com/blog/launch-time.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市翁都,隨后出現(xiàn)的幾起案子碍论,更是在濱河造成了極大的恐慌,老刑警劉巖柄慰,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鳍悠,死亡現(xiàn)場離奇詭異税娜,居然都是意外死亡,警方通過查閱死者的電腦和手機藏研,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進店門敬矩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蠢挡,你說我怎么就攤上這事弧岳。” “怎么了业踏?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵禽炬,是天一觀的道長。 經(jīng)常有香客問我勤家,道長腹尖,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任伐脖,我火速辦了婚禮热幔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘讼庇。我一直安慰自己绎巨,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布蠕啄。 她就那樣靜靜地躺著场勤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪介汹。 梳的紋絲不亂的頭發(fā)上却嗡,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天,我揣著相機與錄音嘹承,去河邊找鬼窗价。 笑死,一個胖子當(dāng)著我的面吹牛叹卷,可吹牛的內(nèi)容都是我干的撼港。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼骤竹,長吁一口氣:“原來是場噩夢啊……” “哼帝牡!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蒙揣,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤靶溜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體罩息,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡嗤详,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了瓷炮。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片葱色。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖娘香,靈堂內(nèi)的尸體忽然破棺而出苍狰,到底是詐尸還是另有隱情,我是刑警寧澤烘绽,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布淋昭,位于F島的核電站,受9級特大地震影響安接,放射性物質(zhì)發(fā)生泄漏响牛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一赫段、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧矢赁,春花似錦糯笙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至额获,卻和暖如春够庙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背抄邀。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工耘眨, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人境肾。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓剔难,卻偏偏與公主長得像,于是被迫代替她去往敵國和親奥喻。 傳聞我的和親對象是個殘疾皇子偶宫,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,685評論 2 360

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