App啟動(dòng)過程
iOS應(yīng)用的啟動(dòng)可分為pre-main階段和main( )階段,其中系統(tǒng)做的事情依次是:

無論對于系統(tǒng)的動(dòng)態(tài)鏈接庫還是對于App本身的可執(zhí)行文件而言,他們都算是image(鏡像),而每個(gè)App都是以image( 鏡像)為單位進(jìn)行加載的
image:
1.Executable: 應(yīng)用的主要二進(jìn)制(比如.o文件)
2.Dylib: [動(dòng)態(tài)鏈接庫](dynamic library,又稱 DSO 或 DLL)
3.Bundle: 資源文件,不能被鏈接的 Dylib,只能在運(yùn)行時(shí)使用 dlopen() 加載
1. pre-main階段
1.1. 加載應(yīng)用的可執(zhí)行文件(自身App的所有.o文件的集合)
1.2. 加載動(dòng)態(tài)鏈接器dyld(dynamic loader,是一個(gè)專門用來加載動(dòng)態(tài)鏈接庫的庫)
1.3. dyld遞歸加載應(yīng)用所有依賴的動(dòng)態(tài)鏈接庫dylib
2. main( )階段
2.1. dyld調(diào)用main( )
2.2. 調(diào)用UIApplicationMain( )
2.3. 調(diào)用applicationWillFinishLaunching
2.4. 調(diào)用didFinishLaunchingWithOptions
一、 pre-main階段的過程和可優(yōu)化項(xiàng)
dyld的加載主要分為4步:
1.Load dylibs

這一階段dyld會(huì)分析應(yīng)用依賴的dylib(xcode7以后.dylib已改為名.tbd),找到其mach-o文件,打開和讀取這些文件并驗(yàn)證其有效性,接著會(huì)找到代碼簽名注冊到內(nèi)核,最后對dylib的每一個(gè)segment調(diào)用mmap()。
一般情況下,iOS應(yīng)用會(huì)加載100-400個(gè)dylibs,其中大部分是系統(tǒng)庫,這部分dylib的加載系統(tǒng)已經(jīng)做了優(yōu)化。
所以,依賴的dylib越少越好。在這一步,我們可以做的優(yōu)化有:
1.1、盡量不使用內(nèi)嵌(embedded)的dylib,加載內(nèi)嵌dylib性能開銷較大
1.2、合并已有的dylib和使用靜態(tài)庫(static archives),減少dylib的使用個(gè)數(shù)
1.3、懶加載dylib,但是要注意dlopen()可能造成一些問題,且實(shí)際上懶加載做的工作會(huì)更多
2. Rebase/Bind

在dylib的加載過程中,系統(tǒng)為了安全考慮,引入了ASLR(Address Space Layout Randomization)技術(shù)和代碼簽名。由于ASLR的存在,鏡像(Image,包括可執(zhí)行文件、dylib和bundle)會(huì)在隨機(jī)的地址上加載,和之前指針指向的地址(preferred_address)會(huì)有一個(gè)偏差(slide),dyld需要修正這個(gè)偏差,來指向正確的地址。
Rebase在前,Bind在后,Rebase做的是將鏡像讀入內(nèi)存,修正鏡像內(nèi)部的指針,性能消耗主要在IO。Bind做的是查詢符號表,設(shè)置指向鏡像外部的指針,性能消耗主要在CPU計(jì)算。
所以,指針數(shù)量越少越好。在這一步,我們可以做的優(yōu)化有:
2.1、減少ObjC類(class)、方法(selector)、分類(category)的數(shù)量
2.2、減少[C++虛函數(shù)]的數(shù)量(創(chuàng)建虛函數(shù)表有開銷)
2.3、使用Swift structs(內(nèi)部做了優(yōu)化,符號數(shù)量更少)
3. Objc setup

大部分ObjC初始化工作已經(jīng)在Rebase/Bind階段做完了,這一步dyld會(huì)注冊所有聲明過的ObjC類,將分類插入到類的方法列表里,再檢查每個(gè)selector的唯一性。
在這一步?jīng)]什么優(yōu)化可做的,Rebase/Bind階段優(yōu)化好了,這一步的耗時(shí)也會(huì)減少。
4. Initializers

到了這一階段,dyld開始運(yùn)行程序的初始化函數(shù),調(diào)用每個(gè)Objc類和分類的+load方法,調(diào)用C/C++ 中的構(gòu)造器函數(shù)(用attribute((constructor))修飾的函數(shù)),和創(chuàng)建非基本類型的C++靜態(tài)全局變量(通常是類或結(jié)構(gòu)體)。Initializers階段執(zhí)行完后,dyld開始調(diào)用main()函數(shù)。
Objc的load函數(shù)和C++的靜態(tài)構(gòu)造函數(shù)采用由底向上的方式執(zhí)行,來保證每個(gè)執(zhí)行的方法,都可以找到所依賴的動(dòng)態(tài)庫。例:

