前言
當(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)鏈接庫(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è)頁面加載完成稱為 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)庫(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文件,
打開并讀取驗(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)庫(kù)
2. 移除不需要用到的類
3. 合并功能類似的類和擴(kuò)展
4. 盡量避免在+load方法里執(zhí)行的操作,可以推遲到+initialize方法中。
t2 時(shí)間的優(yōu)化分析
t2
使用了來自NewPan大大 的打點(diǎn)計(jì)時(shí)器BLStopwatch
可以看到,我的 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)是:
但是如果 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)化后的啟動(dòng)時(shí)間
優(yōu)化思路
梳理各個(gè)三方庫(kù),找到可以延遲加載的庫(kù),做延遲加載處理,比如放到首頁控制器的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)化.
參考: