前言
當(dāng)用戶按下home鍵的時(shí)候搓逾,iOS的App并不會(huì)馬上被kill掉,還會(huì)繼續(xù)存活若干時(shí)間。理想情況下,用戶點(diǎn)擊App的圖標(biāo)再次回來的時(shí)候,App幾乎不需要做什么阅束,就可以還原到退出前的狀態(tài)呼胚,繼續(xù)為用戶服務(wù)。這種持續(xù)存活的情況下啟動(dòng)App息裸,我們稱為熱啟動(dòng)蝇更,相對(duì)而言冷啟動(dòng)就是App被kill掉以后一切從頭開始啟動(dòng)的過程。我們這里只討論App冷啟動(dòng)的情況呼盆。
對(duì)于冷啟動(dòng)來說年扩,啟動(dòng)時(shí)間是指從用戶點(diǎn)擊 APP 那一刻開始到用戶看到第一個(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)鏈接庫加載器dyld(dynamic loader)
1.3. dyld遞歸加載應(yīng)用所有依賴的dylib(dynamic library 動(dòng)態(tài)鏈接庫)
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è)頁面加載完成稱為 t2饭宾。
t1 時(shí)間的優(yōu)化分析
t1部分主要參考自APP啟動(dòng)優(yōu)化的一次實(shí)踐
其中 t1蘋果提供了內(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)庫用了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文件,
打開并讀取驗(yàn)證有效性净嘀,找到代碼簽名注冊(cè)到內(nèi)核报咳,
最后對(duì)dylib的每個(gè)segment調(diào)用mmap()。
rebase/bind
dylib加載完成之后挖藏,它們處于相互獨(dú)立的狀態(tài)暑刃,需要綁定起來。
在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做的是查詢符號(hào)表籽御,設(shè)置指向鏡像外部的指針练慕,性能消耗主要在CPU計(jì)算惰匙。
OC setup
OC的runtime需要維護(hù)一張類名與類的方法列表的全局表。
dyld做了如下操作:
對(duì)所有聲明過的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)庫
2. 移除不需要用到的類
3. 合并功能類似的類和擴(kuò)展
4. 盡量避免在+load方法里執(zhí)行的操作奥此,可以推遲到+initialize方法中。
t2 時(shí)間的優(yōu)化分析
t2使用了來自NewPan大大 的打點(diǎn)計(jì)時(shí)器BLStopwatch
檢測(cè)耗時(shí)
可以看到雁比,我的 APP 加載時(shí)間并沒有很慢稚虎,但是也想看一看有沒有優(yōu)化的空間。
在 didFinishLaunchingWithOptions 方法里我們一般都有以下的邏輯:
初始化第三方 SDK
配置 APP 運(yùn)行需要的環(huán)境
自己的一些工具類的初始化
...
這里主要參考[iOS]一次立竿見影的啟動(dòng)時(shí)間優(yōu)化
從優(yōu)化圖可以看到偎捎,我的應(yīng)用的跳轉(zhuǎn)邏輯是 打開 -> 廣告頁 -> 首頁蠢终,首頁的UI 架構(gòu)是:
UITabBarC管理一堆 UINavigationC
但是如果 UI 架構(gòu)如上,并且在didFinishLaunchingWithOptions里面設(shè)置了根視圖
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSLog(@"didFinishLaunchingWithOptions 開始執(zhí)行");
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
TestTabBarController *tabBarVc = [TestTabBarController new];
self.window.rootViewController = tabBarVc;
[self.window makeKeyAndVisible];
NSLog(@"didFinishLaunchingWithOptions 跑完了");
return YES;
}
然后我們來到 TestTabBarController 里的 viewDidLoad方法里進(jìn)行它的 viewControllers 的設(shè)置茴她,然后再進(jìn)入到每個(gè) viewController 的 viewDidLoad 方法里進(jìn)行更多的初始化操作寻拂。那么你覺得從 didFinishLaunchingWithOptions 到最后顯示展示的 viewController 的 viewDidLoad 這些方法的執(zhí)行順序是怎么樣的呢?
didFinishLaunchingWithOptions 開始執(zhí)行
開始加載 TestTabBarController 的 viewDidLoad
didFinishLaunchingWithOptions 跑完了
開始加載 TestViewController 的 viewDidLoad, 然后執(zhí)行一堆初始化的操作
在TestTabBarController 中操作了 TestViewController 的 view 的話丈牢,那么調(diào)用順序?qū)?huì)是這樣:
didFinishLaunchingWithOptions 開始執(zhí)行
開始加載 TestTabBarController 的 viewDidLoad
開始加載 TestViewController 的 viewDidLoad, 然后執(zhí)行一堆初始化的操作
didFinishLaunchingWithOptions 跑完了
這樣的問題就是當(dāng)我們把界面的初始化祭钉、網(wǎng)絡(luò)請(qǐng)求、數(shù)據(jù)解析己沛、視圖渲染等操作放在了viewDidLoad 方法里慌核,這樣一來每次啟動(dòng) APP 的時(shí)候,在用戶看到第一個(gè)頁面之前申尼,我們要把這些事件全部都處理完垮卓,才會(huì)進(jìn)入到視圖渲染階段。
一般來說师幕,我們放到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 主體的之前是必須要加載完的,我把他放到廣告頁面的viewDidAppear啟動(dòng)泽台。
第三類什荣,由于啟動(dòng)時(shí)間不是必須的,所以我們可以放在第一個(gè)界面的 viewDidAppear 方法里怀酷,這里完全不會(huì)影響到啟動(dòng)時(shí)間稻爬。
優(yōu)化后
這是優(yōu)化后的啟動(dòng)時(shí)間
優(yōu)化思路
梳理各個(gè)三方庫,找到可以延遲加載的庫蜕依,做延遲加載處理桅锄,比如放到首頁控制器的viewDidAppear方法里。
梳理業(yè)務(wù)邏輯样眠,把可以延遲執(zhí)行的邏輯友瘤,做延遲執(zhí)行處理。比如檢查新版本吹缔、注冊(cè)推送通知等邏輯商佑。
避免復(fù)雜/多余的計(jì)算。
避免在首頁控制器的viewDidLoad和viewWillAppear做太多事情厢塘,這2個(gè)方法執(zhí)行完茶没,首頁控制器才能顯示,部分可以延遲創(chuàng)建的視圖應(yīng)做延遲創(chuàng)建/懶加載處理晚碾。
采用性能更好的API抓半。
首頁控制器用純代碼方式來構(gòu)建。
另:[iOS]一次立竿見影的啟動(dòng)時(shí)間優(yōu)化 提到了使用一個(gè)工具類來管理的方法格嘁,可以比較方便的管理優(yōu)化笛求。
總結(jié)
性價(jià)比最高的優(yōu)化階段就是t2的一些邏輯整理,盡量將不需要的耗時(shí)操作延遲到首屏展示之后執(zhí)行糕簿。
同時(shí)一般來說探入,優(yōu)化應(yīng)該在項(xiàng)目完成穩(wěn)定之后進(jìn)行,避免過早優(yōu)化.