優(yōu)化(首頁加載時(shí)間)
- 整理啟動(dòng)項(xiàng), 總結(jié)出耗時(shí)長(zhǎng)的模塊
- 分為2部分 main函數(shù)之前耗時(shí)和mian之后耗時(shí)
- Main前面主要做一些動(dòng)態(tài)庫dyld的加載和load方法的調(diào)用, 對(duì)于pre-main階段赛糟,Apple提供了一種測(cè)量方法來計(jì)算耗時(shí)時(shí)長(zhǎng)
- 減少依賴不必要的庫白粉,無論是動(dòng)態(tài)還是靜態(tài)庫吐根,如果可以的話把動(dòng)態(tài)庫改為靜態(tài)庫
- 用linkmap檢測(cè)出所有的方法和類邑雅,在用AppCode找出無用代碼和類皿伺,刪除掉寝贡,減少load方法調(diào)用
- 少在類的+load方法里做事情悼吱,盡量把這些事情推遲到+initiailize (可以減少main之前的運(yùn)行時(shí)間)
- 對(duì)于main()階段沸停,主要是測(cè)量main()函數(shù)開始執(zhí)行到didFinishLaunchingWithOptions執(zhí)行結(jié)束的耗時(shí),就需要自己插入代碼到工程中了疾宏。先在main()函數(shù)里用變量StartTime記錄當(dāng)前時(shí)間, 最后在didFinishLaunchingWithOptions里张足,再獲取一下當(dāng)前時(shí)間,與StartTime的差值即是main()階段運(yùn)行耗時(shí)坎藐。
- 懶加載(避免在首頁控制器的viewDidLoad和viewWillAppear做太多事情为牍,這2個(gè)方法執(zhí)行完,首頁控制器才能顯示)
- 圖片壓縮顺饮,圖片小了吵聪,io操作量也會(huì)變小,啟動(dòng)就快了
- 非必要加載業(yè)務(wù)延遲去做(比如放到首頁控制器的viewDidAppear方法)
- 主頁不用storyboard或者xib加載, 全部改為純代碼
- 采用性能更好的API
應(yīng)用啟動(dòng)原理流程
- 點(diǎn)擊圖標(biāo)兼雄,創(chuàng)建進(jìn)程
- mmap 主二進(jìn)制吟逝,找到 dyld 的路徑 (mmap 的全稱是 memory map,是一種內(nèi)存映射技術(shù)赦肋,可以把文件映射到虛擬內(nèi)存的地址空間里块攒,這樣就可以像直接操作內(nèi)存那樣來讀寫文件。)
- mmap dyld佃乘,把入口地址設(shè)為_dyld_start (dyld 是啟動(dòng)的輔助程序囱井,是 in-process 的,即啟動(dòng)的時(shí)候會(huì)把 dyld 加載到進(jìn)程的地址空間里趣避,然后把后續(xù)的啟動(dòng)過程交給 dyld;iOS 13 開始 Apple 對(duì)三方 App 啟用了 dyld3庞呕,dyld3 的最重要的特性就是啟動(dòng)閉包,閉包里包含了啟動(dòng)所需要的緩存信息程帕,從而提高啟動(dòng)速度住练。)
- 重啟手機(jī)/更新/下載 App 的第一次啟動(dòng),會(huì)創(chuàng)建啟動(dòng)閉包
- 把沒有加載的動(dòng)態(tài)庫 mmap 進(jìn)來愁拭,動(dòng)態(tài)庫的數(shù)量會(huì)影響這個(gè)階段
- 對(duì)每個(gè)二進(jìn)制做 bind 和 rebase(Rebase:修復(fù)內(nèi)部指針讲逛。這是因?yàn)?Mach-O 在 mmap 到虛擬內(nèi)存的時(shí)候,起始地址會(huì)有一個(gè)隨機(jī)的偏移量 slide岭埠,需要把內(nèi)部的指針指向加上這個(gè) slide盏混。Bind:修復(fù)外部指針。這個(gè)比較好理解惜论,因?yàn)橄?printf 等外部函數(shù)许赃,只有運(yùn)行時(shí)才知道它的地址是什么,bind 就是把指針指向這個(gè)地址)来涨,主要耗時(shí)在 Page In图焰,影響 Page In 數(shù)量的是 objc 的元數(shù)據(jù)有定義,過程:MMU 找到空閑的物理內(nèi)存頁面 蹦掐;觸發(fā)磁盤 IO技羔,把數(shù)據(jù)讀入物理內(nèi)存僵闯;如果是 TEXT 段的頁,要進(jìn)行解密藤滥;對(duì)解密后的頁鳖粟,進(jìn)行簽名驗(yàn)證)
- 初始化 objc 的 runtime,由于閉包已經(jīng)初始化了大部分拙绊,這里只會(huì)注冊(cè) sel 和裝載 category
- +load 和靜態(tài)初始化被調(diào)用向图,除了方法本身耗時(shí),這里還會(huì)引起大量 Page In
- 初始化 UIApplication标沪,啟動(dòng) Main Runloop
- 執(zhí)行 will/didFinishLaunch榄攀,這里主要是業(yè)務(wù)代碼耗時(shí)
- Layout,viewDidLoad 和Layoutsubviews 會(huì)在這里調(diào)用金句,Autolayout 太多會(huì)影響這部分時(shí)間
- Display檩赢,drawRect 會(huì)調(diào)用
- Prepare,圖片解碼發(fā)生在這一步
-
Commit违寞,首幀渲染數(shù)據(jù)打包發(fā)給 RenderServer贞瞒,啟動(dòng)結(jié)束
基于啟動(dòng)原理優(yōu)化
t(App 總啟動(dòng)時(shí)間) = t1(main 調(diào)用之前的加載時(shí)間) + t2(main 調(diào)用之后的加載時(shí)間),分main函數(shù)之前和main函數(shù)之后的相關(guān)方法優(yōu)化
查看耗時(shí)情況
獲得 main() 方法執(zhí)行前的耗時(shí)比較簡(jiǎn)單趁曼,通過 Xcode 自帶的測(cè)量方法既可以军浆。將 Xcode 中 Product -> Scheme -> Edit scheme -> Run -> Environment Variables 將環(huán)境變量 DYLD_PRINT_STATISTICS 或 DYLD_PRINT_STATISTICS_DETAILS 設(shè)為 1 即可獲得執(zhí)行每項(xiàng)耗時(shí):
Total pre-main time: 1.2 seconds (100.0%)
dylib loading time: 147.51 milliseconds (12.0%)
rebase/binding time: 112.82 milliseconds (9.2%)
ObjC setup time: 45.94 milliseconds (3.7%)
initializer time: 919.07 milliseconds (75.0%)
slowest intializers :
libSystem.B.dylib : 6.79 milliseconds (0.5%)
libMainThreadChecker.dylib : 34.62 milliseconds (2.8%)
libglInterpose.dylib : 353.67 milliseconds (28.8%)
TCLTV : 944.10 milliseconds (77.0%)
優(yōu)化實(shí)踐
啟動(dòng)任務(wù)拆分優(yōu)化
(1) 確定在展示 UI 前必須執(zhí)行的任務(wù)。
如果應(yīng)用是第一次啟動(dòng)挡闰,那么沒有必要加載任何用戶偏好乒融,如主題、刷新間隔摄悯、緩存大小等簇抵。此時(shí)是沒有任何自定義值的。初始緩存肆意增長(zhǎng)也是沒問題的射众,因?yàn)樗脑鲩L(zhǎng)不會(huì)超過最終的限制值。崩潰報(bào)告系統(tǒng)應(yīng)第一個(gè)被初始化晃财。
(2) 按順序執(zhí)行任務(wù)叨橱。
排序是非常重要的,因?yàn)槿蝿?wù)之間可能具有相互依賴性断盛,同時(shí)罗洗,排序還可以節(jié)省用戶的寶貴時(shí)間。例如钢猛,如果先觸發(fā)了訪問令牌的驗(yàn)證操作伙菜,那么其他任務(wù)可能會(huì)并行執(zhí)行,因?yàn)轵?yàn)證過程需要進(jìn)行網(wǎng)絡(luò)連接命迈。但是這樣就會(huì)導(dǎo)致一種情況:如果其他任務(wù)先完成贩绕,而驗(yàn)證還未完成火的,應(yīng)用就必須等待驗(yàn)證完成才能繼續(xù)執(zhí)行。
(3) 將任務(wù)拆分為兩類:一類是必須在主線程中執(zhí)行的任務(wù)淑倾,另一類是可以在其他線程中執(zhí)行的任務(wù) 馏鹤,然后分別執(zhí)行。還可以進(jìn)一步將在非主線程中執(zhí)行的任務(wù)分為可以并發(fā)執(zhí)行的和不能并發(fā)執(zhí)行的娇哆。
(4) 其他任務(wù)可以在加載 UI 后執(zhí)行或異步執(zhí)行湃累。
延遲其他子系統(tǒng)(如記錄儀和分析方法)的初始化。在應(yīng)用的后續(xù)階段將一些操作(例如碍讨,寫日志消息或跟蹤事件)放入隊(duì)列中治力,直到子系統(tǒng)完全完成初始化。
優(yōu)化方法
(1)在t1階段加快App啟動(dòng):
- 盡量使用靜態(tài)庫勃黍,減少動(dòng)態(tài)庫的使用宵统,動(dòng)態(tài)鏈接比較耗時(shí),如果要用動(dòng)態(tài)庫溉躲,盡量將多個(gè)dylib動(dòng)態(tài)庫合并成一個(gè)
- 盡量避免對(duì)系統(tǒng)庫使用optional linking榜田,如果App用到的系統(tǒng)庫在你所有支持的系統(tǒng)版本上都有,就設(shè)置為required锻梳,因?yàn)閛ptional會(huì)有些額外的檢查
- 減少Objective-C Class箭券、Selector、Category的數(shù)量疑枯,可以合并或者刪減一些OC類(怎樣刪減無用代碼:ViewConteroller 滲透率辩块,hook 對(duì)應(yīng)的聲明周期方法即可統(tǒng)計(jì);Class 滲透率荆永,遍歷運(yùn)行時(shí)的所有類废亭,通過 Objective C Runtime 的標(biāo)志位判斷類是否被訪問;行級(jí)滲透率具钥,需要用編譯期插樁豆村,對(duì)包大小和執(zhí)行速度均有損。)
- 刪減一些無用的靜態(tài)變量骂删,刪減沒有被調(diào)用到或者已經(jīng)廢棄的方法
- 將不必須在+load中做的事情盡量挪到+initialize中掌动,+initialize是在第一次初始化這個(gè)類之前被調(diào)用,+load在加載類的時(shí)候就被調(diào)用宁玫;或者做load方法遷移
- 盡量不要用C++虛函數(shù)粗恢,創(chuàng)建虛函數(shù)表有開銷
- 不要使用attribute((constructor))將方法顯式標(biāo)記為初始化器,而是讓初始化方法調(diào)用時(shí)才執(zhí)行欧瘪。比如使用dispatch_once()眷射,pthread_once()或 std::once()
- 在初始化方法中不調(diào)用dlopen(),dlopen()有性能和死鎖的可能性
- 在初始化方法中不創(chuàng)建線程
(2)在t2階段加快App啟動(dòng):
- 盡量不要使用xib/storyboard,而是用純代碼作為首頁UI妖碉,如果要用xib/storyboard涌庭,不要在xib/storyboard中存放太多的視圖
- 使用簡(jiǎn)單的廣告頁作為過渡,將首頁的計(jì)算操作及網(wǎng)絡(luò)請(qǐng)求放在廣告頁展示時(shí)異步進(jìn)行嗅绸。
- 對(duì)application:didFinishLaunchingWithOptions:里的任務(wù)盡量延遲加載或懶加載
- 不要在NSUserDefaults中存放太多的數(shù)據(jù)脾猛,NSUserDefaults是一個(gè)plist文件,plist文件會(huì)被反序列化一次
- 避免在啟動(dòng)時(shí)打印過多的log鱼鸠,少用NSLog猛拴,因?yàn)槊恳淮蜰SLog的調(diào)用都會(huì)創(chuàng)建一個(gè)新的NSCalendar實(shí)例
- 為了防止使用GCD創(chuàng)建過多的線程,解決方法是創(chuàng)建串行隊(duì)列蚀狰,或者使用帶有最大并發(fā)數(shù)限制的NSOperationQueue
- 不要在主線程執(zhí)行磁盤愉昆、網(wǎng)絡(luò)、Lock或者dispatch_sync麻蹋、發(fā)送消息給其他線程等操作
總結(jié)
總結(jié)起來跛溉,啟動(dòng)速度優(yōu)化就一句話:讓系統(tǒng)在啟動(dòng)期間少做一些事。當(dāng)然我們得先清楚工程里做的哪些事是在啟動(dòng)期間做的扮授、對(duì)啟動(dòng)速度的影響有多大芳室,然后case by case地分析工程代碼,通過放到子線程刹勃、延遲加載堪侯、懶加載等方式讓系統(tǒng)在啟動(dòng)期間更輕松些。