啟動速度篇

app啟動速度通常關(guān)乎用戶對app的總體評價兰绣,在這方面也有很多優(yōu)秀關(guān)于優(yōu)化方面的文章雕欺,不過這類文章更多地著墨于具體的優(yōu)化方案,對原理的介紹往往并不詳實收捣,所以對于想了解個中原理進(jìn)而深入學(xué)習(xí)系統(tǒng)機制的研發(fā)會有些美中不足的感覺届案。
本文根據(jù)wwdc 2012 iOS App Performance: Responsivenesswwdc 2016 Optimizing App Startup Timewwdc 2017 app start time:Past,Present and Future深入探討啟動原理與優(yōu)化策略

應(yīng)用啟動概論

伴隨app啟動的過程會出現(xiàn)app應(yīng)用界面放大出現(xiàn)的效果罢艾,iPhone及iPad上這個放大動畫分別為400ms與500ms楣颠,如果動畫結(jié)束時app已經(jīng)啟動完成,那么用戶看起來就像app在點擊了圖標(biāo)之后馬上啟動了一樣咐蚯,這樣的啟動速度自然是最佳的童漩。

watchdog

watchdog機制會在app發(fā)生超時的場景下強行中止其運行,由下表可知它對啟動場景的最大容忍時間為20s(xcode在debug期間會禁用watchdog)

場景 watchdog 超時
啟動 20秒
恢復(fù)運行 10秒
暫停(退后臺) 10秒
退出 6秒
后臺執(zhí)行 10分鐘

啟動時間的衡量

watchdog判定啟動結(jié)束的時間點是第一個CATransaction的結(jié)束春锋,這個點意味著UI在CPU中第一次布局和繪制的結(jié)束矫膨,其標(biāo)志性的api調(diào)用為[UIApplication _reportAppLaunchFinished](iOS6及之前版本,是此內(nèi)部api調(diào)用為啟動結(jié)束標(biāo)志,但iOS8以后已經(jīng)無法斷點到這個api侧馅,現(xiàn)在做啟動優(yōu)化判定啟動結(jié)束的一般的做法是rootViewController的viewDidAppear調(diào)用時間點)
而對于特定功能的app比如相機應(yīng)用來說直奋,用戶所感知到的啟動終結(jié)應(yīng)當(dāng)是快門達(dá)到可點按狀態(tài)所需要的時間,也就是說如果在watchdog超時間內(nèi)完成界面啟動而功能卻并未完成初始化施禾,仍然算做啟動未完成脚线,只是已經(jīng)沒有在啟動過程被系統(tǒng)強行終止的危險而已。

啟動過程

階段 主要工作
鏈接和加載 1.庫映射到app進(jìn)程空間 2.綁定符號(比如app引用了framework中的某常量符號)3.運行靜態(tài)初始化
UIKit初始化 創(chuàng)建Fonts, status bar, 讀取user defaults, 反序列化main nib
Application回調(diào) 啟動行將結(jié)束時將控制權(quán)交回給app
首次CoreAnimation transaction new首個CATransaction,用于在didFinishLaunching后批處理layout和繪制views弥搞,發(fā)生在CA::Transaction::commit, iOS6以前這次提交最遲會在[UIApplication _reportAppLaunchFinished]中發(fā)生邮绿,盡管這個內(nèi)部api已經(jīng)難尋蹤跡,但這次commit在后續(xù)的iOS系統(tǒng)也一直發(fā)生在didFinishLaunching之后
鏈接與加載階段優(yōu)化策略

1.精簡依賴的framework(每個OC庫在加載階段都會有些額外的工作要做攀例,比如類的hash表需要在加載階段將各類添加進(jìn)去)
2.不要將require的framework標(biāo)記為optional船逮,因為optional需要更多的檢查消耗
3.避免靜態(tài)的初始化過程:
全局C++對象的創(chuàng)建:

    static std::map<int, int>  GlobalMap = {{1,2},{3,4}};

在main之前的load階段執(zhí)行的代碼:

