iOS啟動時(shí)間優(yōu)化方案記錄

image.png

1. APP啟動時(shí)間

t(App總啟動時(shí)間) = t1(main()之前的加載時(shí)間) + t2(main()之后的加載時(shí)間)

t1 = 系統(tǒng)dylib(動態(tài)鏈接庫)和自身App可執(zhí)行文件的加載塞祈; t2 = main方法執(zhí)行之后到AppDelegate類中的- (BOOL)Application:(UIApplication *)Application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法執(zhí)行結(jié)束前這段時(shí)間,主要是構(gòu)建第一個界面刽射,并完成渲染展示。

2. 針對于T2階段的優(yōu)化

成本最少剃执,效果明顯。

2.1 didFinishLaunchingWithOptions

一般在這個方法里進(jìn)行初始化操作懈息,并且有些是必須執(zhí)行的肾档,可以適當(dāng)?shù)母鶕?jù)功能的不同適當(dāng)延遲其啟動的時(shí)機(jī):

  • 1.日志、統(tǒng)計(jì)等必須在 APP 一起動就最先配置的事件

  • 2.項(xiàng)目配置辫继、環(huán)境配置怒见、用戶信息的初始化 、推送姑宽、IM等事件

  • 3.其他 SDK 和配置事件

  • 4.可以按需加載的配置遣耍,比如分享

優(yōu)化方案:
第一類:可以仍然放在didFinishLaunchingWithOptions方法里面,
第二類:這個功能要在用戶進(jìn)入APP主體前要加載完炮车,比如放在廣告顯示的時(shí)候
第三類:延遲執(zhí)行部分業(yè)務(wù)邏輯和 UI 配置舵变,可以放在第一個頁面渲染完成之后酣溃,避免首屏加載時(shí)大量的本地/網(wǎng)絡(luò)數(shù)據(jù)讀取
第四類:在使用的時(shí)候可以再去加載

3. 針對于T1階段的優(yōu)化

3.1 測量時(shí)間

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

Total pre-main time: 1.1 seconds (100.0%)
        dylib loading time: 458.55 milliseconds (38.8%)
      rebase/binding time: 145.48 milliseconds (12.3%)
          ObjC setup time: 28.99 milliseconds (2.4%)
          initializer time: 548.53 milliseconds (46.4%)
          slowest intializers :
            libSystem.B.dylib :   5.85 milliseconds (0.4%)
        libglInterpose.dylib : 376.73 milliseconds (31.8%)
                AFNetworking : 54.63 milliseconds (4.6%)
                    WKWebKit : 48.15 milliseconds (4.0%)

如果想查看更詳細(xì)的信息碘饼,就設(shè)置DYLD_PRINT_STATISTICS_DETAILS為1:

total time: 2.4 seconds (100.0%)
  total images loaded:  459 (424 from dyld shared cache)
  total segments mapped: 114, into 10022 pages
  total images loading time: 1.7 seconds (71.8%)
  total load time in ObjC:  12.78 milliseconds (0.5%)
  total debugger pause time: 1.6 seconds (69.3%)
  total dtrace DOF registration time:   0.00 milliseconds (0.0%)
  total rebase fixups:  149,991
  total rebase fixups time:  12.35 milliseconds (0.5%)
  total binding fixups: 72,264
  total binding fixups time:  38.72 milliseconds (1.6%)
  total weak binding fixups time:  36.61 milliseconds (1.5%)
  total redo shared cached bindings time:  27.73 milliseconds (1.1%)
  total bindings lazily fixed up: 0 of 0
  total time in initializers and ObjC +load: 574.31 milliseconds (23.9%)
                         libSystem.B.dylib :   7.44 milliseconds (0.3%)
               libBacktraceRecording.dylib :   6.06 milliseconds (0.2%)
                libMainThreadChecker.dylib :  16.50 milliseconds (0.6%)
                      libglInterpose.dylib : 398.79 milliseconds (16.6%)
                       libMTLCapture.dylib :  14.42 milliseconds (0.6%)
                              AFNetworking :  55.91 milliseconds (2.3%)
                                ZWWKWebKit :  52.46 milliseconds (2.1%)
                           Demo :  18.29 milliseconds (0.7%)
total symbol trie searches:    283620
total symbol table binary searches:    0
total images defining weak symbols:  51
total images using weak symbols:  119
3.2 理論理解
3.2.1 Mach-O文件

Mach-O(Mach Object File Format)是一種用于記錄可執(zhí)行文件、對象代碼悲伶、共享庫艾恼、動態(tài)加載代碼和內(nèi)存轉(zhuǎn)儲的文件格式。App 編譯生成的二進(jìn)制可執(zhí)行文件就是 Mach-O 格式的麸锉,iOS 工程所有的類編譯后會生成對應(yīng)的目標(biāo)文件 .o 文件钠绍,而這個可執(zhí)行文件就是這些 .o 文件的集合。

Mach-O 文件主要由三部分組成:

  • Mach header:描述 Mach-O 的 CPU 架構(gòu)淮椰、文件類型以及加載命令等五慈;
  • Load commands:描述了文件中數(shù)據(jù)的具體組織結(jié)構(gòu),不同的數(shù)據(jù)類型使用不同的加載命令主穗;
  • Data:Data 中的每個段(segment)的數(shù)據(jù)都保存在這里泻拦,每個段都有一個或多個 Section,它們存放了具體的數(shù)據(jù)與代碼忽媒,主要包含這三種類型:
    1. __TEXT 包含 Mach header争拐,被執(zhí)行的代碼和只讀常量(如C 字符串)。只讀可執(zhí)行(r-x)晦雨。
    2. __DATA 包含全局變量架曹,靜態(tài)變量等∧智疲可讀寫(rw-)绑雄。
    3. __LINKEDIT 包含了加載程序的元數(shù)據(jù),比如函數(shù)的名稱和地址奥邮。只讀(r–-)万牺。
3.2.2 dylib

dylib也是一種 Mach-O 格式的文件,后綴名為 .dylib 的文件就是動態(tài)庫(也叫動態(tài)鏈接庫)洽腺。動態(tài)庫是運(yùn)行時(shí)加載的脚粟,可以被多個 App 的進(jìn)程共用。
如果想知道 TestDemo 中依賴的所有動態(tài)庫蘸朋,可以通過下面的指令實(shí)現(xiàn):

otool -L /TestDemo.app/TestDemo
3.2.3 dyld

動態(tài)鏈接器核无,其本質(zhì)也是 Mach-O 文件,一個專門用來加載 dylib 文件的庫藕坯。
dyld 位于 /usr/lib/dyld团南,可以在 mac 和越獄機(jī)中找到噪沙。dyld 會將 App 依賴的動態(tài)庫和 App 文件加載到
內(nèi)存后執(zhí)行。

3.2.4 dyld shared cache

是動態(tài)庫共享緩存已慢,當(dāng)需要加載的動態(tài)庫非常多時(shí)曲聂,相互依賴的符號也更多了,為了節(jié)省解析處理符號的時(shí)間佑惠,OS X 和 iOS 上的動態(tài)鏈接器使用了共享緩存朋腋。OS X 的共享緩存位于 /private/var/db/dyld/,iOS 的則在 /System/Library/Caches/com.apple.dyld/膜楷。

當(dāng)加載一個 Mach-O 文件時(shí)旭咽,dyld 首先會檢查是否存在于共享緩存,存在就直接取出使用赌厅。每一個進(jìn)程都會把這個共享緩存映射到了自己的地址空間中穷绵。這種方法大大優(yōu)化了 OS X 和 iOS 上程序的啟動時(shí)間。

3.2.5 images

images 在這里不是指圖片特愿,而是鏡像仲墨。每個 App 都是以 images 為單位進(jìn)行加載的。images 類型包括:

  1. executable:應(yīng)用的二進(jìn)制可執(zhí)行文件揍障;
  2. dylib:動態(tài)鏈接庫目养;
  3. bundle:資源文件,屬于不能被鏈接的 dylib毒嫡,只能在運(yùn)行時(shí)通過 dlopen() 加載癌蚁。
3.2.6 imageLoader

image表示一個二進(jìn)制文件,里面是被編譯過的符號兜畸、代碼等努释,所以ImageLoader作用是將這些文件加載進(jìn)內(nèi)存,且每一個文件對應(yīng)一個ImageLoader實(shí)例來負(fù)責(zé)加載咬摇。
兩步走: 在程序運(yùn)行時(shí)它先將動態(tài)鏈接的 image 遞歸加載伐蒂, 再從可執(zhí)行文件 image 遞歸加載所有符號。

3.2.7 framework

framework 可以是動態(tài)庫肛鹏,也是靜態(tài)庫饿自,是一個包含 dylib、bundle 和頭文件的文件夾龄坪。

3.3 啟動過程分析與優(yōu)化

啟動一個應(yīng)用時(shí),系統(tǒng)會通過fork()方法來新創(chuàng)建一個進(jìn)程复唤,然后執(zhí)行鏡像通過exec()來替換為另一個可執(zhí)行程序健田,然后執(zhí)行如下操作:

  1. 把可執(zhí)行文件加載到內(nèi)存空間,從可執(zhí)行文件中能夠分析出 dyld 的路徑佛纫;
  2. 把 dyld 加載到內(nèi)存妓局;
  3. dyld 從可執(zhí)行文件的依賴開始总放,遞歸加載所有的依賴動態(tài)鏈接庫 dylib 并進(jìn)行相應(yīng)的初始化操作。

