[iOS]一次立竿見影的啓動時間優化

以前公司的 UI 設計師和咱們提過好幾回啓動時間的事情,當時在開發業務,因此沒有時間去作這件事。最近發完版本,終於有時間搞一搞啓動時間了。git

通常而言,啓動時間是指從用戶點擊 APP 那一刻開始到用戶看到第一個界面這中間的時間。咱們進行優化的時候,咱們將啓動時間分爲 pre-main 時間和 main 函數到第一個界面渲染完成時間這兩個部分。github

爲何這麼劃分呢?你們都知道 APP 的入口是 main 函數,在 main 以前,咱們本身的代碼是不會執行的。而進入到 main 函數之後,咱們的代碼都是從性能優化

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;

開始執行的,因此很明顯,優化這兩部分的思路是不同的。網絡

爲了方便起見,咱們將 pre-main 時間成爲 t1 時間,而將main 函數到第一個界面渲染完成這段時間稱爲 t2 時間。架構

01.磨刀不誤砍柴工

咱們先來看第一部分,也就是從 main 函數到第一個界面渲染完成這段時間。在開始以前,咱們先來磨練一個咱們本身的工具。app

生活中,咱們計量一段時間通常是用計時器。這裏咱們要想知道哪些操做,或者說哪些代碼是耗時的,咱們也須要一個打點計時器。用過 profile 的朋友都知道這個工具很強大,可使用它來分析出哪些代碼是耗時的。可是它不夠靈活,咱們來看一下咱們的這個計時器應該怎麼設計。框架

如上圖所示,在時間軸上,咱們從 start 開始打點計時,而後咱們在第一個小紅旗那裏打了一個點,記錄這段代碼的耗時,而後又在第二個小紅旗那裏打了一個點,記錄這中間代碼的耗時。而後在結束的地方打一個點,而後把全部打點的結果展現出來。同時,咱們爲每段計時加上標註,用來區分這段時間是執行了什麼操做花費的時間。這樣一來,咱們就能快速精準的知道到底是誰拖慢了啓動。ide

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 的設置,而後再進入到每一個 viewControllerviewDidLoad 方法裏進行更多的初始化操做。那麼你以爲從 didFinishLaunchingWithOptions 到最後顯示展現的 viewControllerviewDidLoad 這些方法的執行順序是怎麼樣的呢?

下面是我寫的一個 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 中操做 BLViewControllerview,若是咱們在BLTabBarController 中操做了 BLViewControllerview 的話,那麼調用順序將會是這樣:

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%都是帥哥美女,不再用單身了」。

個人文章集合索引

你還能夠關注我本身維護的簡書專題 iOS開發心得。這個專題的文章都是實打實的乾貨。

若是你有問題,除了在文章最後留言,還能夠在微博 @盼盼_HKbuy上給我留言,以及訪問個人 Github

做者:NewPan 連接:http://www.jianshu.com/p/c1734cbdf39b

相關文章
相關標籤/搜索