+ (void)load{  //do any stuff }
__attribute__((constructor)) void doInitializationStuff() {}

將盡可能多的工作放到運行期去做,比如+(void)initialize{}

UIKit 初始化優(yōu)化策略

涉及的api

UIApplicationInitialize(iOS7以后應(yīng)該已經(jīng)沒有了)
UIApplicationInstantiateSingleton
-[UIApplication _createStatusBarWithRequestedStyle: ...]
-[UIApplication _loadMainNibFileName:bundle:]

1 精簡main nib的大小粤铭,更好的優(yōu)化自然是用代碼創(chuàng)建UI
2 不要在userdefault中存太多數(shù)據(jù)挖胃,因為userdefaults是作為property文件存儲的,整個property list會一次性反序列化梆惯,所以不要存大塊的數(shù)據(jù)酱鸭,比如

NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
NSData* data = UIImagePNGRepresentation(image);
[ud setObject: data forKey: @"image"];
Application 回調(diào) 優(yōu)化策略

此階段過程如下:

 回調(diào)application: willFinishLaunchingWithOptions:
 恢復(fù)application 狀態(tài)
 回調(diào)application: didFinishLaunchingWithOptions:
首次Core Animation Transaction

提交的重要階段:

  • 準(zhǔn)備階段:解碼圖片
  • layout: 計算所有l(wèi)ayer的大小(-layoutSubviews)
  • 繪制: -drawRect:
    提交階段的優(yōu)化策略很明確,即盡可能精簡root viewController首次出現(xiàn)時的view層次復(fù)雜度及圖片素材的總量垛吗,同時不要做太復(fù)雜的drawRect操作

app啟動原理及優(yōu)化實踐

這部分根據(jù)wwdc2016 session406, 主要內(nèi)容為啟動原理及優(yōu)化實踐

Mach-O 和虛存 掠影

Mach-O文件類型:

Executable - 應(yīng)用主二進(jìn)制文件
Dylib - 動態(tài)鏈接庫(類似于其它平臺的DSO和DLL)
Bundle - 無法鏈接的Dylib凹髓,只能通過dlopen(), 比如插件

Image - Executable, Dylib 或者 Bundle
Framework - 包含所屬資源與頭文件目錄的Dylib
Mach-O文件又劃分為段segment, 比如__TEXT, __DATA, __LINKEDIT, 每個段大小均為pagesize的整數(shù)倍,在arm64 pagesize為16KB怯屉,其它架構(gòu)為4KB蔚舀,段又劃分為sections,section之間不重疊

常見段名 內(nèi)容
__TEXT Mach-O頭锨络,代碼和只讀常量
__DATA 所有可讀可寫的內(nèi)容赌躺,比如全局變量,靜態(tài)變量等
__LINKEDIT 不包含函數(shù)與變量但包含函數(shù)與變量的信息羡儿,比如名字和地址礼患,即加載程序的“元數(shù)據(jù)”

Mach-O Universal Files
構(gòu)建同時支持32位與64位架構(gòu)時會將兩個架構(gòu)的Mach-O文件合并為UniversalFile


fat header

支持的架構(gòu)會列舉在fat header中,這個header也是一個page size

虛存
虛存是一種間接管理內(nèi)存的方式失受,是為了方便多進(jìn)程使用物理內(nèi)存讶泰,常見特性比如:當(dāng)訪問的虛存對應(yīng)的頁不在內(nèi)存中時發(fā)生的頁錯誤引發(fā)加載,同一內(nèi)存頁映射到多進(jìn)程中的內(nèi)存共享模式拂到,文件映射頁mmap()與lazy reading特性(讀到特定地址時才引發(fā)頁錯誤引發(fā)加載)痪署,copy on write(COW,多進(jìn)程共享數(shù)據(jù)頁,直到對其進(jìn)行修改時才引發(fā)內(nèi)存將頁復(fù)制到新內(nèi)存頁并將進(jìn)程映射的頁指向新內(nèi)存頁)
虛存的這些特性應(yīng)用在__TEXT段極為合適兄旬,COW對于__DATA段是很好的優(yōu)化狼犯,這也引出了另一個概念余寥,即臟數(shù)據(jù)頁和干凈數(shù)據(jù)頁:臟數(shù)據(jù)頁是包含特定進(jìn)程信息的頁,而干凈頁是內(nèi)核可以重新從disk中讀取的數(shù)據(jù)頁悯森,所以臟數(shù)據(jù)頁比干凈數(shù)據(jù)頁會帶來更多消耗宋舷。
頁權(quán)限屬性:rwx(代碼段設(shè)置為只讀 r,數(shù)據(jù)段 rw)

加載dylib的時候瓢姻,會將其文件映射到內(nèi)存中祝蝠,由于大部分全局變量都初始化為0,所以靜態(tài)優(yōu)化把它們都放在后面幻碱,以不占用空間绎狭,而且通過VM特性在首次讀取時將其填充為0,dyld的第一件事情是讀取Mach-O header褥傍,即第一頁儡嘶,會引發(fā)頁錯誤進(jìn)而進(jìn)行頁加載,它會發(fā)現(xiàn)此頁進(jìn)行了文件映射并加載文件首頁到物理內(nèi)存恍风。然后dyld開始讀mach header蹦狂,接著Mach header說在__LINKEDIT段中有些信息需要讀下,于是dyld開始讀最下面那段朋贬,同樣引發(fā)頁錯誤及加載凯楔,然后LINKEDIT告訴dyld需要對DATA段做一些修正才能讓dylib真正可運行。dyld于是開始入數(shù)據(jù)段寫一些數(shù)據(jù)兄世,這時候COW發(fā)生啼辣,數(shù)據(jù)頁變臟。而如果此時另一個進(jìn)程需要這個dylib御滩,那么TEXT段和LINKEDIT段只需要復(fù)用已經(jīng)在物理內(nèi)存頁中的這兩個段即可,操作系統(tǒng)只需要將對應(yīng)的內(nèi)存頁映射到新進(jìn)程的虛存中即可党远。而數(shù)據(jù)段如果仍在內(nèi)存中削解,也可以類似地映射,否則需要再次讀取disk沟娱,這算是動態(tài)庫加載的一項優(yōu)化氛驮。而LINKEDIT段只有在dyld修正DATA段的時候才有用,此后其內(nèi)存頁即可回收做它用济似。


Dylib加載

有兩件想提及的事情是安全是如何影響dyld的矫废,而正是這兩件事關(guān)安全的事情影響了dyld。
一個是ASLR(address space layout randomization)砰蠢,用于隨機化加載地址的常用技術(shù)蓖扑。
另一個是code sign,構(gòu)建時每頁Mach-O文件都會生成各自的加密hash台舱,均存儲在LINKEDIT中律杠,于是每頁均可以驗證其是否被更動過手腳。

EXEC

exec是一個系統(tǒng)調(diào)用,會使用指定的新程序替換當(dāng)前進(jìn)程柜去,內(nèi)核會將整個進(jìn)程空間抹掉并將你指定的可執(zhí)行文件映射進(jìn)來灰嫉,由于ASLR的存在,映射到的是一個隨機的地址嗓奢,并且從這個地址到0的整個區(qū)域都會標(biāo)記為不可讀不可寫不可執(zhí)行讼撒。在32位進(jìn)程中,這個區(qū)域大小為4KB股耽,在64位進(jìn)程最少為4GB根盒。它會用來捕獲空指針引用及指針截斷錯誤,因為塊虛存未映射任何實際物理地址豺谈,所以訪問會引發(fā)異常郑象。
多年以前EXEC很輕松,因為只需要將程序映射到進(jìn)程茬末,再設(shè)置PC就可以開始執(zhí)行了厂榛,但隨著動態(tài)鏈接庫的發(fā)明,出現(xiàn)了helper程序來幫忙加載動態(tài)鏈接庫丽惭,apple平臺上這個程序叫做dyld击奶。內(nèi)核在映射可執(zhí)行文件到進(jìn)程之后,會映射dyld到進(jìn)程另一個隨機地址责掏,然后將PC設(shè)置進(jìn)dyld中柜砾,并讓其完成進(jìn)程的啟動。

dyld執(zhí)行階段 職責(zé)
加載所有dylib 讀取主可執(zhí)行文件的頭以獲取依賴庫的列表换衬,然后開始找每個dylib痰驱,一旦找到即開始打開并讀取dylib的頭部,因為需要確保它是Mach-O文件瞳浦,驗證這個文件并找到其code signature担映,將code signature注冊到內(nèi)核,對dylib的每個段調(diào)用mmap叫潦,最終遞歸加載完所有依賴的dylib(由于大多都是OS dylib蝇完,OS本身在構(gòu)建時預(yù)先做了很多dyld在加載這些dylib時需要做的計算等工作)
fix-up(包含下列2個階段) 將這些獨立的dylib綁定在一起,鑒于code signing不允許我們修改指令矗蕊,而為了在不修改指令的情況下讓dylib調(diào)用另一個dylib短蜕,需要借助現(xiàn)代編譯代碼生成的動態(tài)PIC(Position Independent Code)機制,意味著代碼可以動態(tài)加載進(jìn)虛存傻咖,也就是說調(diào)用是間接尋址的朋魔。如果被調(diào)用方會以指針的形式存在DATA數(shù)據(jù)段中,這個指針才是最后真正被調(diào)用的
rebase 調(diào)整指向image內(nèi)部的指針没龙,早期可以為dylib指定偏好的加載地址铺厨,如果進(jìn)程可以滿足這些要求缎玫,則dyld不需要做任何fix-up,但現(xiàn)在ASLR的存在解滓,dylib會加載到隨機的地址赃磨,這種情況下需要計算一個slide=actual_address-preferred_address,并為每個內(nèi)部指針加上這個slide,而這些內(nèi)部指針本身的地址是存在LINKEDIT段中洼裤。由于我們只是將數(shù)據(jù)映射到進(jìn)程邻辉,修改地址的時候一般會發(fā)生COW,所以rebase通常因為這些IO會很耗時腮鞍。所幸修改是順序進(jìn)行的值骇,所以在內(nèi)核看來寫數(shù)據(jù)是順序地進(jìn)行的,內(nèi)核會為我們預(yù)加載移国,提升性能
binding 調(diào)整指向image外的指針吱瘩,通常是用字符串形式的符號名來表示的,所以"malloc"代表這個指針指向malloc迹缀,而dyld就需要找到其實現(xiàn)并將地址寫入指針使碾,也意味著要查找符號表并做很多計算。雖然需要很多的計算祝懂,但由于rebasing已經(jīng)做了大部分的IO票摇,所以binding需要的IO比較少
objc 到了objc階段,大部分類數(shù)據(jù)結(jié)構(gòu)已經(jīng)就緒砚蓬,指向其方法矢门,父類的指針皆已就緒,但還有一些objc運行時需要的數(shù)據(jù)未完成灰蛙。第一個是OC動態(tài)性需要可以通過類名創(chuàng)建對象祟剔,所以O(shè)C運行時需要維護(hù)一個類名與類的映射表。所以加載類定義時摩梧,類名需要注冊到一個全局表中峡扩。另一個問題是C++中的fragile base class問題,OC由于在加載階段fix-up時會修正所有ivar的偏移而不存在這個問題障本。另一個問題是在另外的dylib中定義的category,需要在這個階段將方法添加到類實現(xiàn)中响鹃。此外OC依賴于selector的唯一性驾霜,所以還需要將selector唯一化
initializer 執(zhí)行c++編譯生成的等號右邊的任意初始化表達(dá)式,執(zhí)行oc中的+load

查看image中需fix-up的指針信息的命令:

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

上述所有工作完成之后买置,dyld會調(diào)用可執(zhí)行文件中的main()

提升啟動速度實用策略

首先最好的啟動時間是比啟動動畫更快粪糙,前述啟動概論有提及,400ms最好
在scheme中添加環(huán)境變量 DYLD_PRINT_STATISTICS 1 后忿项,在device log中會輸出main之前所有執(zhí)行過程的耗時統(tǒng)計

優(yōu)化項 優(yōu)化策略 備注
dylib加載 少用動態(tài)鏈接庫蓉冈,使用靜態(tài)庫,懶加載(dlopen) 合并動態(tài)鏈接庫(基本不太具有可操作性城舞,因為要先拿到所有的代碼),dlopen實際會引發(fā)很多細(xì)微的性能和正確性問題寞酿,而且會在后續(xù)引發(fā)相較下更多的消耗家夺,雖然加載被延遲的,這個方案可行伐弹,但是需要謹(jǐn)慎使用拉馋。
fix-up 使用前述工具查看fix-up指針?biāo)诘膕egment及section,減少fixup的指針數(shù)
減少C++虛函數(shù)的使用惨好,因為它會創(chuàng)建虛函數(shù)表煌茴,會和OC的metaclass一樣在DATA段中添加需要fix-up的數(shù)據(jù)
可以多使用swift struct,因為它們生成的需要fix-up的指針更少日川,而且swift更加內(nèi)聯(lián)化的特性可以更多地減少fix-up指針
如果在objc域中見到oc類的符號蔓腐,則可以考慮精減OC類數(shù)量及實例變量數(shù)量,尤其是對于一些鼓勵實現(xiàn)很多簡單的類的編程模式龄句,這些簡單的類通常只有一到兩個方法回论,這種編程模式會導(dǎo)致啟動速度越來越慢
initializer 將+load更多地用+initialize代替,C/C++中的____attribute____((constructor))顯式初始化函數(shù)更多地由call site initializer即dispatch_once或者 pthread_once, std::once代替
隱式初始化C++全局變量使用call site initializer撒璧,或者使用非全局?jǐn)?shù)據(jù)去代替透葛,又或者不使用重量的初始化,比如C++中的POD,plain old data
當(dāng)然也可以用swift來改寫卿樱,因為swift全局變量首先肯定會在被使用之前初始化僚害,但并不是在initializer中初始化,而是使用dispatch_once
對于POD類型數(shù)據(jù)繁调,靜態(tài)鏈接器會為DATA段預(yù)計算所有數(shù)據(jù)萨蚕,所以這些數(shù)據(jù)不需要運行,也不需要fix-up蹄胰,這種隱式初始化可以通過Apple LLVM 8.1 - Custom Compiler Flags=>Other Warning Flags 添加編譯器警告Flag -Wglobal-constructors 來提示此類initializer
dyld運行時可以不使用鎖因為它是單線程運行的岳遥,但如果使用了dlopen,initializers運行會相應(yīng)發(fā)生改變裕寨,同時需要打開鎖以應(yīng)對多線程運行的情況浩蓉,這也意味著處理不好可能會有死鎖的危險;另外也不要在initializer中啟動線程

