前言
當(dāng)用戶按下home鍵的時(shí)候隅俘,iOS的App并不會(huì)馬上被kill掉,還會(huì)繼續(xù)存活若干時(shí)間笤喳。理想情況下为居,用戶點(diǎn)擊App的圖標(biāo)再次回來(lái)的時(shí)候,App幾乎不需要做什么杀狡,就可以還原到退出前的狀態(tài)蒙畴,繼續(xù)為用戶服務(wù)。這種持續(xù)存活的情況下啟動(dòng)App呜象,我們稱為熱啟動(dòng)膳凝,相對(duì)而言冷啟動(dòng)就是App被kill掉以后一切從頭開(kāi)始啟動(dòng)的過(guò)程。我們這里只討論App冷啟動(dòng)的情況恭陡。
對(duì)于冷啟動(dòng)來(lái)說(shuō)蹬音,啟動(dòng)時(shí)間是指從用戶點(diǎn)擊 APP 那一刻開(kāi)始到用戶看到第一個(gè)界面這中間的時(shí)間。我們進(jìn)行優(yōu)化的時(shí)候休玩,我們將啟動(dòng)時(shí)間分為 pre-main
時(shí)間和 main
函數(shù)到第一個(gè)界面渲染完成時(shí)間這兩個(gè)部分著淆。
因?yàn)?APP 的入口在 main
函數(shù) ,在 main 函數(shù)之后我們的代碼才會(huì)執(zhí)行拴疤。
這里有兩個(gè)階段
1. pre-main階段
1.1. 加載應(yīng)用的可執(zhí)行文件
1.2. 加載動(dòng)態(tài)鏈接庫(kù)加載器dyld(dynamic loader)
1.3. dyld遞歸加載應(yīng)用所有依賴的dylib(dynamic library 動(dòng)態(tài)鏈接庫(kù))
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
階段稱為 t1
永部,main()
階段一直到首個(gè)頁(yè)面加載完成稱為 t2
。
t1 時(shí)間的優(yōu)化分析
t1
部分主要參考自APP啟動(dòng)優(yōu)化的一次實(shí)踐
其中 t1
蘋(píng)果提供了內(nèi)建的測(cè)量方法, Xcode 中 Edit scheme -> Run -> Auguments 將環(huán)境變量 DYLD_PRINT_STATISTICS 設(shè)為 1
//結(jié)果為
Total pre-main time: 1.4 seconds (100.0%)
dylib loading time: 1.3 seconds (89.4%)
rebase/binding time: 36.75 milliseconds (2.5%)
ObjC setup time: 35.65 milliseconds (2.4%)
initializer time: 80.97 milliseconds (5.5%)
slowest intializers :
libSystem.B.dylib : 12.63 milliseconds (0.8%)
//解讀
1呐矾、main()函數(shù)之前總共使用了1.4s
2苔埋、在94.33ms中,加載動(dòng)態(tài)庫(kù)用了1.3s蜒犯,指針重定位使用了36.75ms组橄,ObjC類初始化使用了35.65ms荞膘,各種初始化使用了80.97ms。
3晨炕、在初始化耗費(fèi)的80.97ms中衫画,用時(shí)最多的初始化是libSystem.B.dylib。
可以看到,我的 dylib loading time
花費(fèi)了 1.3s
時(shí)間瓮栗,
其中各部分的作用是
加載dylib
分析每個(gè)dylib(大部分是iOS系統(tǒng)的)削罩,找到其Mach-O文件,
打開(kāi)并讀取驗(yàn)證有效性费奸,找到代碼簽名注冊(cè)到內(nèi)核弥激,
最后對(duì)dylib的每個(gè)segment調(diào)用mmap()。
rebase/bind
dylib加載完成之后愿阐,它們處于相互獨(dú)立的狀態(tài)微服,需要綁定起來(lái)。
在dylib的加載過(guò)程中缨历,系統(tǒng)為了安全考慮以蕴,引入了ASLR(Address Space Layout Randomization)技術(shù)和代碼簽名。
由于ASLR的存在辛孵,鏡像(Image丛肮,包括可執(zhí)行文件、dylib和bundle)會(huì)在隨機(jī)的地址上加載魄缚,和之前指針指向的地址(preferred_address)會(huì)有一個(gè)偏差(slide)缔御,dyld需要修正這個(gè)偏差榨汤,來(lái)指向正確的地址。
Rebase在前到踏,Bind在后培遵,Rebase做的是將鏡像讀入內(nèi)存村砂,修正鏡像內(nèi)部的指針保屯,性能消耗主要在IO辜膝。
Bind做的是查詢符號(hào)表,設(shè)置指向鏡像外部的指針飞蛹,性能消耗主要在CPU計(jì)算须肆。
OC setup
OC的runtime需要維護(hù)一張類名與類的方法列表的全局表。
dyld做了如下操作:
對(duì)所有聲明過(guò)的OC類桩皿,將其注冊(cè)到這個(gè)全局表中(class registration)
將category的方法插入到類的方法列表中(category registration)
檢查每個(gè)selector的唯一性(selector uniquing)
如果在各個(gè) OC 類別的 ‘load’方法里做了不少事情(如在里面使用 Method swizzle),那么這是pre-main階段最耗時(shí)的部分。dyld運(yùn)行APP的初始化函數(shù)幢炸,調(diào)用每個(gè)OC類的+load方法泄隔,調(diào)用C++的構(gòu)造器函數(shù)(attribute((constructor))修飾),創(chuàng)建非基本類型的C++靜態(tài)全局變量宛徊,然后執(zhí)行main函數(shù)佛嬉。
優(yōu)化思路是
1. 移除不需要用到的動(dòng)態(tài)庫(kù)
2. 移除不需要用到的類
3. 合并功能類似的類和擴(kuò)展
4. 盡量避免在+load方法里執(zhí)行的操作逻澳,可以推遲到+initialize方法中。
t2 時(shí)間的優(yōu)化分析
t2
使用了來(lái)自NewPan大大 的打點(diǎn)計(jì)時(shí)器BLStopwatch
可以看到暖呕,我的 APP 加載時(shí)間并沒(méi)有很慢斜做,但是也想看一看有沒(méi)有優(yōu)化的空間。
在 didFinishLaunchingWithOptions
方法里我們一般都有以下的邏輯:
初始化第三方 SDK
配置 APP 運(yùn)行需要的環(huán)境
自己的一些工具類的初始化
...
這里主要參考[iOS]一次立竿見(jiàn)影的啟動(dòng)時(shí)間優(yōu)化
從優(yōu)化圖可以看到湾揽,我的應(yīng)用的跳轉(zhuǎn)邏輯是 打開(kāi)
-> 廣告頁(yè)
-> 首頁(yè)
瓤逼,首頁(yè)的UI 架構(gòu)是:
但是如果 UI 架構(gòu)如上,并且在didFinishLaunchingWithOptions
里面設(shè)置了根視圖
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSLog(@"didFinishLaunchingWithOptions 開(kāi)始執(zhí)行");
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
TestTabBarController *tabBarVc = [TestTabBarController new];
self.window.rootViewController = tabBarVc;
[self.window makeKeyAndVisible];
NSLog(@"didFinishLaunchingWithOptions 跑完了");
return YES;
}
然后我們來(lái)到 TestTabBarController
里的 viewDidLoad
方法里進(jìn)行它的 viewControllers
的設(shè)置库物,然后再進(jìn)入到每個(gè) viewController
的 viewDidLoad
方法里進(jìn)行更多的初始化操作霸旗。那么你覺(jué)得從 didFinishLaunchingWithOptions
到最后顯示展示的 viewController
的 viewDidLoad
這些方法的執(zhí)行順序是怎么樣的呢?
didFinishLaunchingWithOptions 開(kāi)始執(zhí)行
開(kāi)始加載 TestTabBarController 的 viewDidLoad
didFinishLaunchingWithOptions 跑完了
開(kāi)始加載 TestViewController 的 viewDidLoad, 然后執(zhí)行一堆初始化的操作
在TestTabBarController
中操作了 TestViewController
的 view
的話戚揭,那么調(diào)用順序?qū)?huì)是這樣:
didFinishLaunchingWithOptions 開(kāi)始執(zhí)行
開(kāi)始加載 TestTabBarController 的 viewDidLoad
開(kāi)始加載 TestViewController 的 viewDidLoad, 然后執(zhí)行一堆初始化的操作
didFinishLaunchingWithOptions 跑完了
這樣的問(wèn)題就是當(dāng)我們把界面的初始化诱告、網(wǎng)絡(luò)請(qǐng)求、數(shù)據(jù)解析民晒、視圖渲染等操作放在了viewDidLoad
方法里精居,這樣一來(lái)每次啟動(dòng) APP 的時(shí)候,在用戶看到第一個(gè)頁(yè)面之前潜必,我們要把這些事件全部都處理完靴姿,才會(huì)進(jìn)入到視圖渲染階段。
一般來(lái)說(shuō)刮便,我們放到didFinishLaunchingWithOptions
執(zhí)行的代碼空猜,有很多初始化操作,如日志恨旱,統(tǒng)計(jì)辈毯,SDK配置等。盡量做到只放必需的搜贤,其他的可以延遲到MainViewController
展示完成viewDidAppear
以后谆沃。
* 日志、統(tǒng)計(jì)等必須在 APP 一啟動(dòng)就最先配置的事件
* 項(xiàng)目配置仪芒、環(huán)境配置唁影、用戶信息的初始化 、推送掂名、IM等事件
* 其他 SDK 和配置事件
- 第一類据沈,必須第一時(shí)間啟動(dòng),仍然把它留在
didFinishLaunchingWithOptions
里啟動(dòng)饺蔑。 - 第二類锌介,這些功能在用戶進(jìn)入 APP 主體的之前是必須要加載完的,我把他放到廣告頁(yè)面的
viewDidAppear
啟動(dòng)。 - 第三類孔祸,由于啟動(dòng)時(shí)間不是必須的隆敢,所以我們可以放在第一個(gè)界面的
viewDidAppear
方法里,這里完全不會(huì)影響到啟動(dòng)時(shí)間崔慧。
這是優(yōu)化后的啟動(dòng)時(shí)間
優(yōu)化思路
梳理各個(gè)三方庫(kù)拂蝎,找到可以延遲加載的庫(kù),做延遲加載處理惶室,比如放到首頁(yè)控制器的viewDidAppear方法里温自。
梳理業(yè)務(wù)邏輯,把可以延遲執(zhí)行的邏輯拇涤,做延遲執(zhí)行處理捣作。比如檢查新版本、注冊(cè)推送通知等邏輯鹅士。
避免復(fù)雜/多余的計(jì)算券躁。
避免在首頁(yè)控制器的viewDidLoad和viewWillAppear做太多事情,這2個(gè)方法執(zhí)行完掉盅,首頁(yè)控制器才能顯示也拜,部分可以延遲創(chuàng)建的視圖應(yīng)做延遲創(chuàng)建/懶加載處理。
采用性能更好的API趾痘。
首頁(yè)控制器用純代碼方式來(lái)構(gòu)建慢哈。
另:[iOS]一次立竿見(jiàn)影的啟動(dòng)時(shí)間優(yōu)化 提到了使用一個(gè)工具類來(lái)管理的方法,可以比較方便的管理優(yōu)化永票。
總結(jié)
性價(jià)比最高的優(yōu)化階段就是t2
的一些邏輯整理卵贱,盡量將不需要的耗時(shí)操作延遲到首屏展示之后執(zhí)行。
同時(shí)一般來(lái)說(shuō)侣集,優(yōu)化應(yīng)該在項(xiàng)目完成穩(wěn)定之后進(jìn)行键俱,避免過(guò)早優(yōu)化.
參考: