1. 程式人生 > IOS開發 >看到這篇啟動優化,讓你的App有順滑無比的啟動速度~~

看到這篇啟動優化,讓你的App有順滑無比的啟動速度~~

為什麼要做啟動優化

1.APP的啟動速度是直接影響使用者體驗的關鍵因素 2.隨著APP的迭代和程式設計師的懈怠,三方庫的依賴越來越多,自定義的category越來越多,重複的方法越來越多,會直接影響APP的啟動時間

APP的啟動時間指的是什麼

TA(App總啟動時間) = T1(main()之前的載入時間) + T2(main()之後的載入時間)

T1 = 系統dylib(動態連結庫)和自身App可執行檔案的載入

T2 = main方法執行之後到Delegate類中的didFinishLaunchingWithOptions方法執行結束前這段時間(Z這段時間主要是APP在構建第一個介面,並完成渲染展示)

你的APP啟動時間合格嗎?

使用砸殼和MokeyApp這種利器,我們先看幾款APP的啟動時間

Total pre-main time: 642.93 milliseconds (100.0%)
         dylib loading time: 201.45 milliseconds (31.3%)
        rebase/binding time: 131.51 milliseconds (20.4%)
            ObjC setup time:  67.38 milliseconds (10.4%)
           initializer time: 242.33 milliseconds (37.6%)
           slowest intializers :
             libSystem.B.dylib :   4.45 milliseconds (0.6%)
    libMainThreadChecker.dylib :  28.27 milliseconds (4.3%)
          libglInterpose.dylib :  63.57 milliseconds (9.8%)
         libMTLInterpose.dylib :  14.84 milliseconds (2.3%)
                  RevealServer :  48.39 milliseconds (7.5%)
        libdingdingDylib.dylib :  30.33 milliseconds (4.7%)
                      DT :  95.09 milliseconds (14.7%)

複製程式碼
Total pre-main time: 1.1 seconds (100.0%)
         dylib loading time: 230.74 milliseconds (20.8%)
        rebase/binding time:  66.30 milliseconds (5.9%)
            ObjC setup time:  77.95 milliseconds (7.0%)
           initializer time: 732.20 milliseconds (66.1%)
           slowest intializers :
             libSystem.B.dylib :   7.86 milliseconds (0.7%)
    libMainThreadChecker.dylib :  25.69 milliseconds (2.3%)
          libglInterpose.dylib : 307.84 milliseconds (27.7%)
         libMTLInterpose.dylib :  77.95 milliseconds (7.0%)
  libViewDebuggerSupport.dylib :  22.51 milliseconds (2.0%)
                  RevealServer :  94.45 milliseconds (8.5%)
              libKKDylib.dylib :  38.06 milliseconds (3.4%)
                       K : 321.95 milliseconds (29.0%)
複製程式碼
Total pre-main time: 873.18 milliseconds (100.0%)
         dylib loading time: 236.80 milliseconds (27.1%)
        rebase/binding time: 198.80 milliseconds (22.7%)
            ObjC setup time:  98.38 milliseconds (11.2%)
           initializer time: 338.96 milliseconds (38.8%)
           slowest intializers :
             libSystem.B.dylib :   9.01 milliseconds (1.0%)
    libMainThreadChecker.dylib :  26.69 milliseconds (3.0%)
          libglInterpose.dylib : 111.85 milliseconds (12.8%)
         libMTLInterpose.dylib :  42.68 milliseconds (4.8%)
  libViewDebuggerSupport.dylib :  19.92 milliseconds (2.2%)
             TBSharedFramework :  43.33 milliseconds (4.9%)
                  RevealServer :  40.34 milliseconds (4.6%)
              libTBDylib.dylib :  29.17 milliseconds (3.3%)
                            TB :  50.24 milliseconds (5.7%)
複製程式碼

我們可以看到各種APP的啟動時間千差萬別,當啟動時間大於n 秒的時候使用者會感覺明顯的等待。當然這個啟動時間到底為多少合適因人而異,不過除了某些方面,APP冷啟動速度這種東西當然是越快越好

如何檢視自己APP的啟動時間呢?

只需要在 Edit scheme -> Run -> Environment Variables 中將環境變數 DYLD_PRINT_STATISTICS 設為 1,就可以看到 main 之前各個階段的時間消耗

Total pre-main time: 537.96 milliseconds (100.0%)
         dylib loading time: 280.17 milliseconds (52.0%)
        rebase/binding time:  26.71 milliseconds (4.9%)
            ObjC setup time:  16.39 milliseconds (3.0%)
           initializer time: 214.18 milliseconds (39.8%)
           slowest intializers :
             libSystem.B.dylib :   3.62 milliseconds (0.6%)
    libMainThreadChecker.dylib :  21.47 milliseconds (3.9%)
          libglInterpose.dylib :  62.91 milliseconds (11.6%)
         libMTLInterpose.dylib :  30.22 milliseconds (5.6%)
                           Arm : 122.19 milliseconds (22.7%)
複製程式碼

當然我們也可以獲取更詳細的時間,只需將環境變數 DYLD_PRINT_STATISTICS_DETAILS 設為 1 就可以

  total time: 5.2 seconds (100.0%)
  total images loaded:  467 (440 from dyld shared cache)
  total segments mapped: 73,into 6990 pages with 292 pages pre-fetched
  total images loading time: 4.7 seconds (91.4%)
  total load time in ObjC:  12.35 milliseconds (0.2%)
  total debugger pause time: 4.4 seconds (84.3%)
  total dtrace DOF registration time:   0.34 milliseconds (0.0%)
  total rebase fixups:  334,609
  total rebase fixups time:  25.78 milliseconds (0.4%)
  total binding fixups: 576,887
  total binding fixups time: 199.48 milliseconds (3.7%)
  total weak binding fixups time:   2.79 milliseconds (0.0%)
  total redo shared cached bindings time: 199.53 milliseconds (3.8%)
  total bindings lazily fixed up: 0 of 0
  total time in initializers and ObjC +load: 209.92 milliseconds (3.9%)
                         libSystem.B.dylib :   3.53 milliseconds (0.0%)
                libMainThreadChecker.dylib :  20.80 milliseconds (0.3%)
                      libglInterpose.dylib :  64.61 milliseconds (1.2%)
                     libMTLInterpose.dylib :  22.88 milliseconds (0.4%)
                                  linphone :  15.46 milliseconds (0.2%)
                                       Arm : 116.17 milliseconds (2.2%)
total symbol trie searches:    1429688
total symbol table binary searches:    0
total images defining weak symbols:  52
total images using weak symbols:  121
複製程式碼

根據所見即所得的原則,我們可以看到APP的冷啟動大概需要四個步驟: dylib loading、rebase/binding、ObjC setup、initializers,這四個步驟對應的是我們上邊提到的T1時間(main()之前的載入時間) 那麼我們可以認為將上邊的四個步驟優化一下,我們就可以提高部分APP的啟動速度了。那麼這四步分別做了什麼呢?這裡讓我們先盜個圖...

提高main()函式之前的載入時間?

瞭解完畢mian()函式之前載入的步驟後,我們可以簡單的分析出影響T1時間的各種因素:

1.動態庫載入越多,啟動越慢。
2.ObjC類,方法越多,啟動越慢。
3.ObjC的+load越多,啟動越慢。
4.C的constructor函式越多,啟動越慢。
5.C++靜態物件越多,啟動越慢。
複製程式碼

APP啟動T1階段對症下藥

1.刪除無用程式碼,合併一些同樣功能的類

刪除類的方法:ocjc_cover

通過對Mach-O檔案的瞭解,可以知道__TEXT:__objc_methname:中包含了程式碼中的所有方法,而__DATA__objc_selrefs中則包含了所有被使用的方法的引用,通過取兩個集合的差集就可以得到所有未被使用的程式碼

def referenced_selectors(path):
    re_sel = re.compile("__TEXT:__objc_methname:(.+)") //獲取所有方法
    refs = set()
    lines = os.popen("/usr/bin/otool -v -s __DATA __objc_selrefs %s" % path).readlines() # ios & mac //真正被使用的方法
    for line in lines:
        results = re_sel.findall(line)
        if results:
            refs.add(results[0])
    return refs
}
複製程式碼

對Mach-O檔案的組成感興趣的同學可以閱讀以下這篇文章 Mach-O

2.+load優化

大部分APP因為業務需求或者一些奇淫巧技的關係,多多少少的回使用+load方法來執行一些操作,但是並不是每個方法都需要在+load那麼早。部分操作可以延遲到+initialize中

3.減少framework的使用,動態連結比較耗時

4.儘量不要用C++虛擬函式(建立虛擬函式表有開銷)

