啟動(dòng)問(wèn)題陨界,APP啟動(dòng)慢顿痪,從啟動(dòng)到展示主頁(yè)面視圖需要5秒以上镊辕。
首先,研究APP啟動(dòng)流程蚁袭。
優(yōu)化方向征懈,main函數(shù)之前和main函數(shù)之后。
1.mach-O
哪些名詞指的是Mach-O
Executable 可執(zhí)行文件
Dylib 動(dòng)態(tài)庫(kù)
Bundle 無(wú)法被連接的動(dòng)態(tài)庫(kù)揩悄,只能通過(guò)dlopen()加載
Image 指的是Executable卖哎,Dylib或者Bundle的一種,文中會(huì)多次使用Image這個(gè)名詞删性。
Framework 動(dòng)態(tài)庫(kù)和對(duì)應(yīng)的頭文件和資源文件的集合
Apple出品的操作系統(tǒng)的可執(zhí)行文件格式幾乎都是mach-O亏娜,iOS當(dāng)然也不例外。mach-o可以大致分為三部分:
Header頭部蹬挺,包含可以執(zhí)行的CPU架構(gòu)维贺,比如x86,arm64
Load commands 加載命令巴帮,包含文件的組織架構(gòu)和在虛擬內(nèi)存中的布局方式
Data溯泣,數(shù)據(jù),包含load commands中需要的各個(gè)段(segment)的數(shù)據(jù)榕茧,每個(gè)Segment的大小都是Page的整數(shù)倍垃沦。
我們用MachOView打開(kāi)Demo工程的可以執(zhí)行文件,來(lái)驗(yàn)證下mach-o的文件布局:
那么Data部分又包含那些segment呢用押?絕大多數(shù)mach-o包括以下三個(gè)階段(支持用戶自定義Segment肢簿,但是很少使用)
__TEXT代碼段,只讀蜻拨,包含函數(shù)池充,和只讀的字符串,上圖中類似__TEXT,__text的都是代碼段
__Data數(shù)據(jù)段缎讼,讀寫(xiě)纵菌,包括可讀寫(xiě)的全局變量等,__DATA,__data都是數(shù)據(jù)段
__LINKEDIT包含了方法和變量的元數(shù)據(jù)(位置休涤,偏移量)咱圆,以及代碼簽名等信息笛辟。
關(guān)于mach-o更多細(xì)節(jié),可以看看文檔:《Mac OS X ABI Mach-O File Format Reference》
2.dyld
dyld的全稱是dynamic loader序苏,它的作用是加載一個(gè)進(jìn)程所需要的image手幢,dyld是開(kāi)源的。
3.Virtual Memory
虛擬內(nèi)存是在物理內(nèi)存上建立的一個(gè)邏輯地址空間忱详,它向上(應(yīng)用)提供了一個(gè)連續(xù)的邏輯地址空間围来,向下隱藏了物理內(nèi)存的細(xì)節(jié)。
虛擬內(nèi)存使得邏輯地址可以沒(méi)有實(shí)際的物理地址匈睁,也可以讓多個(gè)邏輯地址對(duì)應(yīng)到一個(gè)物理地址监透。
虛擬內(nèi)存被劃分為一個(gè)個(gè)大小相同的Page(64位系統(tǒng)上是16KB),提高管理和讀寫(xiě)的效率航唆。 Page又分為只讀和讀寫(xiě)的Page胀蛮。
4.Page fault
在應(yīng)用執(zhí)行的時(shí)候,它被分配的邏輯地址空間都是可以訪問(wèn)的糯钙,當(dāng)應(yīng)用訪問(wèn)一個(gè)邏輯Page粪狼,而在對(duì)應(yīng)的物理內(nèi)存中并不存在的時(shí)候,這時(shí)候就發(fā)生了一次Page fault任岸。當(dāng)Page fault發(fā)生的時(shí)候再榄,會(huì)中斷當(dāng)前的程序,在物理內(nèi)存中尋找一個(gè)可用的Page享潜,然后從磁盤(pán)中讀取數(shù)據(jù)到物理內(nèi)存困鸥,接著繼續(xù)執(zhí)行當(dāng)前程序。
5.Dirty Page & Clean Page
如果一個(gè)Page可以從磁盤(pán)上重新生成剑按,那么這Page稱為Clear Page
如果一個(gè)Page包含了進(jìn)程相關(guān)信息疾就,那么這個(gè)Page稱為Dirty Page
像代碼段這種只讀的Page就是Clean Page。而數(shù)據(jù)段(__DATA)這種讀寫(xiě)的Page吕座,當(dāng)寫(xiě)數(shù)據(jù)發(fā)生的時(shí)候虐译,會(huì)觸發(fā)CO(Copy on write)瘪板,也就是寫(xiě)時(shí)復(fù)制吴趴,Page會(huì)被標(biāo)記成Dirty,同時(shí)會(huì)被復(fù)制侮攀。
想要了解更多細(xì)節(jié)锣枝,可以閱讀文檔:Memory Usage Performance Guidelines
6.啟動(dòng)過(guò)程
使用dyld2啟動(dòng)應(yīng)用的過(guò)程如圖:
大致的過(guò)程如下:
加載dyld到App進(jìn)程
加載動(dòng)態(tài)庫(kù)(包括所依賴的所有動(dòng)態(tài)庫(kù))
Rebase
Bind
初始化Objective C Runtime
其它的初始化代碼
(1)加載動(dòng)態(tài)庫(kù)
dyld會(huì)首先讀取mach-o文件的Header和load commands。
接著就知道了這個(gè)可執(zhí)行文件依賴的動(dòng)態(tài)庫(kù)兰英。例如加載動(dòng)態(tài)庫(kù)A到內(nèi)存撇叁,接著檢查A所依賴的動(dòng)態(tài)庫(kù),就這樣的遞歸加載畦贸,直到所有的動(dòng)態(tài)庫(kù)加載完畢陨闹。通常一個(gè)App所依賴的動(dòng)態(tài)庫(kù)在100-400個(gè)左右楞捂,其中大多數(shù)都是系統(tǒng)的動(dòng)態(tài)庫(kù),它們會(huì)被緩存到dyld shared cache趋厉,這樣讀取的效率會(huì)很高寨闹。
查看mach-o文件所依賴的動(dòng)態(tài)庫(kù),可以通過(guò)MachOView的圖形化界面(展開(kāi)Load Command就能看到)君账,也可以通過(guò)命令行otool繁堡。
(2)Rebase && Bind
這里先來(lái)講講為什么要Rebase?
有兩種主要的技術(shù)來(lái)保證應(yīng)用的安全:ASLR和Code Sign乡数。
ASLR的全稱是Address space layout randomization椭蹄,翻譯過(guò)來(lái)就是“地址空間布局隨機(jī)化”。App被啟動(dòng)的時(shí)候净赴,程序會(huì)被影射到邏輯的地址空間绳矩,這個(gè)邏輯的地址空間有一個(gè)起始地址,而ASLR技術(shù)使得這個(gè)起始地址是隨機(jī)的劫侧。如果是固定的埋酬,那么黑客很容易就可以由起始地址+偏移量找到函數(shù)的地址。
Code Sign相信大多數(shù)開(kāi)發(fā)者都知曉烧栋,這里要提一點(diǎn)的是写妥,在進(jìn)行Code sign的時(shí)候,加密哈希不是針對(duì)于整個(gè)文件审姓,而是針對(duì)于每一個(gè)Page的珍特。這就保證了在dyld進(jìn)行加載的時(shí)候,可以對(duì)每一個(gè)page進(jìn)行獨(dú)立的驗(yàn)證魔吐。
mach-o中有很多符號(hào)扎筒,有指向當(dāng)前mach-o的,也有指向其他dylib的酬姆,比如printf嗜桌。那么,在運(yùn)行時(shí)辞色,代碼如何準(zhǔn)確的找到printf的地址呢骨宠?
mach-o中采用了PIC技術(shù),全稱是Position Independ code相满。當(dāng)你的程序要調(diào)用printf的時(shí)候层亿,會(huì)先在__DATA段中建立一個(gè)指針指向printf,在通過(guò)這個(gè)指針實(shí)現(xiàn)間接調(diào)用立美。dyld這時(shí)候需要做一些fix-up工作匿又,即幫助應(yīng)用程序找到這些符號(hào)的實(shí)際地址。主要包括兩部分
Rebase 修正內(nèi)部(指向當(dāng)前mach-o文件)的指針指向
Bind 修正外部指針指向
之所以需要Rebase建蹄,是因?yàn)閯倓偺岬降腁SLR使得地址隨機(jī)化碌更,導(dǎo)致起始地址不固定裕偿,另外由于Code Sign,導(dǎo)致不能直接修改Image痛单。Rebase的時(shí)候只需要增加對(duì)應(yīng)的偏移量即可击费。待Rebase的數(shù)據(jù)都存放在__LINKEDIT中。
可以通過(guò)MachOView查看:Dynamic Loader Info -> Rebase Info
Rebase解決了內(nèi)部的符號(hào)引用問(wèn)題桦他,而外部的符號(hào)引用則是由Bind解決蔫巩。在解決Bind的時(shí)候,是根據(jù)字符串匹配的方式查找符號(hào)表快压,所以這個(gè)過(guò)程相對(duì)于Rebase來(lái)說(shuō)是略慢的圆仔。
同樣,也可以通過(guò)xcrun dyldinfo來(lái)查看Bind的信息蔫劣,比如我們查看bind信息中坪郭。
(3)Objective C
Objective C是動(dòng)態(tài)語(yǔ)言,所以在執(zhí)行main函數(shù)之前脉幢,需要把類的信息注冊(cè)到一個(gè)全局的Table中歪沃。同時(shí),Objective C支持Category嫌松,在初始化的時(shí)候沪曙,也會(huì)把Category中的方法注冊(cè)到對(duì)應(yīng)的類中,同時(shí)會(huì)唯一Selector萎羔,這也是為什么當(dāng)你的Cagegory實(shí)現(xiàn)了類中同名的方法后液走,類中的方法會(huì)被覆蓋。
另外贾陷,由于iOS開(kāi)發(fā)時(shí)基于Cocoa Touch的缘眶,所以絕大多數(shù)的類起始都是系統(tǒng)類,所以大多數(shù)的Runtime初始化起始在Rebase和Bind中已經(jīng)完成髓废。
(4)Initializers
接下來(lái)就是必要的初始化部分了巷懈,主要包括幾部分:
+load方法。
C/C++靜態(tài)初始化對(duì)象和標(biāo)記為attribute(constructor)的方法
這里要提一點(diǎn)的就是慌洪,+load方法已經(jīng)被棄用了顶燕,如果你用Swift開(kāi)發(fā),你會(huì)發(fā)現(xiàn)根本無(wú)法去寫(xiě)這樣一個(gè)方法蒋譬,官方的建議是實(shí)用initialize割岛。區(qū)別就是愉适,load是在類裝載的時(shí)候執(zhí)行犯助,而initialize是在類第一次收到message前調(diào)用。
dyld3
上文的講解是dyld2的加載方式维咸。而最新的是dyld3加載方式略有不同:
dyld2是純粹的in-process剂买,也就是在程序進(jìn)程內(nèi)執(zhí)行的惠爽,也就意味著只有當(dāng)應(yīng)用程序被啟動(dòng)的時(shí)候,dyld2才能開(kāi)始執(zhí)行任務(wù)瞬哼。
dyld3則是部分out-of-process婚肆,部分in-process。圖中坐慰,虛線之上的部分是out-of-process的较性,在App下載安裝和版本更新的時(shí)候會(huì)去執(zhí)行,out-of-process會(huì)做如下事情:
分析Mach-o Headers
分析依賴的動(dòng)態(tài)庫(kù)
查找需要Rebase & Bind之類的符號(hào)
把上述結(jié)果寫(xiě)入緩存
這樣结胀,在應(yīng)用啟動(dòng)的時(shí)候赞咙,就可以直接從緩存中讀取數(shù)據(jù),加快加載速度糟港。
7.啟動(dòng)時(shí)間
(1)冷啟動(dòng) VS 熱啟動(dòng)
如果你剛剛啟動(dòng)過(guò)App攀操,這時(shí)候App的啟動(dòng)所需要的數(shù)據(jù)仍然在緩存中,再次啟動(dòng)的時(shí)候稱為熱啟動(dòng)秸抚。如果設(shè)備剛剛重啟速和,然后啟動(dòng)App,這時(shí)候稱為冷啟動(dòng)剥汤。
啟動(dòng)時(shí)間在小于400ms是最佳的颠放,因?yàn)閺狞c(diǎn)擊圖標(biāo)到顯示Launch Screen,到Launch Screen消失這段時(shí)間是400ms吭敢。啟動(dòng)時(shí)間不可以大于20s慈迈,否則會(huì)被系統(tǒng)殺掉。
在Xcode中省有,可以通過(guò)設(shè)置環(huán)境變量來(lái)查看App的啟動(dòng)時(shí)間痒留,DYLD_PRINT_STATISTICS和DYLD_PRINT_STATISTICS_DETAILS。
Total pre-main time: 43.00 milliseconds (100.0%)
dylib loading time: 19.01 milliseconds (44.2%)
rebase/binding time: 1.77 milliseconds (4.1%)
ObjC setup time: 3.98 milliseconds (9.2%)
initializer time: 18.17 milliseconds (42.2%)
slowest intializers :
libSystem.B.dylib : 2.56 milliseconds (5.9%)
libBacktraceRecording.dylib : 3.00 milliseconds (6.9%)
libMainThreadChecker.dylib : 8.26 milliseconds (19.2%)
ModelIO : 1.37 milliseconds (3.1%)
對(duì)于這個(gè)libMainThreadChecker.dylib估計(jì)很多同學(xué)會(huì)有點(diǎn)陌生蠢沿,這是XCode 9新增的動(dòng)態(tài)庫(kù)伸头,用來(lái)做主線成檢查的。
(2)優(yōu)化啟動(dòng)時(shí)間
啟動(dòng)時(shí)間這個(gè)名詞舷蟀,不同的人有不同的定義恤磷。在我看來(lái),
啟動(dòng)時(shí)間是用戶點(diǎn)擊App圖標(biāo)野宜,到第一個(gè)界面展示的時(shí)間扫步。
以main函數(shù)作為分水嶺,啟動(dòng)時(shí)間其實(shí)包括了兩部分:main函數(shù)之前和main函數(shù)到第一個(gè)界面的viewDidAppear:匈子。所以河胎,優(yōu)化也是從兩個(gè)方面進(jìn)行的,個(gè)人建議優(yōu)先優(yōu)化后者虎敦,因?yàn)榻^大多數(shù)App的瓶頸在自己的代碼里游岳。
(3)Main函數(shù)之后
我們首先來(lái)分析下政敢,從main函數(shù)開(kāi)始執(zhí)行,到你的第一個(gè)界面顯示胚迫,這期間一般會(huì)做哪些事情喷户。
執(zhí)行AppDelegate的代理方法,主要是didFinishLaunchingWithOptions
初始化Window访锻,初始化基礎(chǔ)的ViewController結(jié)構(gòu)(一般是UINavigationController+UITabViewController)
獲取數(shù)據(jù)(Local DB/Network)褪尝,展示給用戶。
(4)UIViewController
延遲初始化那些不必要的UIViewController期犬。
比如網(wǎng)易新聞:在啟動(dòng)的時(shí)候只需要初始化首頁(yè)的頭條頁(yè)面即可恼五。像“要聞”,“我的”等頁(yè)面哭懈,則延遲加載灾馒,即啟動(dòng)的時(shí)候只是一個(gè)UIViewController作為占位符給TabController,等到用戶點(diǎn)擊了再去進(jìn)行真正的數(shù)據(jù)和視圖的初始化工作遣总。
(5)AppDelegate
通常我們會(huì)在AppDelegate的代理方法里進(jìn)行初始化工作睬罗,主要包括了兩個(gè)方法:
didFinishLaunchingWithOptions
applicationDidBecomeActive
優(yōu)化這些初始化的核心思想就是:
能延遲初始化的盡量延遲初始化,不能延遲初始化的盡量放到后臺(tái)初始化旭斥。
這些工作主要可以分為幾類:
三方SDK初始化容达,比如Crash統(tǒng)計(jì); 像分享之類的,可以等到第一次調(diào)用再出初始化垂券。
初始化某些基礎(chǔ)服務(wù)花盐,比如WatchDog,遠(yuǎn)程參數(shù)菇爪。
啟動(dòng)相關(guān)日志算芯,日志往往涉及到DB操作,一定要放到后臺(tái)去做
業(yè)務(wù)方初始化凳宙,這個(gè)交由每個(gè)業(yè)務(wù)自己去控制初始化時(shí)間熙揍。
對(duì)于didFinishLaunchingWithOptions的代碼,建議按照以下的方式進(jìn)行劃分:
@interfaceAppDelegate()
//業(yè)務(wù)方需要的生命周期回調(diào)@property(strong,nonatomic)NSArray<id<UIApplicationDelegate>>*eventQueues;
//主框架負(fù)責(zé)的生命周期回調(diào)
@property(strong,nonatomic)id<UIApplicationDelegate>basicDelegate;
@end
然后氏涩,你會(huì)得到一個(gè)非常干凈的AppDelegate文件:
-(BOOL)application:(UIApplication)application didFinishLaunchingWithOptions:(NSDictionary)launchOptions{
for(id<UIApplicationDelegate> delegatein self.eventQueues){
[delegate application:application didFinishLaunchingWithOptions:launchOptions];
}
return [self.basicDelegate application:application didFinishLaunchingWithOptions:launchOptions];
}
由于對(duì)這些初始化進(jìn)行了分組届囚,在開(kāi)發(fā)期就可以很容易的控制每一個(gè)業(yè)務(wù)的初始化時(shí)間:
CFTimeInterval startTime=CACurrentMediaTime();
//執(zhí)行方法
CFTimeInterval endTime=CACurrentMediaTime();
(6)用Time Profiler找到元兇
Time Profiler在分析時(shí)間占用上非常強(qiáng)大。實(shí)用的時(shí)候注意三點(diǎn)
在打包模式下分析(一般是Release),這樣和線上環(huán)境一樣是尖。
記得開(kāi)啟dsym意系,不然無(wú)法查看到具體的函數(shù)調(diào)用堆棧
分析性能差的設(shè)備,對(duì)于支持iOS 8的饺汹,一般分析iphone 4s或者iphone 5蛔添。
一個(gè)典型的分析界面如下:
幾點(diǎn)要注意:
分析啟動(dòng)時(shí)間,一般只關(guān)心主線程
選擇Hide System Libraries和Invert Call Tree,這樣我們能專注于自己的代碼
右側(cè)可以看到詳細(xì)的調(diào)用堆棧信息
在某一行上雙擊作郭,我們可以進(jìn)入到代碼預(yù)覽界面,去看看實(shí)際每一行占用了多少時(shí)間:
小結(jié)
不同的App在啟動(dòng)的時(shí)候做的事情往往不同弦疮,但是優(yōu)化起來(lái)的核心思想無(wú)非就兩個(gè):
能延遲執(zhí)行的就延遲執(zhí)行夹攒。比如SDK的初始化,界面的創(chuàng)建胁塞。
不能延遲執(zhí)行的咏尝,盡量放到后臺(tái)執(zhí)行。比如數(shù)據(jù)讀取啸罢,原始JSON數(shù)據(jù)轉(zhuǎn)對(duì)象编检,日志發(fā)送。
(7)Main函數(shù)之前
Main函數(shù)之前是iOS系統(tǒng)的工作扰才,所以這部分的優(yōu)化往往更具有通用性允懂。
(8)dylibs
啟動(dòng)的第一步是加載動(dòng)態(tài)庫(kù),加載系統(tǒng)的動(dòng)態(tài)庫(kù)使很快的衩匣,因?yàn)榭梢跃彺胬僮埽虞d內(nèi)嵌的動(dòng)態(tài)庫(kù)速度較慢。所以琅捏,提高這一步的效率的關(guān)鍵是:減少動(dòng)態(tài)庫(kù)的數(shù)量生百。
合并動(dòng)態(tài)庫(kù),比如公司內(nèi)部由私有Pod建立了如下動(dòng)態(tài)庫(kù):XXTableView, XXHUD, XXLabel柄延,強(qiáng)烈建議合并成一個(gè)XXUIKit來(lái)提高加載速度蚀浆。
(9)Rebase & Bind & Objective C Runtime
Rebase和Bind都是為了解決指針引用的問(wèn)題。對(duì)于Objective C開(kāi)發(fā)來(lái)說(shuō)搜吧,主要的時(shí)間消耗在Class/Method的符號(hào)加載上市俊,所以常見(jiàn)的優(yōu)化方案是:
減少__DATA段中的指針數(shù)量。
合并Category和功能類似的類滤奈。比如:UIView+Frame,UIView+AutoLayout…合并為一個(gè)
刪除無(wú)用的方法和類秕衙。
多用Swift Structs,因?yàn)镾wfit Structs是靜態(tài)分發(fā)的僵刮。感興趣的同學(xué)可以看看我之前這篇文章:《Swift進(jìn)階之內(nèi)存模型和方法調(diào)度》
(10)Initializers
通常据忘,我們會(huì)在+load方法中進(jìn)行method-swizzling,這也是Nshipster推薦的方式搞糕。
用initialize替代load勇吊。不少同學(xué)喜歡用method-swizzling來(lái)實(shí)現(xiàn)AOP去做日志統(tǒng)計(jì)等內(nèi)容,強(qiáng)烈建議改為在initialize進(jìn)行初始化窍仰。
減少atribute((constructor))的使用汉规,而是在第一次訪問(wèn)的時(shí)候才用dispatch_once等方式初始化。
不要?jiǎng)?chuàng)建線程
使用Swfit重寫(xiě)代碼。
參考資料:
WWDC 2016: Optimizing App Startup Time
WWDC 2017: App Startup Time: Past, Present, and Future
好文:小暖風(fēng)http://www.reibang.com/p/6aa6bf62bf5d