文章來自于WWDC2016-406_optimizing_app_startup_time
目錄
一四瘫、理論
???????1.1认轨、Mach-O術語
???????1.2流礁、Mach-O Image File
???????1.3、Mach-O universal file
???????1.4硫戈、Virtual Memory虛擬內存
二、Mach-O 鏡像和虛擬內存的映射
???????2.1、Load dylibs
??????????????2.1.1窟坐、ASLR和code signing
??????????????2.1.2、exec
???????2.2绵疲、Rebase 和bind
???????2.3哲鸳、ObjC
???????2.4、initializers
三盔憨、減少啟動時間
???????3.1徙菠、設置環(huán)境變量
???????3.1、binding and rebasing
???????3.1郁岩、initializers
一婿奔、理論
1.1、Mach-O術語
`Mach-O`是一種用于不同運行時可執(zhí)行文件的文件類型问慎。
所以第一個可執(zhí)行文件萍摊,即應用程序中的主要二進制文件,它也是應用程序擴展中的主要二進制文件如叼。
文件類型:
- Executable -- 應用程序的主要二進制文件
-
dylib
-- 動態(tài)鏈接庫(對應Linux
平臺的DSO
和Windows
平臺的DLL
) -
Bundle -- 特殊類型的
dylib
, 只能在運行時通過dlopen
打開冰木,主要用于MacOS上的插件
Image -- 一種Executable
、dylib
或者Bundle
類型
Framework -- 一種dylib
笼恰,它有一個特殊的目錄結構片酝,用于持有該dylib
所需的文件(資源和頭文件)。
1.2挖腰、Mach-O 鏡像文件
???????Mach-O
鏡像由多個segment
組成雕沿,按照慣例,segment
名稱全部由大寫字母組成猴仑。每個segment
由多個page size
組成审轮,上面演示圖中的TEXT這個SEGMENT包含三個page size
肥哎,DATA 和LINKEDIT各占一個。page size
主要由硬件平臺決定疾渣,對于arm64
篡诽,page size
是16k,其它的平臺是4k榴捡。
另外可以以section
的角度審視Mach-O
的組成杈女。section
是segment
的一部分,它沒有整數(shù)倍page size
的限制吊圾,但是section
之間不能重疊达椰。section
是以小寫字母命名的。
???????大多數(shù)二進制文件都存在__TEXT, __DATA, __LINKEDIT 這三個segment
项乒。
-
__TEXT -- 二進制文件的起始段啰劲,內部包含
Mach header
,所有的機器指令以及只讀變量 如:c 中的字符串檀何。 - __DATA -- 包含所有的可讀寫內容:全局變量蝇裤,靜態(tài)變量等
- __LINKEDIT -- 包含如果加載程序的“元數(shù)據(jù)”,比如函數(shù)的名稱和地址等數(shù)據(jù)
1.3频鉴、Mach-O universal file
???????假如建立了一個64位的iOS 應用栓辜,那么就有了一個Mach-O
文件。如果想讓應用運行在32的機器上垛孔,只能重新在Xcode上編譯藕甩,這是會生成另外一個Mach-O
文件,如果兩個Mach-O文件合并到第三個文件中似炎,那么這個文件就是Mach-O universal file。
???????Mach-O universal文件起始位置是一個header文件,即Fat header
悯姊,它占用一頁空間的大小羡藐。Fat header
包含了所有的架構體系以及它們在文件中的偏移量。
???????按分頁來存儲這些segement
和 header
會浪費空間悯许,但這有利于虛擬內存的實現(xiàn)仆嗦。
1.4、Virtual Memory虛擬內存
軟件工程中有個諺語先壕,“計算機科學領域的任何問題都可以通過增加一個間接的中間層來解決” 瘩扼。
???????當有多個進程,如果同時映射到物理的RAM垃僚,就是利用了這一點集绰。
每個進程都有一個邏輯地址空間,它被映射到RAM的某個物理頁面∽还祝現(xiàn)在這個映射不是一對一的關系栽燕,邏輯地址空間也許沒有對應的RAM的物理頁面,或者多個邏輯地址空間被映射到同一個物理頁面。這就給我們提供了很多機會碍岔。
???????針對第一種情況會發(fā)生page fault(頁缺失)
浴讯,內核會停止該進程,并嘗試查看需要做什么蔼啦。
???????針對第二種情況RAM內存中的某些頁會發(fā)生共享榆纽。
???????第三個有意思的特點是file backed mapping
(文件備份映射)。實際情況不是將整個鏡像文件讀入到RAM中捏肢,而是利用mmap 調用來通知VM系統(tǒng)奈籽,我想要把鏡像文件中的一部分映射到進程中的某個范圍。沒有將整個鏡像文件讀入到RAM中猛计,而通過建立映射唠摹,當你第一次操作不同的地址時好像他們已經(jīng)被讀入到了內存中,每次當操作了一塊地址而這塊地址之前未被讀入到內存中時奉瘤,就會發(fā)生page fault
勾拉,內核就會將讀入那塊地址并與RAM中的某一頁建立映射。所以這就給了懶加載文件的機會盗温。
???????現(xiàn)在把這些特點串聯(lián)一下藕赞,dylib
或者image
中的TEXT段可能被映射到多個進程,它的讀取是懶加載形式的卖局,加載到內存中的page
可以被多個進程共享斧蜕。
???????DATA段是可讀可寫的,因此我們有一個稱為copy on write
的技巧砚偶,它類似于Apple文件系統(tǒng)中的克隆批销。
???????當所有的進程只是讀取全局變量,這時候共享起作用染坯,當有一個進程試著對某一個DATA頁寫操作時均芽,寫時復制就產(chǎn)生了。寫時復制會將當前RAM中的那一頁復制到RAM中的另一頁单鹿,并重新將進行寫操作的進程映射到新的頁掀宋。所以這個進程就擁有了這個頁。這時產(chǎn)生了兩個概念仲锄,clean page 和dirty page
劲妙。clean page 是被復制的那個頁,dirty page
是復制的那個頁儒喊。clean page是可以重新磁盤上讀取時內核可以重新產(chǎn)生的一些東西镣奋,dirty page
包含了進程中一些特殊信息。因此dirty page
的操作是相對昂貴的怀愧。
???????權限問題唆途,對于每一個page富雅,都可對其設定readable, writable和executable,或者這三個權限的組合肛搬。
二没佑、Mach-O 鏡像 和虛擬內存的映射
下面的這段話,最好結合官方的PPT温赔,要不然可以略過蛤奢。這部分主要利用一個例子講解Mach-O如何映射到內存中去,精華所在陶贼。英文好的同學可以直接看視頻啤贩,或相應的文檔。翻譯可能有些出入拜秧,有錯誤希望各位指出痹屹。
???????假設我們有一個dylib
,沒有將其讀入內存枉氮,而是跟內存有一個映射志衍,那么在內存中這個dylib
將占用8個頁大小的空間。對于節(jié)省下來的空間(這個dylib
映射到內存中占用8頁聊替,在磁盤中占用5頁)楼肪,他們的不同點是用零填充,事實證明全局變量就是用零進行初始化的惹悄,所以靜態(tài)鏈接器會做一次優(yōu)化春叫,把所有零填充的全局變量放到底部(__DATA段底部),而不占用任何磁盤空間泣港。相反暂殖,我們使用vm
特性告訴vm
第一次訪問這個頁面時,用零填充它当纱,所以不需要讀取呛每。
???????接下來dyld
(dynamic loader)第一件要做的事就是在當前的線程對應的內存中查看Mach header
,所以它將查看內存的頂部惫东,當查看的時候莉给,那里什么都沒有毙石,即沒有一個對應的物理映射廉沮,所以page fault
發(fā)生了。就在那時徐矩,內核意識到應該映射一個文件滞时,因為它讀取文件中的第一個頁并把它放入物理RAM中,并建立映射滤灯。
???????現(xiàn)在dyld
可以通過Mach header
讀取數(shù)據(jù)了坪稽。它讀取Mach header
曼玩,Mach header
說LINKEDIT段有一些你需要查看的信息,所以再一次窒百,dyld
指向線程一的底部黍判,它也導致了page fault
。內核將LINKEDIT讀入到物理內存中的另一個頁「萆遥現(xiàn)在dyld
可以依賴LINKEDIT顷帖。
???????現(xiàn)在LINKEDIT將會告訴dyld
需要對DATA做一些修復讓dylib
可以運行。因為同樣的事情發(fā)生了渤滞,dyld
將從DATA頁讀取一些數(shù)據(jù)贬墩,但是這里稍微有些不同。實際上dyld
正在執(zhí)行回寫操作妄呕,那就意味著它正在改變DATA頁陶舞,因此copy on write
(寫時復制)就發(fā)生了。那一頁就變成了dirty
頁绪励。如果我們分配了8頁的空間之后全部將他們讀入肿孵,在內存中我們將會得到8頁的dirty page
,但是現(xiàn)在我們只一個dirty page
,和兩個clean page
(解釋: 目前只讀入了三個頁,一個Mach header
优炬, 一個LINKEDIT, 一個DATA)颁井,內存中的對應的DATA頁變成了dirty page
)。
???????第二個線程載入相同的dylib
的時候同樣會以相同的步驟蠢护。首先查看Mach header
,但是這次內核說雅宾,我已經(jīng)在內存中存在了這個頁,所以它就只是簡單進行映射葵硕,頁沒有進行io操作眉抬。LINKEDIT也是同樣的,它進行的非常的快懈凹。
???????現(xiàn)在操作DATA頁蜀变,這時內核查看是否DATA頁對應的clean copy
是否存在于內存中的某個地方,如果存在復用它介评,如果不存在就重新讀取 库北。在這個進程中,它會將RAM變成dirty们陆。
???????現(xiàn)在進入最后一步寒瓦,dyld
只有在進行它自己的操作時才需要LINKEDIT,所以那就意味著告訴內核,一旦操作完成坪仇,LINKEDIT頁將不再需要杂腰。可以回收他們供其它使用內存的使用椅文。
???????因此現(xiàn)在的結果是兩個進程共享這個dylib
喂很,本來每一個線程占用8頁惜颇,一共16頁,但現(xiàn)在我們只有兩個dirty page
少辣,一個clean頁以及其它的共享的頁凌摄。
2.1、ASLR和code signing
???????接下來我們會討論下兩個不太重要的事情是如何影響dyld
的漓帅。
???????一個是ASLR(地址空間隨機分布)望伦,這是一個一二十年前的老技術,依靠它可以使加載的地址隨機化煎殷。
???????第二個是code signing屯伞。在Xcode中,很多人都必須處理code signing豪直,并且你認為的代碼簽名是劣摇,對整個文件運行一個加密散列,然后用簽名對其進行簽名弓乙。那意味著如何想驗證它必須讀入整個文件末融。相反在編譯期間真正發(fā)生的是Mach-O
文件中的每一個page都會有自己獨立的加密散列。這些散列存儲在LINKEDIT中暇韧,這允許對每個頁面進行驗證勾习,確保它沒有被篡改,并且在頁面上每次只有一個擁有者懈玻。
2.2巧婶、exec
什么是exec?exec是一個系統(tǒng)調用涂乌。
???????當陷入內核中艺栈,你可能想用一個新程序替換個這個進程。內核將清空整個地址空間來運行你指定的可執(zhí)行文件湾盒。ASLR隨機為它映射了一個空間湿右,接下來要做的是自底至頂將整個區(qū)域標記不為可訪問(意味著它是不可讀,不可寫罚勾,不可執(zhí)行的)毅人。對于32位系統(tǒng),這塊區(qū)域的大小至少4KB尖殃,對于64位系統(tǒng)丈莺,至少4GB。它捕獲所有的空指針引用異常分衫,并預測更多的位场刑,它捕獲任何指針截斷般此。
三蚪战、 dyld的加載順序
3.1牵现、Load dylibs
???????在最初的幾十年里,Unix的生活很輕松邀桑,因為所做的只是映射一個程序瞎疼,將PC(程序計數(shù)器)設置到其中,然后開始運行它壁畸。之后共享庫產(chǎn)生了贼急。那誰來加載這些dylib
呢?他們很快意識到事情變得非常復雜捏萍,內核開發(fā)人員不想讓內核來處理這件事太抓,所以這時幫助程序(helper program)就產(chǎn)生了。在Mach平臺它叫dyld
令杈,在其它的Unix平臺這叫LD.SO
走敌。因此,當內核映射完一個進程后逗噩,它現(xiàn)在將另一個名為dyld
的Mach-O映射到另一個隨機地址的進程中掉丽。設置PC到dyld
中,讓其完成啟動這個進程∫煅悖現(xiàn)在dyld
運行在進程中捶障,它的任務就是加載這個進程所依賴的dylib
,讓所有東西就緒并準備運行纲刀。
???????現(xiàn)在我們捋一下這些步驟项炼。這是一系列的步驟,它在底部有一個時間線示绊,當我們經(jīng)歷過這些步驟時芥挣,我們會走過時間線。
???????第一件事是dyld
映射所有的獨立的dylib
那什么是獨立的dylib
耻台?為了找到這些獨立的dylib
空免,dyld
首先讀取main executable
的頭部,并且這個頭部已經(jīng)被內核映射到了內存中盆耽。這個頭部是一個所有獨立庫的列表√Q猓現(xiàn)在開始解析頭部,它將找到每一個dylib
摄杂,發(fā)現(xiàn)一個dylib
坝咐,它將會打開并解析每個文件的起始,需要確認這個文件是一個Mach-O
類型析恢,驗證它找到它的code signing(代碼簽名),并向內核注冊發(fā)現(xiàn)的code signing墨坚。實際上dyld
會在當前的dylib
中的每個segment調用mmap。
???????你的App知道dyld
,dyld
會說你的app依賴A和B兩個dylib
映挂,把他們加載到內存中泽篮,任務就完成了盗尸。但是還可以更復雜些,因為A dylib
和 B dylib
他們自己可能會依賴其它的dylib
,因此帽撑,dyld
以解析main executable
相同的方式解析每一個dylib
泼各,每一個dylib
依賴的dylib
可能已經(jīng)被加載到內存中或者需要決定是否一些新的東西已經(jīng)被加載,如果沒有加載亏拉,那么需要加載它扣蜻。這個操作持續(xù)進行下去,直到所有的文件都被加載及塘。
???????現(xiàn)在看下進程莽使,在mach系統(tǒng)中平均每個進程需要加載100到400個dylib
,也就是說需要加載好多個dylib
笙僚,幸運的是它們中的大多數(shù)是OS dylib
,開發(fā)者在編譯操作系統(tǒng)的時候會pre-calculate
(預計算)和pre-cache
(預緩存)許多工作吮旅,這些工作是dyld
加載dylib
做的事件。因此OS dylib
加載非常非澄犊龋快庇勃。
3.2、Rebase 和bind
???????現(xiàn)在我們已經(jīng)加載了所有的dylib
槽驶,但是他們分布在各自獨立位置上责嚷, 我們現(xiàn)在要做的是把他們綁定到一塊,這步操作稱作fix-ups.
???????對于fix-ups我們所了解的是因為有code signing
的存在掂铐,所以我們不能改變指令(instruction)罕拂。那么,如果不能改變一個dylib
如何調用的指令全陨,那么它如何調用另一個dylib
呢爆班?答案是我們依然通過添加中間層來解決。在Mach 平臺這個code-gen(代碼產(chǎn)生器)叫做dynamic PIC(Position Independent Code)辱姨。它定位獨立的代碼柿菩,這意味著代碼可以加載到地址中,并且是動態(tài)的雨涛,意味著它是間接尋址的枢舶。
這意味著一個對象調用另一個對象時,co-gen實際上在DATA段中創(chuàng)建了一個指針替久,該指針指向所要調用的東西凉泄。代碼加載那個指針并跳到指向的地址。所以dyld
要做的事件就是修復指針和數(shù)據(jù)蚯根。
???????fix-ups主要包含兩個方面:rebasing和binding后众。rebasing
就是如果有一個指針,并且這個指針指向鏡像內的某個地方,可以通過它進行調整蒂誉。binding
就是指向鏡像外的某個地方教藻。它需要做些不同的操作,來看下下面的步驟拗盒。
???????但是首先,如果你好奇的話锥债,有一個命令陡蝇,上面有很多選項。 你可以在任何二進制文件上運行它哮肚,你將看到dyld
為準備該二進制文件所必須做的所有修復登夫。
[~]> xcrun dyldinfo -rebase -bind -lazy_bind myapp.app/myapp
rebase information:
segment section
__DATA __const
__DATA __const
__DATA __const
__DATA __const
...
address type
0x10000C1A0 pointer
0x10000C1C0 pointer
0x10000C1E0 pointer
0x10000C210 pointer
bind information:
segment section
__DATA __objc_classrefs 0x10000D1E8 pointer 0 CoreFoundation _OBJC_CLASS_$_NSObject
symbol
0x10000D4D0 pointer 0 CoreFoundation _OBJC_METACLASS_$_NSObject
0x10000D558 pointer 0 CoreFoundation _OBJC_METACLASS_$_NSObject
0x10000C018 pointer 0 libswiftCore __TMSS
address type add dylib
__DATA __data
__DATA __data
__DATA __got
...
lazy binding information:
segment section
__DATA __la_symbol_ptr 0x10000C0A8 0x0000 libSystem
__DATA __la_symbol_ptr 0x10000C0B0 0x0014 libSystem
__DATA __la_symbol_ptr 0x10000C0B8 0x002B libSystem
...
???????在以前你可能為每一個dylib
分配一個加載地址,這個加載地址是靜態(tài)鏈接器和dyld
在一塊工作的地方允趟,當加載dylib
到指定的地址恼策,所有的指針和數(shù)據(jù)都是內部的,它們都是正確的所以不需要修復潮剪。但是現(xiàn)在由于ASLR的存在涣楷,它滑動到了其它的地址,那就意味著指針和數(shù)據(jù)仍然指向舊的地址抗碰。為了修復這些指針和數(shù)據(jù)狮斗,我們需要計算偏移量。對于每一個內部的指針弧蝇,需要在原來的基礎上加上偏移量碳褒。所以rebasing
意味著遍歷所有的數(shù)據(jù)指針,在原來的基礎上加上偏移量看疗。概念非常簡單沙峻,讀、加两芳、寫摔寨。但是數(shù)據(jù)指針在哪?數(shù)據(jù)指針被編碼在了LINKEDIT段〔懒荆現(xiàn)在所有的東西都進行了映射祷肯,當開始rebasing
的時候我們實際上是在所有的DATA頁引起page fault
。當改變他們的時候出現(xiàn)copy on write
疗隶,所以rebasing
是昂貴的佑笋,因為它會對所有涉及到的io進行操作坯临。但是蘋果開發(fā)人員做了一部分技巧那就是順序的執(zhí)行操作蚀乔,從內核的角度看,所有的page fault
都是順序的發(fā)生粪滤。當明白這一點,內核將會為我們提前讀取以此減少代價蜀备。
???????接下來是binding
关摇,它其實是根據(jù)名字進行限制的。它們實際上是字符串碾阁,malloc分配的空間信息存儲在LINKEDIT输虱,那就是說數(shù)據(jù)指針需要指向malloc。所以在運行期間脂凶,dyld
要做的是在符號表里找到symbol的實現(xiàn)宪睹,這將花費大量的計算。一旦找到蚕钦,就把值存儲在數(shù)據(jù)指針內亭病。這一步的復雜的計算量是rebasing
是不能比的,但是做的io操作會很少嘶居,因為rebasing
階段已經(jīng)做了大部分罪帖。
3.3、ObjC
???????接下來ObjC包含大量DATA結構邮屁,其中類DATA結構是一個指針指向自己的方法整袁,另一個指向指向super gloss
等等。經(jīng)過rebasing
和binding
所有的東西都被修復佑吝。
但是ObjC在運行的時候需要有一些額外的東西葬项。
???????首先,ObjC是一門動態(tài)的語言迹蛤,你可以根據(jù)類名實現(xiàn)一個類民珍。那就意味著ObjC運行時必須包含所有名字的一張表,每個名字映射到對應的類盗飒。所以每次加載一些東西逆趣,它定義一個類宣渗,它的名字需要注冊到全局的表中痕囱。
???????在c++中你可能聽過fragile ivar
問題鞍恢,fragile base class
問題,但是在ObjC中這些問題都不存在窒典,因為在加載的時候瀑志,修復階段中其中一個階段就是動態(tài)地改變所有變量的偏移量劈猪。在ObjC中可以定義分類來改變另一個定義的方法的實現(xiàn)疾层。有時被定義分類的原始類不在自己的鏡像文件中而是在另一個dylib
中痛黎,這些方法需要在這個階段進行修復湖饱。
???????最后ObjC是基于唯一的selector的井厌,所以必須保證selector是唯一的仅仆。
3.4港柜、initializers
???????現(xiàn)在輪到我們動態(tài)修復DATA夏醉。在c++中畔柔,你可以有一個初始化器肠槽,在這里你可以添加任何的表達秸仙。任意的表達需要在這里運行寂纪,并且已經(jīng)運行了捞蛋,所以C++為這些任意的DATA初始化產(chǎn)生初始化器。在ObjC中也有一個類似的方法叫+load
“嵘瑁現(xiàn)在+load
被廢棄拿穴,不建議使用默色。建議用+initialize
方法.
???????到目前為止,上述的東西形式了一個龐大的圖表吃度,main executable
在頂層规肴,下面是所有依賴的dylib
拖刃。在這張圖表中,我們要必須運行初始化化器方法均函,但是初始化的順序是什么樣的呢洛勉?答案是自底向上收毫。原因是當initialize運行的時候它可能調用一些dylib
,你要確保依賴的dylib
已經(jīng)被加載输拇。所以通過自底向上一直到app類這個過程運行各自初始化器,你可以安全地調用你所依賴的東西奴曙。一旦所有的初始化器完成炉菲,我們最終可以調用主dyld
程序了拍霜。
四越驻、減少啟動時間
???????想要啟動速度多快?
???????啟動速度在不同的平臺是不同的并巍,但是刽射,一個很好的經(jīng)驗法則是400毫秒是一個很好的啟動時間。原因是當我們看到app從桌面到該應用的時候,這個過程會有一個過渡的動畫戒祠,給我們一種連續(xù)性的感覺。這些動畫需要時間馏颂,它給我們一個隱藏啟動時間的機會。很明顯它是不同的,在不同的環(huán)境中派昧,它有不同的啟動時間。phone 五慈、TV和watch是不同的平臺黔牵,但是400毫秒會是一個好的目標猾浦。不要將啟動時間超過20s对嚼,OS會將其殺死,這樣的話它就會進入一個無限的循環(huán)。
另外,在支持的最低設備上測試app啟動時間是非常重要的画舌。如果你現(xiàn)在在6s上測試的時間是400毫秒佑惠,那么它在iPhone 5上測試的時間可能超過400毫秒乍丈。
???????App啟動的時間都需要做什么? 我們需要解析鏡像察蹲,映射鏡像,rebase鏡像,bind鏡像,運行鏡像的初始化器洽洁,然后調用main。在那之后會調用UIApplicationMain,你可能在ObjC 應用中看到,但是在Swift 應用中唱星,它被隱式處理了抵拘。啟動過程還會做其它的事情飘言,包括運行framework的初始化器,加載nib等倒源,最后在application delegate中苛预, 我們會得到一個回調。在400毫秒的啟動時間里其實已經(jīng)將最后兩步的時候計算在內笋熬。
4.1热某、冷啟動VS熱啟動
???????當啟動app的時間,我們討論的是冷啟動和熱啟動。
???????熱啟動是app已經(jīng)在內存中昔馋,可能是因為它之前已經(jīng)啟動并退出了芜繁,但它仍然位于內核的discache中,也可能是因為剛剛對它進行了復制绒极。
???????冷啟動是應用程序沒有在discache中骏令,冷啟動的啟動時間測量是非常重要的,原因是當重啟手機之后或者在很長時間之后第一次啟動app,這時的啟動時間是我們真正需要的垄提。為了測量榔袋,你需要在兩次測量之間重啟app。話雖如此铡俐,但是如果你提高了熱啟動的啟動時間凰兑,那么相應的冷啟動的啟動時間也會有所降低∩笄穑可以針對熱啟動執(zhí)行快速開發(fā)吏够,但之后每隔一段時間,都要用冷啟動進行測試滩报。那么如何測量main之前的啟動時間锅知?可以通過在dyld
中的測量系統(tǒng)測量,就是設置下環(huán)境變量脓钾。dyld
可以打印數(shù)據(jù)售睹。它其實已經(jīng)在過渡的操作系統(tǒng)中已經(jīng)存在,但是它會打印一些可能沒有用的debug信息可训,并且可能遺漏一些你想要的信息昌妹。在新的操作系統(tǒng)中已經(jīng)進行了顯著的提高,它只打印可能對提高啟動時間有幫助的相關信息握截。
???????為了解析應用程序中的符號并加載斷點飞崖,調試器必須在每次加載dylib
時暫停啟動,而這可能會非常耗時谨胞。但dyld
知道這一點固歪,它從注冊的數(shù)字中減去了調試器超時。
所以你不必擔心畜眨,但是你注意到了昼牛,因為dyld
給你的數(shù)字比你看墻上的鐘看到的要小得多。
4.2康聂、設置環(huán)境變量
在xcode里面設置環(huán)境變量DYLD_PRINT_STATISTICS
,如下圖所示:
設置完之后胞四,你就會在控制臺得新的輸出日志恬汁。
Total pre-main time: 10.6 seconds (100.0%)
dylib loading time: 240.09 milliseconds (2.2%)
rebase/binding time: 351.29 milliseconds (3.3%)
ObjC setup time: 11.83 milliseconds (0.1%)
initializer time: 10 seconds (94.3%)
slowest intializers :
MyAwesomeApp : 10.0 seconds (94.2%)
???????前面提到,操作系統(tǒng)在編譯的時候已經(jīng)預計算了一些數(shù)據(jù)辜伟,但是不可能包含每個app中所有dylib
氓侧。當進程加載這些dylib
的時候我們就會經(jīng)歷一個非常慢的進程脊另。解決方案是盡量少的使用dylib
。具體執(zhí)行的方案有如下:
???????一是可以使用靜態(tài)歸檔文件约巷,并將它們鏈接到兩個應用程序中偎痛,以這種方式連接到應用程序中。
???????二是對dylib
進行懶加載其加載方式是通過dlopen独郎。但是dlopen會造成微妙的性能問題和正確性的問題踩麦,它可能導致以后會做許多工作。所以這個方法廢棄氓癌。
這里有一個包含26個dylib
的app,將他們全部加載需要花費240毫秒的時間谓谦,但是將這些庫合并成兩個dylib
,它只占用了20毫秒贪婉。合并到一塊反粥,仍然有這些功能,仍然可能共享他們疲迂,但是對這些dylib
進行限制是非常有作用的才顿。
這是開發(fā)便利和啟動時間之間的一種權衡。因為dylib
越多尤蒿,你可能更容易編譯和鏈接你的app娜膘,加快開發(fā)周期。
???????所以你絕對可以而且應該使用一些优质,但最好是把目標設定在有限的數(shù)量上竣贪,我想說,一個好的目標大約是6個巩螃。
4.3演怎、binding and rebasing
???????rebasing
因為io操作往往是很緩慢的,binding
往往是計算比較花費時間但是io操作幾乎做完避乏。因此它們的io操作是混合到一塊的爷耀,所以時間也是混合到一塊的∨钠ぃ可以發(fā)現(xiàn)fix-up修復的主要是DATA段的指針歹叮,所以我們要做的就是減少指針的數(shù)量。
dyld
info指令可以幫助我們查看DATA段的什么指針將被修復铆帽,它會指出dylib
中包含什么segment和section,從而會讓你對正在修復的問題有一個很好的了解咆耿。
例如:如果在ObjC部分中看到一個ObjC類的符號,那么很可能有許多ObjC類爹橱。
所以萨螺,你可以做的一件事就是減少ObjC類對象和ivar的數(shù)量。
有許多編碼風格鼓勵使用輕量級的類,它們可能只有一個或兩個函數(shù)慰技,但是隨著類的增多椭盏,這種特殊的模式會讓的你的應用的啟動逐漸減慢,對于你要注意這個情況吻商。
App包含100或10000個類掏颊,這不是個問題,但是我們可以看到隨著app的類從5艾帐,10乌叶,15 到20000增加的過程中,當內核將他們加載進內存時掩蛤,app的啟動時間增加了7或800毫秒枉昏。
???????另一件減少啟動時間的方法是可以減少c++ 虛函數(shù),他們比OjbC元數(shù)據(jù)小揍鸟,但是他們對某些應用程序是非常重要兄裂,替代的方法是使用Swift的結構體。Swift傾向于使用較少的包含需要修復的指針的數(shù)據(jù)阳藻。Swift具有更好的內聯(lián)特性晰奖,可以更好的避免這一點。所以遷移到Swift是提高減少啟動時間的一種選擇腥泥。
???????另外匾南,你要注意機器產(chǎn)生的代碼,當你采用DSL或自定義的語言描述一些數(shù)據(jù)結構蛔外,你可能有一些程序將這些描述生成代碼蛆楞,如果生成的代碼中包含大量的指針,他們變得非常昂貴的夹厌,因為他們產(chǎn)生了非常大的結構豹爹。但好處是,你通常擁有大量的控制權矛纹,因為你可以更改代碼生成器臂聋,使其使用非指針的內容,例如基于偏移量的結構或南。
這將是一個巨大的勝利孩等。
4.4、initializers
???????有兩種類型的初始化器一種是顯式初始化器比如+ load采够,它應該被 +initialize代替肄方,因為它會在類被證實存在的時候而不是文件被加載的時候執(zhí)行你的代碼。
或者吁恍,在C/C++中扒秸,有一個屬性可以放在函數(shù)上(__attribute__((constructor))
)播演,從而生成初始化器冀瓦,所以這是一個顯式的初始化器伴奥,不建議使用。
???????建議使用site initializers翼闽,其指的是像dispatch once
的初始化器拾徙。
在跨平臺代碼可以使用pthread once
, c++代碼可以使用std once
。
上述的這些函數(shù)基本上都有相似的功能感局,即block里面的代碼只會在第一次命中是執(zhí)行尼啡,僅有一次。dispatch once
在蘋果系統(tǒng)上做了很大的優(yōu)化询微。在第一次執(zhí)行之后崖瞭,如果再次執(zhí)行,block相應于一個空任務撑毛,什么也不做书聚。蘋果系統(tǒng)開發(fā)者強烈建議使用dispatch once
而不是顯式的初始化器。
???????隱式初始化器指的是有關c++全局變量的non-trivial
初始化器藻雌〈菩可以用site 初始化器代替,當然胯杭,有些地方可以放置具有non-trivial
的全局變量或指向要初始化的對象的指針驯杜。可以不使用non-trival初始化器做个,代替的是c++中的POD( a plain old data)
鸽心。如果是一個POD對象,靜態(tài)鏈接器會為DATA 段預計算所有的數(shù)據(jù)居暖,沒必要運行顽频,沒必要修復。
隱式初始化器很難被發(fā)現(xiàn)膝但,可以用-Wglobal-constructors
設置編譯器來產(chǎn)生相應的警告冲九。
另外一個選擇是可以用swift進行重寫。原因是swift有全局變量跟束,它們會被初始化莺奸。他們會在你使用之前被初始化。它底層的實現(xiàn)不是利用initializer而是利用的dispatch once冀宴,site initializers中的一種灭贷。所以利用swift會自動處理這些東西。
???????在初始化器中不要調用 dlopen 略贮,它會產(chǎn)生性能問題甚疟。app啟動之前仗岖,dyld
正在運行,我們能做的是關掉鎖览妖,因為現(xiàn)在處于單線程中轧拄。只要dlopen調用了,情況就會發(fā)生改變讽膏,初始化器如何運行的整體構造將會改變檩电。我們可能處在多線程中,不得不打開鎖府树,它將會產(chǎn)生很糟糕的性能問題俐末。你也可能遇到不易察覺的死鎖問題或者無法預期的行為。作為同樣的原因奄侠,不要在初始化器中開啟線程卓箫。
總結:
- 移除不需要用到的動態(tài)庫
- 移除不需要用到的類
- 合并功能類似的類和擴展
- 盡量避免在+load方法里執(zhí)行的操作,可以推遲到+initialize方法中垄潮。
- 使用swift烹卒。