APP啟動T2階段

這裡我們再重提一下我們對於T2階段的定義

T2 = main方法執行之後到Delegate類中的didFinishLaunchingWithOptions方法執行結束前這段時間(Z這段時間主要是APP在構建第一個介面,並完成渲染展示)
複製程式碼

而在這個階段隨著業務的開發,我們可能會一股腦的把所有問題都堆在這裡


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

    [self setupNetwork];
    [self startTabbarControlService];
    [self startUserDefaultService];
    [self startPANEL];
    [self startUIToastService];
    [self startRootRouter];
    [self startBackgroundRefreshService];
    [self start3DTouchService:application];
    [self setUpNotification];
    [self startDebuggerService];
     return YES;
}

複製程式碼

而過多無用的啟動項很明顯會拖累我們App的啟動速度,而我們需要做的便是分析整個專案的業務需求以及架構設計,將所有的啟動項分門別類,然後在不同的階段分別啟動

// AppDelegate
- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
    // 效能檢測以及Crash
    [self setUPCrashAndPerformanceModule]
    // 統計資訊上報
    [self setUPStatisticsInfoModule]
}
複製程式碼
// AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 網路
    [self setupNetwork];
    // 基礎資訊
    [self setUPBaseInfo];
    // 基礎依賴的SDK
    [self setUPBaseSDKMoudle];

    return YES;
}

複製程式碼
// MainVC
- (void)viewDidLoad {
    [super viewDidLoad];
    // debug面板
    [self setUPDebuggerService];
    // 自定義配置資訊
    [self setCustomConfiguration];
    // 等等
}

複製程式碼

上面只是單純羅列了一下具體操作的方法,但是App的業務千差萬別,我們還需要具體問題具體分析。

APP啟動T2階段對症下藥

1.專案自啟動

這裡可能有些同學要提問了,我們專案做了元件化,不同的元件可能在不同的專案裡啟動時間不同,這個問題怎麼解決呢?如何做到元件可插拔呢?所以我們需要用到一個叫做"專案自啟動"的技術,而這裡核心用到的"_DATA"段,"_DATA"可以涵蓋App啟動的所有階段(包括main函式之前)

具體的原理呢,大家可以直接閱讀這篇文章 利用__attribute__((section()))構建初始化函式表,這裡就不再做贅述了

這裡為大家提供OC版本的自啟動項工具ONLDynamicLoader

//
//  ONLDynamicLoader.h
//  ONLDynamicLoader
//
//  Created by only on 2019/10/14.
//  Copyright © 2019 only. All rights reserved.
//

#import <Foundation/Foundation.h>

static char * LEVEL_A = "LEVEL_A";
static char * LEVEL_B = "LEVEL_B";
static char * LEVEL_C = "LEVEL_C";

typedef void (*ONLDynamicLoaderInjectFunction)(void);

#define CRDYML_SEGMENTNAME "__DATA"
#define CRDYML_ATTRIBUTE(sectionName) __attribute((used,section(CRDYML_SEGMENTNAME "," #sectionName )))

#define CRDYML_FUNCTIONS_EXPORT_BEGIN(KEY) \
static void CRDYML_INJECT_##KEY##_FUNCTION(void){

#define CRDYML_FUNCTIONS_EXPORT_END(KEY) \
} \
static ONLDynamicLoaderInjectFunction dymlLoader##KEY##function CRDYML_ATTRIBUTE(KEY) = CRDYML_INJECT_##KEY##_FUNCTION;


NS_ASSUME_NONNULL_BEGIN

@interface ONLDynamicLoader : NSObject

+ (void)executeFunctionsForKey:(char *)key;

@end

NS_ASSUME_NONNULL_END

複製程式碼
//
//  ONLDynamicLoader.m
//  ONLDynamicLoader
//
//  Created by only on 2019/10/14.
//  Copyright © 2019 only. All rights reserved.
//

#import "ONLDynamicLoader.h"
#include <mach-o/getsect.h>
#include <mach-o/loader.h>
#include <mach-o/dyld.h>
#include <dlfcn.h>

static void ONLDynamicLoader_invoke_method(void *key){
    Dl_info info;
    int ret = dladdr(ONLDynamicLoader_invoke_method,&info);
    if(ret == 0){
        // fatal error
    }
#ifndef __LP64__
    const struct mach_header *mhp = (struct mach_header*)info.dli_fbase;
    unsigned long size = 0;
    uint32_t *memory = (uint32_t*)getsectiondata(mhp,QWLoadableSegmentName,QWLoadableSectionName,& size);
#else /* defined(__LP64__) */
    const struct mach_header_64 *mhp = (struct mach_header_64*)info.dli_fbase;
    unsigned long size = 0;
    uint64_t *memory = (uint64_t*)getsectiondata(mhp,CRDYML_SEGMENTNAME,key,&size);
#endif /* defined(__LP64__) */
    
    for(int idx = 0; idx < size/sizeof(void*); ++idx){
        ONLDynamicLoaderInjectFunction func = (ONLDynamicLoaderInjectFunction)memory[idx];
        func(); //crash tofix
    }
}


@implementation ONLDynamicLoader

+ (void)executeFunctionsForKey:(char *)key
{
    ONLDynamicLoader_invoke_method(key);
}
// 示例 編譯階段註冊的啟動項
CRDYML_FUNCTIONS_EXPORT_BEGIN(LEVEL_A)
NSLog(@"=====LEVEL_A==========");
CRDYML_FUNCTIONS_EXPORT_END(LEVEL_A)

CRDYML_FUNCTIONS_EXPORT_BEGIN(LEVEL_B)
NSLog(@"=====LEVEL_B==========");
CRDYML_FUNCTIONS_EXPORT_END(LEVEL_B)

CRDYML_FUNCTIONS_EXPORT_BEGIN(LEVEL_C)
NSLog(@"=====LEVEL_C==========");
CRDYML_FUNCTIONS_EXPORT_END(LEVEL_C)

@end
複製程式碼

利用這個工具我們可以很輕鬆的做到在合適的階段宣告啟動項的啟動階段

// AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 所有註冊到A的啟動項將會在這個階段啟動
    [ONLDynamicLoader executeFunctionsForKey:LEVEL_A];
    return YES;
}
複製程式碼
// MainVC
- (void)viewDidLoad {
    [super viewDidLoad];
    // 所有註冊到B的啟動項將會在這個階段啟動
    [ONLDynamicLoader executeFunctionsForKey:LEVEL_B];
    
}
複製程式碼

其他的元件在內部註冊自己需要的啟動階段

//
//  Created by only on 2019/10/14.
//  Copyright © 2019 only. All rights reserved.
//

#import "ONLMoudleA.h"
#import "ONLDynamicLoader.h"


@implementation ONLMoudleA

+ (instancetype)shareMoudeleA {
    static id shareMoudeleA = nil;
    static dispatch_once_t onceToken = 0;
    dispatch_once(&onceToken,^{
        // alloc & init work
        shareMoudeleA = [[self alloc]init];
    });
    
    return shareMoudeleA;
}

- (void)setUP{
    NSLog(@"ONLMoudleA 啟動了");
}
// 根據實際的專案需求做到可插拔、解耦合、可複用等等等的問題
CRDYML_FUNCTIONS_EXPORT_BEGIN(LEVEL_B)
[[ONLMoudleA shareMoudeleA]setUP];
CRDYML_FUNCTIONS_EXPORT_END(LEVEL_B)

@end

複製程式碼

而到這裡App的啟動優化其實已經完成了2/3,有些同學可能會問了WTF說好的

TA(App總啟動時間) = T1(main()之前的載入時間) + T2(main()之後的載入時間)
複製程式碼

怎麼還有1/3沒有完成呢???其實呢,還有一個T3時間無論是閃屏頁和首頁的網路請求還是資料載入都是耗時操作,只有使用者眼睛能夠看到介面並且開始操作才算是真正的完成App啟動。即:

TA(App總啟動時間) = T1(main()之前的載入時間) + T2(main()之後的載入時間) +T3(首頁資料載入+閃屏頁資料同步)
複製程式碼

針對T3的優化較為複雜了,需要針對介面UI等等方面去考慮,有興趣的同學可以參考分析支付寶的做法,嗯,後期會補上。(一個巨型的App竟然有著鬼一樣的啟動速度!!)

嗯,這篇部落格就暫到這裡,寫的比較快,有錯誤的地方或者理解錯誤的地方希望大家指正,一起進步一起學習!

參考資料

1.iOS App冷啟動治理:來自美團外賣的實踐

2.今日頭條iOS客戶端啟動速度優化

3.廖威雄: 利用__attribute__((section()))構建初始化函式表與Linux核心init的實現