在這一步,我們可以做的優(yōu)化有:
4.1、少在類的+load方法里做事情,盡量把這些事情推遲到+initiailize
4.2、減少構(gòu)造器函數(shù)個(gè)數(shù),在構(gòu)造器函數(shù)里少做些事情
4.3、減少C++靜態(tài)全局變量的個(gè)數(shù)
二、main( )階段的可優(yōu)化項(xiàng)
這一階段的優(yōu)化主要是減少didFinishLaunchingWithOptions方法里的工作,在didFinishLaunchingWithOptions方法里,我們會(huì)創(chuàng)建應(yīng)用的window,指定其rootViewController,調(diào)用window的makeKeyAndVisible方法讓其可見。由于業(yè)務(wù)需要,我們會(huì)初始化各個(gè)二方/三方庫,設(shè)置系統(tǒng)UI風(fēng)格,檢查是否需要顯示引導(dǎo)頁、是否需要登錄、是否有新版本等,由于歷史原因,這里的代碼容易變得比較龐大,啟動(dòng)耗時(shí)難以控制。
所以,滿足業(yè)務(wù)需要的前提下,didFinishLaunchingWithOptions在主線程里做的事情越少越好。我們可以做的優(yōu)化有:
1、梳理各個(gè)二方/三方庫,找到可以延遲加載的庫,做延遲加載處理,比如放到首頁控制器的viewDidAppear方法里。
2、梳理業(yè)務(wù)邏輯,把可以延遲執(zhí)行的邏輯,做延遲執(zhí)行處理。比如檢查新版本、注冊推送通知等邏輯。
3、避免復(fù)雜/多余的計(jì)算。
4、采用性能更好的API。
5、避免在首頁控制器的viewDidLoad和viewWillAppear做太多事情,這2個(gè)方法執(zhí)行完,首頁控制器才能顯示,部分可以延遲創(chuàng)建的視圖應(yīng)做延遲創(chuàng)建/懶加載處理。
6、首頁控制器用純代碼方式來構(gòu)建。
三、啟動(dòng)優(yōu)化總結(jié)
1. pre-main階段的優(yōu)化
pre-main階段優(yōu)化蘋果給出的建議最好是400ms之內(nèi),但這個(gè)肯定要按照項(xiàng)目的實(shí)際情況有所取舍。
1.1、排查無用的dylib(不確定的可以先刪除,在編譯下項(xiàng)目試試),減少dylib的數(shù)目
1.2、檢查 framework應(yīng)當(dāng)設(shè)為optional和required,如果該framework在當(dāng)前App支持的所有iOS系統(tǒng)版本都存在,那么就設(shè)為required,否則就設(shè)為optional
1.3、減少ObjC類(項(xiàng)目中不常用的庫,廢棄的代碼等)、方法(selector)、分類(category)的數(shù)量、無用的庫、非基本類型的C++靜態(tài)全局變量(通常是類或結(jié)構(gòu)體)
1.4、壓縮資源圖片,刪除無用的圖片(IO操作)
1.5、少在類的+load方法里做事情,盡量把這些事情推遲到+initiailize
1.6、使用Swift structs(這是長期工作,可以考慮未來新頁面用swift寫)
2. main()階段的優(yōu)化
2.1、可使用instruments的Time Profiler先分析啟動(dòng)時(shí)哪些地方比較耗時(shí),是否可以做優(yōu)化
2.2、梳理各個(gè)二方/三方庫,找到可以延遲加載的庫,做延遲加載處理,比如放到首頁控制器或tabBar控制器的viewDidAppear方法里,并且保證只執(zhí)行一次(按項(xiàng)目結(jié)構(gòu),放在合適的地方)
2.3、梳理業(yè)務(wù)邏輯,把可以延遲執(zhí)行的邏輯,做延遲執(zhí)行處理。比如檢查新版本、注冊推送通知等邏輯。
2.4、避免復(fù)雜/多余的計(jì)算
2.5、每次用NSLog方式打印會(huì)隱式的創(chuàng)建一個(gè)Calendar,因此需要?jiǎng)h減啟動(dòng)時(shí)各業(yè)務(wù)方打的log
2.6、避免在用戶看到的第一個(gè)界面(首頁控制器或注冊登錄頁面)的viewDidLoad和viewWillAppear做太多事情,這2個(gè)方法執(zhí)行完,第一個(gè)頁面才能顯示,部分可以延遲創(chuàng)建的視圖應(yīng)做延遲創(chuàng)建/懶加載處理
2.7、首頁控制器或注冊登錄頁面用純代碼方式來構(gòu)建
2.8、我們項(xiàng)目中每次啟動(dòng)會(huì)全量拉取AppServerConfig的配置,內(nèi)容太多,未來需要api配合拆分,等頁面使用的時(shí)候在拉取相應(yīng)配置
2.9、持久化數(shù)據(jù)的讀取到內(nèi)存中的時(shí)間也可以評估一下