以前公司的 UI 設計師和咱們提過好幾回啓動時間的事情,當時在開發業務,因此沒有時間去作這件事。最近發完版本,終於有時間搞一搞啓動時間了。git
通常而言,啓動時間是指從用戶點擊 APP 那一刻開始到用戶看到第一個界面這中間的時間。咱們進行優化的時候,咱們將啓動時間分爲 pre-main
時間和 main
函數到第一個界面渲染完成時間這兩個部分。github
爲何這麼劃分呢?你們都知道 APP 的入口是 main
函數,在 main
以前,咱們本身的代碼是不會執行的。而進入到 main
函數之後,咱們的代碼都是從性能優化
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;
開始執行的,因此很明顯,優化這兩部分的思路是不同的。網絡
爲了方便起見,咱們將 pre-main
時間成爲 t1
時間,而將main
函數到第一個界面渲染完成這段時間稱爲 t2
時間。架構
咱們先來看第一部分,也就是從 main
函數到第一個界面渲染完成這段時間。在開始以前,咱們先來磨練一個咱們本身的工具。app
生活中,咱們計量一段時間通常是用計時器。這裏咱們要想知道哪些操做,或者說哪些代碼是耗時的,咱們也須要一個打點計時器。用過 profile
的朋友都知道這個工具很強大,可使用它來分析出哪些代碼是耗時的。可是它不夠靈活,咱們來看一下咱們的這個計時器應該怎麼設計。框架
如上圖所示,在時間軸上,咱們從 start 開始打點計時,而後咱們在第一個小紅旗那裏打了一個點,記錄這段代碼的耗時,而後又在第二個小紅旗那裏打了一個點,記錄這中間代碼的耗時。而後在結束的地方打一個點,而後把全部打點的結果展現出來。同時,咱們爲每段計時加上標註,用來區分這段時間是執行了什麼操做花費的時間。這樣一來,咱們就能快速精準的知道到底是誰拖慢了啓動。ide
下面這張截圖是貝聊老師端沒有開始優化的耗時,由於涉及到公司具體的業務,因此我將部分信息加了遮擋。藉助於咱們的工具,咱們能夠定位任何一行代碼的耗時。函數
咱們看 t2
耗時那裏,總共花費了 6.361
秒,這是從 didFinishLaunchingWithOptions
到第一個界面渲染出來花費的時間。從這個結果來看,咱們的啓動時間的優化已經到了刻不容緩的地步了。工具
再仔細分析一下上面的結果, t2
時間也分爲了兩個部分,didFinishLaunchingWithOptions
花了 4.010
秒,第一個頁面渲染耗時花了 2.531
秒。好,看樣子大魔頭住在 didFinishLaunchingWithOptions
這個方法裏,另外,第一頁面的渲染中也有很多問題。下面咱們分別展開。
didFinishLaunchingWithOptions
上面說到大魔頭住在 didFinishLaunchingWithOptions
,如今咱們仔細看一下 didFinishLaunchingWithOptions
方法裏的代碼耗時,有兩行代碼的耗時竟然爲一秒以上,並且耗時最多的竟然有 1.620
秒之多。
其實 didFinishLaunchingWithOptions
方法裏咱們通常都有如下的邏輯:
- 初始化第三方 SDK
- 配置 APP 運行須要的環境
- 本身的一些工具類的初始化
- ...
若是咱們的 UI 架構是上面這樣的話。而後咱們在 AppDelegate 裏寫下這麼一段代碼:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSLog(@"didFinishLaunchingWithOptions 開始執行"); self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; BLTabBarController *tabBarVc = [BLTabBarController new]; self.window.rootViewController = tabBarVc; [self.window makeKeyAndVisible]; NSLog(@"didFinishLaunchingWithOptions 跑完了"); return YES; }
而後咱們來到 BLTabBarController
裏的 viewDidLoad
方法裏進行它的 viewControllers
的設置,而後再進入到每一個 viewController
的 viewDidLoad
方法裏進行更多的初始化操做。那麼你以爲從 didFinishLaunchingWithOptions
到最後顯示展現的 viewController
的 viewDidLoad
這些方法的執行順序是怎麼樣的呢?
下面是我寫的一個 demo,用來展現加載的順序:
2017-08-15 10:46:57.860 Demo[1404:325698] didFinishLaunchingWithOptions 開始執行 2017-08-15 10:46:57.862 Demo[1404:325698] 開始加載 BLTabBarController 的 viewDidLoad 2017-08-15 10:46:57.874 Demo[1404:325698] didFinishLaunchingWithOptions 跑完了 2017-08-15 10:46:57.876 Demo[1404:325698] 開始加載 BLViewController 的 viewDidLoad, 而後執行一堆初始化的操做
上面的狀況是能保證咱們不在 BLTabBarController
中操做 BLViewController
的 view
,若是咱們在BLTabBarController
中操做了 BLViewController
的 view
的話,那麼調用順序將會是這樣:
2017-08-15 11:09:03.661 Demo[1458:349413] didFinishLaunchingWithOptions 開始執行 2017-08-15 11:09:03.663 Demo[1458:349413] 開始加載 BLTabBarController 的 viewDidLoad 2017-08-15 11:09:03.664 Demo[1458:349413] 開始加載 BLViewController 的 viewDidLoad, 而後執行一堆初始化的操做 2017-08-15 11:09:03.676 Demo[1458:349413] didFinishLaunchingWithOptions 跑完了
這是很可怕的一件事情,爲何呢?由於通常咱們都把界面的初始化、網絡請求、數據解析、視圖渲染等操做放在了 viewDidLoad
方法裏,這樣一來每次啓動 APP 的時候,在用戶看到第一個頁面以前,咱們要把這些事件所有都處理完,纔會進入到視圖渲染階段。
上面分析了拖慢 t2
的兩個因素,它們是 didFinishLaunchingWithOptions
裏面的初始化以及第一個頁面渲染耗時。對於這兩個不一樣的方面,咱們的優化思路也是不同的。
didFinishLaunchingWithOptions
對於 didFinishLaunchingWithOptions
,這裏面的初始化是必須執行的,可是咱們能夠適當的根據功能的不一樣對應的適當延遲啓動的時機。對於咱們項目,我將初始化分爲三個類型:
- 日誌、統計等必須在 APP 一塊兒動就最早配置的事件
- 項目配置、環境配置、用戶信息的初始化 、推送、IM等事件
- 其餘 SDK 和配置事件
對於第一類,因爲這類事件的特殊性,因此必須第一時間啓動,仍然把它留在 didFinishLaunchingWithOptions
裏啓動。第二類事件,這些功能在用戶進入 APP 主體的以前是必需要加載完的,因此咱們能夠把它放在第二批,也就是用戶已經看到廣告頁面,再進行廣告倒計時的時候再啓動。第三類事件,因爲不是必須的,因此咱們能夠放在第一個界面渲染完成之後的 viewDidAppear
方法裏,這裏徹底不會影響到啓動時間。
就這樣,進行過這一輪優化之後,咱們的 t2
事件就從 6 秒多
降到 2 秒多
。
咱們的思路是這樣的,用戶點擊 APP,我先儘快把廣告頁面加載出來。這樣,用戶就不會以爲啓動慢了,同時咱們能夠在廣告讀秒的過程當中進行第二批啓動事件的加載,這個加載用戶也感受不到。但還沒完,等會廣告展現完,切到主 APP 的時候,若是一系列 viewDidLoad
裏方法裏有不少耗時的操做,那用戶仍是會感受到卡頓。
因此對於第一個頁面渲染的優化思路就是,先立馬展現一個空殼的 UI 給用戶,而後在 viewDidAppear
方法裏進行數據加載解析渲染等一系列操做,這樣一來,用戶已經看到界面了,就不會以爲是啓動慢,這個時候的等待就變成等待數據請求了,這樣就把這部分時間轉嫁出去了。
通過這兩輪優化,咱們的 t2
時間就從 6 秒多
變成了 0.1 秒不到
,也便是總共砍掉了 6 秒多
的啓動時間。
爲此,我專門建了一個類來負責啓動事件,爲何呢?若是不這麼作,那麼這次優化之後,之後再引入第三方的時候,別的同事可能很直覺的就把第三方的初始化放到了 didFinishLaunchingWithOptions
方法裏,這樣長此以往, didFinishLaunchingWithOptions
又變得不堪重負,到時候又要專門花時間來作重複的優化。
下面是這個類的頭文件:
/** * 注意: 這個類負責全部的 didFinishLaunchingWithOptions 延遲事件的加載. * 之後引入第三方須要在 didFinishLaunchingWithOptions 裏初始化或者咱們本身的類須要在 didFinishLaunchingWithOptions 初始化的時候, * 要考慮儘可能少的啓動時間帶來好的用戶體驗, 因此應該根據須要減小 didFinishLaunchingWithOptions 裏耗時的操做. * 第一類: 好比日誌 / 統計等須要第一時間啓動的, 仍然放在 didFinishLaunchingWithOptions 中. * 第二類: 好比用戶數據須要在廣告顯示完成之後使用, 因此須要伴隨廣告頁啓動, 只須要將啓動代碼放到 startupEventsOnADTimeWithAppDelegate 方法裏. * 第三類: 好比直播和分享等業務, 確定是用戶能看到真正的主界面之後才須要啓動, 因此推遲到主界面加載完成之後啓動, 只須要將代碼放到 startupEventsOnDidAppearAppContent 方法裏. */ #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface BLDelayStartupTool : NSObject /** * 啓動伴隨 didFinishLaunchingWithOptions 啓動的事件. * 啓動類型爲:日誌 / 統計等須要第一時間啓動的. */ + (void)startupEventsOnAppDidFinishLaunchingWithOptions; /** * 啓動能夠在展現廣告的時候初始化的事件. * 啓動類型爲: 用戶數據須要在廣告顯示完成之後使用, 因此須要伴隨廣告頁啓動. */ + (void)startupEventsOnADTime; /** * 啓動在第一個界面顯示完(用戶已經進入主界面)之後能夠加載的事件. * 啓動類型爲: 好比直播和分享等業務, 確定是用戶能看到真正的主界面之後才須要啓動, 因此推遲到主界面加載完成之後啓動. */ + (void)startupEventsOnDidAppearAppContent; @end NS_ASSUME_NONNULL_END
下面是 .m
文件,這裏作了一層自動校驗,若是 30 秒
之後,這些啓動項有沒有被啓動的,就會在 DEBUG
環境下彈出警告信息。同時也會將那些沒有啓動的啓動項進行啓動。
#import "BLDelayStartupTool.h" static BOOL _isCalledStartupEventsOnAppDidFinishLaunchingWithOptions = NO; static BOOL _isCalledStartupEventsOnADTimeWithAppDelegate = NO; static BOOL _isCalledStartupEventsOnDidAppearAppContent = NO; const NSTimeInterval kBLDelayStartupEventsToolCheckCallTimeInterval = 30; @implementation BLDelayStartupTool + (void)load { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kBLDelayStartupEventsToolCheckCallTimeInterval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self checkStartupEventsDidLaunched]; }); } + (void)checkStartupEventsDidLaunched { NSString *alertString = @""; if (!_isCalledStartupEventsOnAppDidFinishLaunchingWithOptions) { alertString = [alertString stringByAppendingString:@"AppDidFinishLaunching, "]; [self startupEventsOnAppDidFinishLaunchingWithOptions]; } if (!_isCalledStartupEventsOnADTimeWithAppDelegate) { alertString = [alertString stringByAppendingString:@"ADTime, "]; [self startupEventsOnADTime]; } if (!_isCalledStartupEventsOnDidAppearAppContent) { alertString = [alertString stringByAppendingString:@"DidAppearAppContent"]; [self startupEventsOnDidAppearAppContent]; } if (alertString.length > 0) { #if DEBUG alertString = [alertString stringByAppendingString:@" 等延遲啓動項沒有啓動, 這會形成應用奔潰"]; UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"注意" message:alertString delegate:nil cancelButtonTitle:@"好的" otherButtonTitles:nil]; [alertView show]; #endif } } + (void)startupEventsOnAppDidFinishLaunchingWithOptions { _isCalledStartupEventsOnAppDidFinishLaunchingWithOptions = YES; } + (void)startupEventsOnADTime { _isCalledStartupEventsOnADTimeWithAppDelegate = YES; } + (void)startupEventsOnDidAppearAppContent { _isCalledStartupEventsOnDidAppearAppContent = YES; } @end
pre-main
時間上面已經將 t2
時間處理好了,接下來看看 pre-main
。
蘋果爲查看 pre-main
提供了支持,具體配置以下,配置的 key 爲:DYLD_PRINT_STATISTICS
。
還須要勾選下面這個選項:
而後再運行項目,Xcode
就會在控制檯輸出這部分 pre-main
的耗時:
Total pre-main time: 2.2 seconds (100.0%) dylib loading time: 1.0 seconds (45.2%) rebase/binding time: 100.05 milliseconds (4.3%) ObjC setup time: 207.21 milliseconds (9.0%) initializer time: 946.39 milliseconds (41.3%) slowest intializers : libSystem.B.dylib : 8.54 milliseconds (0.3%) libBacktraceRecording.dylib : 46.30 milliseconds (2.0%) libglInterpose.dylib : 187.42 milliseconds (8.1%) beiliao : 896.56 milliseconds (39.1%)
可是這部分不是那麼好處理,由於這部分主要是由如下幾個方面影響的:
- 用到的系統的動態庫的數量,好比
UIKit.framework
等- cocoapods 裏引用的第三方框架數量
- 項目中類的數量
load
方法中執行的代碼- 組件化
其餘還有,請大神補充。上面幾點中,咱們能作的也就是把全部類的 load
方法掃一遍,不要在這裏面執行耗時的操做。其餘的不是短期能改變的。
若是你想在這些方面有所突破的話,請看下面參考文章。
參考文章:
App Startup Time: Past, Present, and Future
iOS App 啓動性能優化
WWDC 之優化 App 啓動速度
iOS Dynamic Framework 對App啓動時間影響實測
優化 App 的啓動時間
下面這個連接是我全部文章的一個集合目錄。這些文章凡是涉及實現的,每篇文章中都有 Github 地址,Github 上都有源碼。若是某篇文章恰好在你的實際開發中幫到你,又或者提供一種不一樣的實現思路,讓你以爲有用,那就看看這句話 「堅持天天點讚的人,99%都是帥哥美女,不再用單身了」。
你還能夠關注我本身維護的簡書專題 iOS開發心得。這個專題的文章都是實打實的乾貨。
若是你有問題,除了在文章最後留言,還能夠在微博 @盼盼_HKbuy上給我留言,以及訪問個人 Github。
做者:NewPan 連接:http://www.jianshu.com/p/c1734cbdf39b