整個(gè)文章由來是一次內(nèi)部的分享酵熙, 是分享關(guān)于程序啟動(dòng)的一些事澄阳。從創(chuàng)建進(jìn)程到內(nèi)存分配斗遏,以及這些過程中的時(shí)間花費(fèi)脆炎。整個(gè)分享分為三部分梅猿,分別為:
1.理論部分:預(yù)備知識(shí)以及從點(diǎn)擊icon到程序啟動(dòng)完成都經(jīng)過了哪些過程。
2.實(shí)踐部分:具體看看程序啟動(dòng)有哪些流程和啟動(dòng)時(shí)間花在哪了秒裕。
3.具體建議:根據(jù)啟動(dòng)流程及時(shí)間瓶頸談?wù)剝?yōu)化建議袱蚓,建議更偏向每一個(gè)人能做的,如果是優(yōu)化工程團(tuán)隊(duì)的具體實(shí)踐請(qǐng)參考美團(tuán)關(guān)于APP冷啟動(dòng)時(shí)間優(yōu)化的實(shí)踐簇爆。
下面開始分享癞松。
首先是簡(jiǎn)單編譯原理介紹:
我們的 .h、.m 入蛆、亦或是.c响蓉、.mm、.swift 都會(huì)從高級(jí)語言被預(yù)處理哨毁、編譯枫甲、匯編、鏈接之后成為二進(jìn)制可執(zhí)行文件扼褪。
然后講一些預(yù)備知識(shí)想幻,如下:
- executable :就是我們 APP 的可執(zhí)行二進(jìn)制文件;
- dylib:動(dòng)態(tài)庫话浇,一般指程序啟動(dòng)時(shí)動(dòng)態(tài)鏈接的動(dòng)態(tài)庫脏毯;
- Bundle:動(dòng)態(tài)庫的一種,但是需要使用 dlopen() 打開幔崖,一般用于運(yùn)行時(shí)動(dòng)態(tài)打開食店;
- Framework:包含相關(guān)的資源和頭文件的動(dòng)態(tài)庫,如下圖中微信的 SDK赏寇;
我們來看看實(shí)際中我們的程序包是怎樣的:
將 .ipa 改成 .zip 然后打開吉嫩,由于這是直接編譯的 release 包,可以看到里面有符號(hào)表嗅定,一些extension 的支持以及支持 Swift 的動(dòng)態(tài)庫自娩,再往里就是 .app 文件以及打開后的二進(jìn)制文件、一堆資源和簽名文件等渠退。
關(guān)于我們的二進(jìn)制文件忙迁,可以用 MachOView.app 打開看到格式化后的一些信息脐彩,如下圖:
可以看到里面有加載這個(gè)二進(jìn)制文件到內(nèi)存中的一些信息,例如CPU類型动漾,文件的類型等丁屎。
然后簡(jiǎn)單介紹二進(jìn)制文件被加載到內(nèi)存中后是怎樣的映射關(guān)系:
引用 linux 內(nèi)存圖,可以看到:
- _PAGEZERO 填充在初始區(qū)域 (4G用捕獲空指針引用)旱眯;
- _Text 映射到代碼段晨川,_DATA 映射到數(shù)據(jù)段等;
- 引導(dǎo)開辟了堆删豺,共享庫映射區(qū)共虑,棧,dyld呀页,內(nèi)核代碼區(qū)等妈拌;
以上僅作為參考和教學(xué)用,實(shí)際情況相差較大且更為復(fù)雜蓬蝶。比如上圖沒有畫出因?yàn)?ASLR 隨機(jī)地址映射引起的段偏移尘分,并且實(shí)際情況下每個(gè)線程都是有自己的線程棧的,但是圖中都是表示為一塊區(qū)域丸氛。
下面我們進(jìn)入正題培愁,關(guān)于啟動(dòng)流程和啟動(dòng)時(shí)間我們需要關(guān)心的步驟:
兩部分:
pre-main階段:
1.1. 加載應(yīng)用的可執(zhí)行文件;
1.2. 加載動(dòng)態(tài)鏈接庫加載器dyld(dynamic loader)缓窜;
1.3. dyld遞歸加載應(yīng)用所有依賴的dylib(dynamic library 動(dòng)態(tài)鏈接庫)定续;main()階段:
2.1. dyld調(diào)用main();
2.2. 調(diào)用UIApplicationMain()禾锤;
2.3. 調(diào)用applicationWillFinishLaunching私股;
2.4. 調(diào)用didFinishLaunchingWithOptions;
表示為圖即是:
然后詳細(xì)介紹每個(gè)步驟:
1.execve(const char *filename, const char *argv[], const char *envp[]) : 一個(gè)內(nèi)核級(jí)系統(tǒng)調(diào)用函數(shù)恩掷,根據(jù)參數(shù)可知倡鲸,傳入一個(gè)文件名、一些命令行參數(shù)黄娘、一些環(huán)境變量然后開始打開這個(gè)文件(包含 fork 進(jìn)程并找到文件等過程)旦签,以下是細(xì)化的兩個(gè)步驟:
①. parse header:解析可執(zhí)行文件的 header 讀取文件的加載信息;
②. mmap 和 copyOnWrite:將可執(zhí)行文件映射進(jìn)內(nèi)存以及寫時(shí)復(fù)制技術(shù)(自行腦補(bǔ)懶加載)寸宏;
2.load dylib: 加載動(dòng)態(tài)庫的過程,大多為系統(tǒng)的動(dòng)態(tài)庫偿曙,下面細(xì)化:
①. 依據(jù)Apple的動(dòng)態(tài)鏈接器 dylib 進(jìn)行遞歸加載動(dòng)態(tài)庫及其動(dòng)態(tài)庫依賴的動(dòng)態(tài)庫氮凝;
②. rebase (ASLR矾缓,page fault 和 COW) 交惯,由于 ASLR 隨機(jī)地址偏移的存在开呐,所以需要修正我們指針的引用序臂,這一步主要是讀取并修正我們二進(jìn)制文件中的指針引用,主要是IO操作 稿壁;
③. bind (lazy bind) 幽钢,由于動(dòng)態(tài)庫共享內(nèi)存的使用,我們需要鏈接動(dòng)態(tài)庫后才知道我們二進(jìn)制文件中指向動(dòng)態(tài)庫內(nèi)容的指針實(shí)際的地址值傅是,所以需要鏈接時(shí)修正這些值匪燕,大部分是 (lazy bind) 即第一次訪問這個(gè)指針才去尋找這個(gè)指針實(shí)際指向的地址值并綁定供后續(xù)使用,所以首次訪問會(huì)慢一些喧笔,這一步主要是 CPU 尋找和計(jì)算指針值帽驯;
3.objc runtime(objc setup):這一步主要是OC運(yùn)行時(shí)的一些初始化工程,以下細(xì)分:
①. register class (class name map to class):類注冊(cè)书闸,動(dòng)態(tài)語言運(yùn)行時(shí)的需要尼变,會(huì)將類注冊(cè)到全局注冊(cè)表中,這個(gè)注冊(cè)表是一個(gè)字典浆劲,key 是類名的字符串嫌术,類對(duì)象本身是值;
②. 讀取 protocol牌借、category :讀取協(xié)議以及分類度气,并將分類方法加入對(duì)應(yīng)類的方法列表并保證其唯一性;
4.Initializers:這一步主要是+ load 這個(gè)方法以及一些全局變量的初始化
①. + load
添加標(biāo)志就能在啟動(dòng)時(shí)在控制臺(tái)打印啟動(dòng)時(shí)間:
我們來看看APP的啟動(dòng)時(shí)間:
可以看到四個(gè)過程對(duì)應(yīng)本文之前說的四個(gè)過程走哺。
最后針對(duì)四個(gè)過程談?wù)勎覀兤綍r(shí)編程中有什么事可以做的更好的:
1.Dylib Loading:
①.減少或者合并動(dòng)態(tài)庫蚯嫌;
②.這里和我們相關(guān)無非是引入第三方時(shí)斟酌這個(gè)第三方導(dǎo)致引入動(dòng)態(tài)庫相關(guān)的問題,這里和平時(shí)開發(fā)相關(guān)性較小就跳過了丙躏;
2.Objc setup:
相關(guān)過程:
①. class registration择示;
②.Non-fragile ivars offsets updated;
③.category registration晒旅;
④.selector uniquing 栅盲。
我們能做的:
①. 不要濫用分類,繼承等特性废恋,合并小類;
②. 不宜過長的類和方法名
③. 重構(gòu)谈秫、改版的時(shí)候及時(shí)刪除不再使用但是還有引用的老類和方法等,不要害怕以后會(huì)用到就用注釋掉的方式鱼鼓,害怕丟失版本管理也可以找到拟烫;
④. 屬性可以標(biāo)記為 readonly 就不要標(biāo)記 readwrite 可以少生成方法;
3.rebase/binding:
這里主要就是修正指針的消耗迄本,例如類硕淑、分類和成員變量和方法,這里能做的在之前講 Objc setup 的時(shí)候已經(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
參考清理無用類的一些清理工具:
AppCode
FUI
4.Initializers:
這一步和我們相關(guān)主要是 + load 這個(gè)方法以及一些全局變量的初始化置媳。
①.不要在初始化時(shí)做耗時(shí)工作于樟;
②.盡量不要使用 + load 方法,我們工程中大量在 + load 中加邏輯拇囊,很多其實(shí)是沒有必要的迂曲;
③.使用 + initialize 或者別的地方并配合 dispatch_once 是更好的選擇;
④. 如果是為了執(zhí)行一次,在 + load 中是不能保證的寥袭,因?yàn)檫@個(gè)方法是可以被調(diào)用的路捧,比如 [People load] ;
⑤.即使寫在 + load 里配合 dispatch_once 也是更安全的選擇纠永;
⑥.使用 swift 鬓长, swift 實(shí)現(xiàn)靜態(tài)變量初始化時(shí)是使用類似 dispatch_once 這樣的技術(shù)在首次調(diào)用時(shí)初始化的;
最后(重點(diǎn)):
重點(diǎn)談?wù)?Swift 對(duì)我們的幫助:
- 值類型(value type) 例如 struct尝江、enum 是在棧上分配的涉波,我們知道棧上是運(yùn)行時(shí)分配,并且值類型是不需要像類一樣全局注冊(cè)的炭序,也沒有那么多指針啤覆。這里重點(diǎn)談?wù)剹5暮锰帲?br>
①. 效率更高,因?yàn)椴恍枰穸岩粯泳S護(hù)全局引用計(jì)數(shù)惭聂,計(jì)算計(jì)數(shù)完成還要去尋找空內(nèi)存窗声,分配完畢之后還需要釋放,釋放后還要合并小內(nèi)存等一系列需要加鎖保證同步的繁瑣工作辜纲;每個(gè)線程有自己的線程棧笨觅,所以是不需要同步的,也沒有那么多維護(hù)加鎖引用計(jì)數(shù)的開銷耕腾,所以棧上分配是你更好的選擇见剩;
②. 面向協(xié)議變成是完全可以替代面向?qū)ο螅悾┚幊痰模⑶夷氵€有 Swift 強(qiáng)大的泛型系統(tǒng)支持扫俺;
③. OC 的 Category 開銷是很大的苍苞,因?yàn)槭擎溄与A段進(jìn)行的并且會(huì)引入大量的指針,而 Swift 的 extension 就沒有這方面問題狼纬,因?yàn)樗詈髸?huì)和它相關(guān)的 class 或 struct羹呵、enum 一起編譯。
④. 更強(qiáng)大的 feature 讓你寫出更少疗琉,更優(yōu)雅的代碼冈欢;
⑤. 強(qiáng)大的泛型系統(tǒng),和泛型特化等優(yōu)化會(huì)讓你的很多代碼可以被內(nèi)聯(lián)優(yōu)化盈简,這樣可以減少大量指針凑耻;
⑥. 你可以使用 final犯戏、private 等修飾符讓IDE去保證而不是依靠文檔,并且這些優(yōu)化關(guān)鍵字配合 Swift whole module 優(yōu)化能力可以讓你的代碼將方法動(dòng)態(tài)派發(fā)轉(zhuǎn)化為靜態(tài)派發(fā)拳话,不僅提升了效率還減少的指針。
⑦. Swift 中值類型以及 class 可以沒有父類的特性讓你可以避免 OC 龐大而復(fù)雜的繼承體系种吸,這樣也會(huì)讓APP跑的更快弃衍,內(nèi)存使用更小。
⑧. 命名空間和更嚴(yán)格的作用域也讓編程更加愉快坚俗。
希望這些建議能對(duì)編寫代碼有更高追求的你以幫助镜盯,以上。
引用:
WWDC 2016:Optimizing App Startup Time
WWDC 2015: Optimizing Swift Performance
WWDC 2016: Understanding Swift Performance