結(jié)合上面 pre-main 打印的結(jié)果好爬,我們可以大致了解整個啟動過程如下圖所示:

exec() -> Load Executable -> Load Dyld -> Load Dylibs -> Rebase -> Binding ->ObjCSetUp -> Initializers
3.3.1 Load Dylibs

這一步局雄,指的是動態(tài)庫加載。在此階段存炮,dyld 會:

  • 分析 App 依賴的所有 dylib炬搭;
  • 找到 dylib 對應(yīng)的 Mach-O 文件;
  • 打開穆桂、讀取這些 Mach-O 文件宫盔,并驗(yàn)證其有效性;
  • 在系統(tǒng)內(nèi)核中注冊代碼簽名享完;
  • 對 dylib 的每一個 segment 調(diào)用 mmap()灼芭。

一般情況下,iOS App 需要加載 100-400 個 dylibs般又。這些動態(tài)庫包括系統(tǒng)的彼绷,也包括開發(fā)者手動引入的。其中大部分 dylib 都是系統(tǒng)庫茴迁,系統(tǒng)已經(jīng)做了優(yōu)化寄悯,因此開發(fā)者更應(yīng)關(guān)心自己手動集成的內(nèi)嵌 dylib,加載它們時(shí)性能開銷較大笋熬。

App 中依賴的 dylib 越少越好热某,Apple 官方建議盡量將內(nèi)嵌 dylib 的個數(shù)維持在6個以內(nèi)。

優(yōu)化方案:

  1. 盡量不使用內(nèi)嵌dylib
  2. 合并已有內(nèi)嵌dylib
  3. 檢查 framework 的 optional 和 required 設(shè)置胳螟,如果 framework 在當(dāng)前的 App 支持的 iOS 系統(tǒng)版本中都存在昔馋,就設(shè)為 required,因?yàn)樵O(shè)為 optional 會有額外的檢查導(dǎo)致加載變慢糖耸;
  4. 使用靜態(tài)庫作為代替秘遏;(不過靜態(tài)庫會在編譯期被打進(jìn)可執(zhí)行文件,造成可執(zhí)行文件體積增大嘉竟,兩者各有利弊邦危,開發(fā)者自行權(quán)衡。)
  5. 懶加載 dylib舍扰。(但使用 dlopen() 對性能會產(chǎn)生影響倦蚪,因?yàn)?App 啟動時(shí)是原本是單線程運(yùn)行,系統(tǒng)會取消加鎖边苹,但 dlopen() 開啟了多線程陵且,系統(tǒng)不得不加鎖,這樣不僅會使性能降低个束,可能還會造成死鎖及未知的后果慕购,不是很推薦這種做法聊疲。)
3.3.2 Rebase/Binding

指針重定位。

在 dylib 的加載過程中沪悲,系統(tǒng)為了安全考慮获洲,引入了 ASLR(Address Space Layout Randomization)技術(shù)和代碼簽名。由于 ASLR 的存在殿如,鏡像會在新的隨機(jī)地址(actual_address)上加載贡珊,和之前指針指向的地址(preferred_address)會有一個偏差(slide,slide=actual_address-preferred_address)握截,因此 dyld 需要修正這個偏差飞崖,指向正確的地址。具體通過這兩步實(shí)現(xiàn):

第一步:Rebase谨胞,在 image 內(nèi)部調(diào)整指針的指向固歪。將 image 讀入內(nèi)存,并以 page 為單位進(jìn)行加密驗(yàn)證胯努,保證不會被篡改牢裳,性能消耗主要在 IO。

第二步:Binding叶沛,符號綁定蒲讯。將指針指向 image 外部的內(nèi)容。查詢符號表灰署,設(shè)置指向鏡像外部的指針判帮,性能消耗主要在 CPU 計(jì)算。

通過以下命令可以查看 rebase 和 bind 等信息:

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

通過 LC_DYLD_INFO_ONLY 可以查看各種信息的偏移量和大小溉箕。如果想要更方便直觀地查看晦墙,推薦使用 MachOView 工具。

指針數(shù)量越少肴茄,指針修復(fù)的耗時(shí)也就越少晌畅。所以,優(yōu)化該階段的關(guān)鍵就是減少 __DATA 段中的指針數(shù)量寡痰。

