@NewPan 貝聊科技 iOS 菜鳥工程師
之前公司的 UI 設計師和我們提過好幾次啟動時間的事情,當時在開發業務,所以沒有時間去做這件事。最近發完版本,終於有時間搞一搞啟動時間了。
一般而言,啟動時間是指從使用者點選 APP 那一刻開始到使用者看到第一個介面這中間的時間。我們進行優化的時候,我們將啟動時間分為 pre-main
時間和 main
函式到第一個介面渲染完成時間這兩個部分。
為什麼這麼劃分呢?大家都知道 APP 的入口是 main
函式,在 main
之前,我們自己的程式碼是不會執行的。而進入到 main
函式以後,我們的程式碼都是從
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;複製程式碼
開始執行的,所以很明顯,優化這兩部分的思路是不一樣的。
為了方便起見,我們將 pre-main
時間成為 t1
時間,而將main
函式到第一個介面渲染完成這段時間稱為 t2
時間。
01.磨刀不誤砍柴工
我們先來看第一部分,也就是從 main
函式到第一個介面渲染完成這段時間。在開始之前,我們先來磨練一個我們自己的工具。
生活中,我們計量一段時間一般是用計時器。這裡我們要想知道哪些操作,或者說哪些程式碼是耗時的,我們也需要一個打點計時器。用過 profile
的朋友都知道這個工具很強大,可以使用它來分析出哪些程式碼是耗時的。但是它不夠靈活,我們來看一下我們的這個計時器應該怎麼設計。
如上圖所示,在時間軸上,我們從 start 開始打點計時,然後我們在第一個小紅旗那裡打了一個點,記錄這段程式碼的耗時,然後又在第二個小紅旗那裡打了一個點,記錄這中間程式碼的耗時。然後在結束的地方打一個點,然後把所有打點的結果展示出來。同時,我們為每段計時加上標註,用來區分這段時間是執行了什麼操作花費的時間。這樣一來,我們就能快速精準的知道究竟是誰拖慢了啟動。
02.定位元凶
下面這張截圖是貝聊老師端沒有開始優化的耗時,因為涉及到公司具體的業務,所以我將部分資訊加了遮擋。藉助於我們的工具,我們可以定位任何一行程式碼的耗時。
我們看 t2
耗時那裡,總共花費了 6.361
秒,這是從 didFinishLaunchingWithOptions
到第一個介面渲染出來花費的時間。從這個結果來看,我們的啟動時間的優化已經到了刻不容緩的地步了。
再仔細分析一下上面的結果, t2
時間也分為了兩個部分,didFinishLaunchingWithOptions
花了 4.010
秒,第一個頁面渲染耗時花了 2.531
秒。好,看樣子大魔頭住在 didFinishLaunchingWithOptions
這個方法裡,另外,第一頁面的渲染中也有不少問題。下面我們分別展開。
02.1.didFinishLaunchingWithOptions
上面說到大魔頭住在 didFinishLaunchingWithOptions
,現在我們仔細看一下 didFinishLaunchingWithOptions
方法裡的程式碼耗時,有兩行程式碼的耗時居然為一秒以上,而且耗時最多的居然有 1.620
秒之多。
其實 didFinishLaunchingWithOptions
方法裡我們一般都有以下的邏輯:
- 初始化第三方 SDK
- 配置 APP 執行需要的環境
- 自己的一些工具類的初始化
- ...
02.2.第一個頁面渲染
如果我們的 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 的時候,在使用者看到第一個頁面之前,我們要把這些事件全部都處理完,才會進入到檢視渲染階段。
03.解決策略
上面分析了拖慢 t2
的兩個因素,它們是 didFinishLaunchingWithOptions
裡面的初始化以及第一個頁面渲染耗時。對於這兩個不同的方面,我們的優化思路也是不一樣的。
03.1.didFinishLaunchingWithOptions
對於 didFinishLaunchingWithOptions
,這裡面的初始化是必須執行的,但是我們可以適當的根據功能的不同對應的適當延遲啟動的時機。對於我們專案,我將初始化分為三個型別:
- 日誌、統計等必須在 APP 一起動就最先配置的事件
- 專案配置、環境配置、使用者資訊的初始化 、推送、IM等事件
- 其他 SDK 和配置事件
對於第一類,由於這類事件的特殊性,所以必須第一時間啟動,仍然把它留在 didFinishLaunchingWithOptions
裡啟動。第二類事件,這些功能在使用者進入 APP 主體的之前是必須要載入完的,所以我們可以把它放在第二批,也就是使用者已經看到廣告頁面,再進行廣告倒數計時的時候再啟動。第三類事件,由於不是必須的,所以我們可以放在第一個介面渲染完成以後的 viewDidAppear
方法裡,這裡完全不會影響到啟動時間。
就這樣,進行過這一輪優化以後,我們的 t2
事件就從 6 秒多
降到 2 秒多
。
03.2.第一個頁面渲染
我們的思路是這樣的,使用者點選 APP,我先儘快把廣告頁面載入出來。這樣,使用者就不會覺得啟動慢了,同時我們可以在廣告讀秒的過程中進行第二批啟動事件的載入,這個載入使用者也感覺不到。但還沒完,等會廣告展示完,切到主 APP 的時候,如果一系列 viewDidLoad
裡方法裡有很多耗時的操作,那使用者還是會感覺到卡頓。
所以對於第一個頁面渲染的優化思路就是,先立馬展示一個空殼的 UI 給使用者,然後在 viewDidAppear
方法裡進行資料載入解析渲染等一系列操作,這樣一來,使用者已經看到介面了,就不會覺得是啟動慢,這個時候的等待就變成等待資料請求了,這樣就把這部分時間轉嫁出去了。
經過這兩輪優化,我們的 t2
時間就從 6 秒多
變成了 0.1 秒不到
,也即是總共砍掉了 6 秒多
的啟動時間。
03.3.總結
為此,我專門建了一個類來負責啟動事件,為什麼呢?如果不這麼做,那麼此次優化以後,以後再引入第三方的時候,別的同事可能很直覺的就把第三方的初始化放到了 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複製程式碼
04. 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%都是帥哥美女,再也不用單身了”。