抖音代碼段重排啟動優(yōu)化方案

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末宾袜,一起剝皮案震驚了整個濱河市捻艳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌庆猫,老刑警劉巖认轨,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異月培,居然都是意外死亡嘁字,警方通過查閱死者的電腦和手機恩急,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來纪蜒,“玉大人衷恭,你說我怎么就攤上這事』舨簦” “怎么了匾荆?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長杆烁。 經(jīng)常有香客問我牙丽,道長,這世上最難降的妖魔是什么兔魂? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任烤芦,我火速辦了婚禮,結(jié)果婚禮上析校,老公的妹妹穿的比我還像新娘构罗。我一直安慰自己,他們只是感情好智玻,可當(dāng)我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布遂唧。 她就那樣靜靜地躺著,像睡著了一般吊奢。 火紅的嫁衣襯著肌膚如雪盖彭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天页滚,我揣著相機與錄音召边,去河邊找鬼。 笑死裹驰,一個胖子當(dāng)著我的面吹牛隧熙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播幻林,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼贞盯,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了沪饺?” 一聲冷哼從身側(cè)響起邻悬,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎随闽,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肝谭,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡掘宪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年蛾扇,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片魏滚。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡镀首,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出鼠次,到底是詐尸還是另有隱情更哄,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布腥寇,位于F島的核電站成翩,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏赦役。R本人自食惡果不足惜麻敌,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望掂摔。 院中可真熱鬧术羔,春花似錦、人聲如沸乙漓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽叭披。三九已至寥殖,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間趋观,已是汗流浹背扛禽。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留皱坛,地道東北人编曼。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像剩辟,于是被迫代替她去往敵國和親掐场。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,925評論 2 344

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