背景知識(shí):
- mach-o文件為基于Mach核心的操作系統(tǒng)的可執(zhí)行文件席里、目標(biāo)代碼或動(dòng)態(tài)庫(kù),是.out的代替拢驾,其提供了更強(qiáng)的擴(kuò)展性并提升了符號(hào)表中信息的訪問(wèn)速度奖磁,
- 符號(hào)表,用于標(biāo)記源代碼中包括標(biāo)識(shí)符独旷、聲明信息、行號(hào)寥裂、函數(shù)名稱等元素的具體信息嵌洼,比如說(shuō)數(shù)據(jù)類(lèi)型、作用域以及內(nèi)存地址封恰,iOS符號(hào)表在dSYM文件中
- 程序構(gòu)建過(guò)程:編譯分三步走麻养,對(duì)
源文件進(jìn)行預(yù)處理(processing),處理預(yù)編譯指令诺舔,生成.i文件鳖昌,下一步進(jìn)行編譯备畦,進(jìn)行詞法分析(lex工具識(shí)別詞法規(guī)則語(yǔ)義表)、語(yǔ)法分析和語(yǔ)義分析生成.s匯編文件许昨,最后進(jìn)行匯編懂盐,生成二進(jìn)制目標(biāo)文件.o。目標(biāo)文件再進(jìn)行鏈接器鏈接糕档,形成可執(zhí)行文件.a或mach-o文件莉恼。 - 鏈接分為動(dòng)態(tài)鏈接和靜態(tài)鏈接,靜態(tài)鏈接會(huì)將所有目標(biāo)文件.o全部?jī)?nèi)容鏈接到執(zhí)行文件中速那,如果另外的執(zhí)行文件需要其中的功能俐银,也必須全部收錄。動(dòng)態(tài)鏈接為了解決這樣的空間浪費(fèi)問(wèn)題端仰,只將函數(shù)信息鏈接加入執(zhí)行文件
- dyld是加載動(dòng)態(tài)鏈接庫(kù)的庫(kù)捶惜,該庫(kù)在加載可執(zhí)行文件的時(shí)候,遞歸加載所需要的所有動(dòng)態(tài)庫(kù)荔烧。動(dòng)態(tài)庫(kù)包括iOS操作系統(tǒng)的系統(tǒng)framework吱七,oc的runtime系統(tǒng)libobjc,系統(tǒng)級(jí)別的庫(kù)libSystem茴晋,例如libdispatch(GCD)陪捷、libsystem_block(Block)
App啟動(dòng)大致流程
對(duì)于一個(gè)可執(zhí)行文件來(lái)說(shuō),它的加載過(guò)程是:
分為兩大部分:
- pre-main 指的是操作系統(tǒng)開(kāi)始執(zhí)行一個(gè)可執(zhí)行文件诺擅,并完成進(jìn)程創(chuàng)建市袖、執(zhí)行文件加載、動(dòng)態(tài)鏈接烁涌、環(huán)境配置
- main 指的是從加載main函數(shù)入口以后苍碟,到app delegate完成加載回調(diào)的過(guò)程
操作系統(tǒng)加載App可執(zhí)行文件
操作系統(tǒng)加載可執(zhí)行文件,通過(guò)fork(創(chuàng)建一個(gè)進(jìn)程)指令在新的空間內(nèi)來(lái)執(zhí)行可執(zhí)行文件撮执,加載依賴的可執(zhí)行文件(mach-o)文件微峰,定位其內(nèi)部與外部指針引用,例如字符串與函數(shù)抒钱,執(zhí)行聲明為attribute((constructor))的C函數(shù)蜓肆,加載擴(kuò)展(Category)中的方法,C++靜態(tài)對(duì)象加載谋币,調(diào)用ObjC的+load函數(shù)
基本流程:
App 開(kāi)始啟動(dòng)后仗扬,系統(tǒng)首先加載可執(zhí)行文件(自身 App 的所有 .o 文件的集合),然后加載動(dòng)態(tài)鏈接器 dyld蕾额,dyld 是一個(gè)專(zhuān)門(mén)用來(lái)加載動(dòng)態(tài)鏈接庫(kù)的庫(kù)早芭。 執(zhí)行從 dyld 開(kāi)始,dyld 從可執(zhí)行文件的依賴開(kāi)始诅蝶,遞歸加載所有的依賴動(dòng)態(tài)鏈接庫(kù)退个。
動(dòng)態(tài)鏈接庫(kù)包括:iOS 中用到的所有系統(tǒng) framework募壕,加載 OC runtime 方法的 libobjc,系統(tǒng)級(jí)別的 libSystem语盈,例如 libdispatch(GCD) 和 libsystem_blocks (Block)舱馅。
dyld加載動(dòng)態(tài)庫(kù)
動(dòng)態(tài)鏈接庫(kù)的加載過(guò)程主要由dyld來(lái)完成,dyld是蘋(píng)果的動(dòng)態(tài)鏈接器黎烈。
- 系統(tǒng)先讀取App的可執(zhí)行文件(Mach-O文件)里的mach-o headers
- dyld去初始化運(yùn)行環(huán)境习柠,從里面獲得動(dòng)態(tài)依賴,開(kāi)啟緩存策略照棋,加載程序相關(guān)依賴庫(kù)(其中也包含我們的可執(zhí)行文件)资溃,并對(duì)這些庫(kù)進(jìn)行鏈接,最后調(diào)用每個(gè)依賴庫(kù)的初始化方法烈炭,在這一步溶锭,runtime被初始化。當(dāng)所有依賴庫(kù)的初始化后,輪到最后一位(程序可執(zhí)行文件)進(jìn)行初始化。
- 檢查和確認(rèn)符號(hào)表的是否存在和正確
- Map所有mach-o文件兵钮,用來(lái)整體統(tǒng)計(jì)變量聲明、函數(shù)調(diào)用等信息
- 進(jìn)行bind操作拱绑,對(duì)從其他庫(kù)的引用的符號(hào)、函數(shù)等丽蝎,進(jìn)行其內(nèi)存地址進(jìn)行修正綁定
- 進(jìn)行rebase操作猎拨,對(duì)自身庫(kù)內(nèi)部的引用進(jìn)行修正
- 進(jìn)行runtime系統(tǒng)初始化,會(huì)對(duì)項(xiàng)目中所有類(lèi)進(jìn)行類(lèi)結(jié)構(gòu)初始化屠阻,然后調(diào)用所有的load方法红省。
- 最后dyld返回main函數(shù)地址,main函數(shù)被調(diào)用国觉,我們便來(lái)到了熟悉的程序入口吧恃。
當(dāng)加載一個(gè) Mach-O 文件 (一個(gè)可執(zhí)行文件或者一個(gè)庫(kù)) 時(shí),動(dòng)態(tài)鏈接器首先會(huì)檢查共享緩存看看是否存在其中麻诀,如果存在痕寓,那么就直接從共享緩存中拿出來(lái)使用。每一個(gè)進(jìn)程都把這個(gè)共享緩存映射到了自己的地址空間中蝇闭。這個(gè)方法大大優(yōu)化了 OS X 和 iOS 上程序的啟動(dòng)時(shí)間呻率。
Mach-O 鏡像文件
Mach-O是OS X中二進(jìn)制文件的本機(jī)可執(zhí)行格式,是傳送代碼的首選格式丁眼】攴铮可執(zhí)行格式確定二進(jìn)制文件中的代碼和數(shù)據(jù)被讀入內(nèi)存的順序昭殉。代碼和數(shù)據(jù)的排序會(huì)影響內(nèi)存使用和分頁(yè)活動(dòng)苞七,從而直接影響程序的性能藐守。段的大小通過(guò)其包含的所有段中的字節(jié)數(shù)來(lái)度量,并向上舍入到下一個(gè)虛擬內(nèi)存頁(yè)邊界蹂风。
Mach-O二進(jìn)制文件被組織成segements卢厂。每個(gè)segement包含一個(gè)或多個(gè)部分。每個(gè)部分都有不同類(lèi)型的代碼或數(shù)據(jù)惠啄。segement始終從頁(yè)面邊界開(kāi)始慎恒,但section不一定是頁(yè)面對(duì)齊的。因此撵渡,segement終是4096字節(jié)或4千字節(jié)的倍數(shù)融柬,其中4096字節(jié)是最小大小。
Mach-O可執(zhí)行文件的segement和section根據(jù)其預(yù)期用途命名趋距。segement名稱的約定是使用以雙下劃線開(kāi)頭的全大寫(xiě)字母(例如粒氧,TEXT); section名稱的約定是使用以雙下劃線開(kāi)頭的全小寫(xiě)字母(例如, text)节腐。
Mach-O可執(zhí)行文件中有幾個(gè)可能的segements外盯,但只有兩個(gè)與性能有關(guān):__TEXT段和__DATA段。
The __TEXT Segment: Read Only
__TEXT segment是包含可執(zhí)行代碼和常量數(shù)據(jù)的只讀區(qū)域翼雀。按照慣例饱苟,編譯器工具創(chuàng)建具有至少一個(gè)只讀__TEXT segment的每個(gè)可執(zhí)行文件。由于該段是只讀的狼渊,因此內(nèi)核可以將__TEXT segment直接從可執(zhí)行文件映射到內(nèi)存中一次箱熬。當(dāng)segment被映射到內(nèi)存時(shí),它可以在所有進(jìn)程之間共享其內(nèi)容囤锉。 (這主要是框架和其他共享庫(kù)的情況坦弟。)只讀屬性還意味著構(gòu)成__TEXT segment的頁(yè)面永遠(yuǎn)不必保存到后備存儲(chǔ)。如果內(nèi)核需要釋放物理內(nèi)存官地,它可以丟棄一個(gè)或多個(gè)__TEXT頁(yè)面酿傍,并在需要時(shí)從磁盤(pán)重新讀取它們。
__TEXT segment的主要部分,sections分布
- __text 已編譯的可執(zhí)行文件的機(jī)器代碼
- __const 一般的常量數(shù)據(jù)
- __cstring 文字字符串常量(源代碼中的引用字符串)
- __picsymbol_stub 動(dòng)態(tài)鏈接器(dyld)使用的與位置無(wú)關(guān)的代碼存根例程
The __DATA Segment: Read/Write
__DATA segment 包含可執(zhí)行文件的非常量變量驱入。該 segement 是可讀寫(xiě)的赤炒,因?yàn)樗强蓪?xiě)的,所以對(duì)于與庫(kù)鏈接的每個(gè)進(jìn)程亏较,邏輯上復(fù)制靜態(tài)庫(kù)或其他動(dòng)態(tài)共享庫(kù)的__DATA段莺褒。當(dāng)內(nèi)存頁(yè)面可讀寫(xiě)時(shí),內(nèi)核會(huì)使其變?yōu)閏opy-on-write雪情。此技術(shù)可以做到遵岩,動(dòng)態(tài)庫(kù)是在內(nèi)存中共享的,可以被其他各個(gè)進(jìn)程訪問(wèn),但因?yàn)開(kāi)_DATA Segment是可讀可寫(xiě)的尘执,就會(huì)通過(guò)某一進(jìn)程對(duì)共享的_DATA Segment有寫(xiě)操作的時(shí)候舍哄,再進(jìn)行單獨(dú)的_DATA內(nèi)存空間復(fù)制。
__DATA segment 有許多部分誊锭,其中一些僅由動(dòng)態(tài)鏈接器使用表悬。下面 列出了可以出現(xiàn)在__DATA segment 中的一些更重要的部分。有關(guān)段的完整列表丧靡,請(qǐng)參閱Mach-O運(yùn)行時(shí)體系結(jié)構(gòu)蟆沫。
- __data 初始化的全局變量(例如int a = 1;或static int a = 1;)。
- __const 需要重定位的常量數(shù)據(jù)(例如温治,char * const p =“foo”;)
- __bss 未初始化的靜態(tài)變量(例如饭庞,static int a;)。
- __common 未初始化的外部全局變量(例如熬荆,int a;外部功能塊)但绕。
- __dyld 占位符部分,由動(dòng)態(tài)鏈接器使用惶看。
- __la_symbol_ptr lazy符號(hào)指針捏顺。可執(zhí)行文件調(diào)用的每個(gè)未定義函數(shù)的符號(hào)指針纬黎。
- __nl_symbol_ptr 非lazy符號(hào)指針幅骄。可執(zhí)行文件引用的每個(gè)未定義數(shù)據(jù)符號(hào)的符號(hào)指針本今。
Mach-O 性能影響
Mach-O可執(zhí)行文件的__TEXT segment和__DATA segment的組成與性能有直接關(guān)系拆座。優(yōu)化這些sections的技術(shù)和目的是不同的。但是冠息,它們的共同目標(biāo)是:提高內(nèi)存使用效率挪凑。
最典型的Mach-O的文件由可執(zhí)行代碼組成,在__TEXT逛艰,__text當(dāng)中躏碳。如__TEXT segment,該__TEXT是只讀的散怖,并直接映射到可執(zhí)行文件菇绵,所以如果內(nèi)核需要回收某些__text頁(yè)面占用的物理內(nèi)存,就不必將頁(yè)面保存到back store再將其分頁(yè)镇眷。它只需要釋放內(nèi)存咬最,并在后面代碼引用的時(shí)候從磁盤(pán)重新讀回。雖然這比交換內(nèi)存分頁(yè)的成本低欠动,因?yàn)檫@只是一個(gè)磁盤(pán)訪問(wèn)永乌,而不是兩個(gè)內(nèi)存分頁(yè)的交換 , 但這仍然很損耗性能,特別是如果必須從磁盤(pán)重新創(chuàng)建許多頁(yè)面翅雏。
對(duì)于這種情況的改進(jìn)硝桩,是通過(guò)程序重新排序來(lái)改進(jìn)代碼的引用位置,如改進(jìn)參考位置中所述枚荣。該技術(shù)將方法和功能組合在一起,具體取決于它們的執(zhí)行順序啼肩,調(diào)用頻率以及它們相互調(diào)用的頻率橄妆。如果__text部分組中的頁(yè)面以這種方式邏輯上起作用,則它們不太可能被多次釋放和讀回祈坠。例如害碾,如果將所有啟動(dòng)時(shí)初始化函數(shù)放在一個(gè)或兩個(gè)頁(yè)面上,則在發(fā)生初始化后不必重新創(chuàng)建頁(yè)面赦拘。
與__TEXT段不同慌随,__DATA可以寫(xiě)入段,因此段中的頁(yè)面__DATA不可共享躺同「蟛拢框架中的非常量全局變量可能會(huì)對(duì)性能產(chǎn)生影響,因?yàn)榕c框架鏈接的每個(gè)進(jìn)程都會(huì)獲得這些變量的副本蹋艺。解決這個(gè)問(wèn)題的主要解決辦法是盡可能多的非恒定的全局變量盡可能轉(zhuǎn)移到__TEXT剃袍,__const通過(guò)宣布他們部分const。減少共享內(nèi)存頁(yè)面描述了此技術(shù)和相關(guān)技術(shù)捎谨。這通常不是應(yīng)用程序的問(wèn)題民效,因?yàn)閼?yīng)用程序中的__DATA部分不與其他應(yīng)用程序共享。
編譯器將不同類(lèi)型的非常量全局?jǐn)?shù)據(jù)存儲(chǔ)在段的不同部分中__DATA涛救。這些類(lèi)型的數(shù)據(jù)是未初始化的靜態(tài)數(shù)據(jù)和符號(hào)與未聲明的“暫定定義”的ANSI C概念一致extern畏邢。未初始化的靜態(tài)數(shù)據(jù)位于__bss段的__DATA部分中。暫定的符號(hào)在__common 該__DATA部分检吆。
該 ANSI C和 C ++標(biāo)準(zhǔn)指定系統(tǒng)必須將未初始化的靜態(tài)變量設(shè)置為零舒萎。(未初始化的其他類(lèi)型的未初始化數(shù)據(jù)。)由于未初始化的靜態(tài)變量和臨時(shí)定義符號(hào)存儲(chǔ)在單獨(dú)的部分中蹭沛,因此系統(tǒng)需要以不同方式對(duì)待它們逆甜。但是當(dāng)變量位于不同的部分時(shí),它們更有可能最終出現(xiàn)在不同的內(nèi)存頁(yè)面上致板,因此可以單獨(dú)進(jìn)行交換交煞,從而使代碼運(yùn)行速度變慢。如減少共享內(nèi)存頁(yè)面中所述斟或,這些問(wèn)題的解決方案是在段的一個(gè)部分中合并非常量全局?jǐn)?shù)據(jù)__DATA素征。
ObjC Runtime
dyld的加載過(guò)程會(huì)初始化Runtime系統(tǒng),在此階段,有相當(dāng)多的優(yōu)化工作可以做
這過(guò)程包括:
- 所有類(lèi)型的定義和注冊(cè)御毅,Objective-C的類(lèi)不是編譯器決定的根欧,是運(yùn)行時(shí)動(dòng)態(tài)載入到全局表中的
- 非脆弱的ivars變量抵消更新,修改實(shí)例變量的內(nèi)存地址偏移問(wèn)題
- 分類(lèi)替換并添加到方法列表中端蛆,將分類(lèi)中的方法加載到方法列表中
- 確認(rèn)選擇器全局唯一
Initializers 階段
在Runtime系統(tǒng)加載以后凤粗,開(kāi)始進(jìn)行初始化
- Objc的+load()函數(shù)
- C++的構(gòu)造函數(shù)屬性函數(shù) 形如attribute((constructor)) void DoSomeInitializationWork()
- 非基本類(lèi)型的C++靜態(tài)全局變量的創(chuàng)建(通常是類(lèi)或結(jié)構(gòu)體)(non-trivial initializer) 比如一個(gè)全局靜態(tài)結(jié)構(gòu)體的構(gòu)建,如果在構(gòu)造函數(shù)中有繁重的工作今豆,那么會(huì)拖慢啟動(dòng)速度
pre-main階段分析
從上面可以得出以下幾個(gè)結(jié)論嫌拣,影響該階段啟動(dòng)時(shí)間的因素如下:
- Mach-O可執(zhí)行文件的加載和內(nèi)存重新分配規(guī)劃,對(duì)于其segment和section進(jìn)行虛擬內(nèi)存的分頁(yè)管理的調(diào)度
- dyld動(dòng)態(tài)鏈接內(nèi)存中的公共鏡像呆躲,在運(yùn)行時(shí)進(jìn)行檢查共享數(shù)據(jù)和鏈接調(diào)用
- Runtime的初始化异逐,包括class注冊(cè)、category加載插掂、變量對(duì)齊等
- C++靜態(tài)對(duì)象和全局變量的加載
- ObjeC所有l(wèi)oad函數(shù)的調(diào)用加載
優(yōu)化措施:
- 減少ObjC的類(lèi)膨脹問(wèn)題灰瞻,清理沒(méi)有使用的類(lèi),合并松散無(wú)用的類(lèi)
- 減少靜態(tài)變量的聲明和初始化的分離
static int x;
static short conv_table [128];
//更換為
static int x = 0;
static short conv_table [128] = {0};
減少靜態(tài)變量的使用
- 減少符號(hào)表的導(dǎo)出
通過(guò)設(shè)置-exported_symbols_list或-unexported_symbols_lis來(lái)限制符號(hào)表的導(dǎo)出辅甥,從而減少dyld的工作量 - 去除沒(méi)有使用的動(dòng)態(tài)庫(kù)依賴酝润,明確所依賴的frameworks是require還是optional,optional會(huì)動(dòng)態(tài)進(jìn)行額外檢查
- 刪除沒(méi)有用的方法
- 減少+load函數(shù)的實(shí)現(xiàn)璃弄,并減少在其中操作的邏輯
- 對(duì)某些經(jīng)常調(diào)用的代碼進(jìn)行二進(jìn)制化袍祖,生成靜態(tài)庫(kù),多使用靜態(tài)庫(kù)代替動(dòng)態(tài)庫(kù)谢揪,將多個(gè)靜態(tài)庫(kù)框架蕉陋,集中制作成靜態(tài)framework,從而能夠減少dyld的鏈接工作
關(guān)于冷啟動(dòng)和熱啟動(dòng)的不同如下:
main階段
從上圖可以得到拨扶,影響main階段的啟動(dòng)時(shí)間因素是:
- AppDelegate代理的加載生命周期回調(diào)
- Application Window的布局凳鬓、繪制和加載
- RootViewController的加載
優(yōu)化點(diǎn): - 壓縮和減小啟動(dòng)圖片
- 盡量不使用storyboard或者是nib來(lái)布局rootViewController
- 在didFinishLaunchingWithOptions階段,盡可能減少阻塞代碼的執(zhí)行患民,可以利用多線程進(jìn)行加載邏輯的處理缩举,注意多線程對(duì)主線程同步阻塞可能造成的黑屏問(wèn)題
- 將非同步需求的初始化邏輯進(jìn)行異步加載