優(yōu)化方案:

  1. 減少 ObjC 類(class)抗楔、方法(selector)、分類(category)的數(shù)量拦坠,比如合并一些功能连躏,刪除無效的類、方法和分類等(可以借助 AppCode 的 Inspect Code 功能進(jìn)行代碼瘦身)贞滨;
  2. 減少 C++ 虛函數(shù)反粥;(虛函數(shù)會創(chuàng)建 vtable,這也會在 __DATA 段中創(chuàng)建結(jié)構(gòu)。)
3.3.3 ObjC Setup

完成 Rebase 和 Bind 之后才顿,通知 runtime 去做一些代碼運(yùn)行時(shí)需要做的事情:

  • dyld 會注冊所有聲明過的 ObjC 類;
  • 將分類插入到類的方法列表中尤蒿;
  • 檢查每個 selector 的唯一性郑气。

優(yōu)化方案:

Rebase/Binding 階段優(yōu)化好了,這一步的耗時(shí)也會相應(yīng)減少腰池。

3.3.4 Initializers

Rebase 和 Binding 屬于靜態(tài)調(diào)整(fix-up)尾组,修改的是 __DATA 段中的內(nèi)容,而這里則開始動態(tài)調(diào)整示弓,往堆和棧中寫入內(nèi)容讳侨。具體工作有:

  • 調(diào)用每個 Objc 類和分類中的 +load 方法;
  • 調(diào)用 C/C++ 中的構(gòu)造器函數(shù)(用 attribute((constructor)) 修飾的函數(shù))奏属;
  • 創(chuàng)建非基本類型的 C++ 靜態(tài)全局變量跨跨。

優(yōu)化方案:

  1. 盡量避免在類的 +load 方法中初始化,可以推遲到 +initiailize 中進(jìn)行囱皿;(因?yàn)樵谝粋€ +load 方法中進(jìn)行運(yùn)行時(shí)方法替換操作會帶來 4ms 的消耗)

  2. 避免使用 atribute((constructor)) 將方法顯式標(biāo)記為初始化器勇婴,而是讓初始化方法調(diào)用時(shí)再執(zhí)行。比如用 dispatch_once()嘱腥、pthread_once() 或 std::once()耕渴,相當(dāng)于在第一次使用時(shí)才初始化,推遲了一部分工作耗時(shí)齿兔。:

  3. 減少非基本類型的 C++ 靜態(tài)全局變量的個數(shù)橱脸。(因?yàn)檫@類全局變量通常是類或者結(jié)構(gòu)體,如果在構(gòu)造函數(shù)中有繁重的工作分苇,就會拖慢啟動速度)

3.3.5 總結(jié)pre-main 階段可行的優(yōu)化方案
  • 重新梳理架構(gòu)添诉,減少不必要的內(nèi)置動態(tài)庫數(shù)量;

  • 進(jìn)行代碼瘦身组砚,合并或刪除無效的ObjC類吻商、Category、方法糟红、C++ 靜態(tài)全局變量等艾帐;

  • 將不必須在 +load 方法中執(zhí)行的任務(wù)延遲到 +initialize 中;

  • 減少 C++ 虛函數(shù)盆偿。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末柒爸,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子事扭,更是在濱河造成了極大的恐慌捎稚,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異今野,居然都是意外死亡葡公,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進(jìn)店門条霜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來催什,“玉大人,你說我怎么就攤上這事宰睡∑研祝” “怎么了?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵拆内,是天一觀的道長旋圆。 經(jīng)常有香客問我,道長麸恍,這世上最難降的妖魔是什么灵巧? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮或南,結(jié)果婚禮上孩等,老公的妹妹穿的比我還像新娘。我一直安慰自己采够,他們只是感情好肄方,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蹬癌,像睡著了一般权她。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上逝薪,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天隅要,我揣著相機(jī)與錄音,去河邊找鬼董济。 笑死步清,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的虏肾。 我是一名探鬼主播廓啊,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼封豪!你這毒婦竟也來了谴轮?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤吹埠,失蹤者是張志新(化名)和其女友劉穎第步,沒想到半個月后疮装,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡粘都,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年廓推,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片翩隧。...
    茶點(diǎn)故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡受啥,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出鸽心,到底是詐尸還是另有隱情,我是刑警寧澤居暖,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布顽频,位于F島的核電站,受9級特大地震影響太闺,放射性物質(zhì)發(fā)生泄漏糯景。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一省骂、第九天 我趴在偏房一處隱蔽的房頂上張望蟀淮。 院中可真熱鬧,春花似錦钞澳、人聲如沸怠惶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽策治。三九已至,卻和暖如春兰吟,著一層夾襖步出監(jiān)牢的瞬間通惫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工混蔼, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留履腋,地道東北人。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓惭嚣,卻偏偏與公主長得像遵湖,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子料按,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評論 2 353