App啓動時間優化之二進制重排

App啓動時間優化之二進制重排

一、啓動時間的定義

  • 點擊app圖標到首頁數據加載完畢
  • 點擊app圖標到launch界面徹底消失的第一幀

咱們這裏按照第二種來去定義應用的啓動時間。node

因爲啓動動畫時長爲400ms,因此通常狀況下app的啓動最佳時間是400ms內
複製代碼

下面直接進入正題,其餘概念方面的東西很少作贅述。緩存

二、二進制重排

看了不少大佬的文章,抖音大佬主要講了二進制重排的大概原理和一些實現的思路,主要採用的是靜態掃描的方式去獲取函數符號,文章比較粗略。戳我👉🏻markdown

本記錄主要是經過此文章進行的實踐戳戳戳👉🏻這篇文章的做者包括概念性的東西都講述的很詳細。app

咱們不墨跡直接開始操做!函數

一、systemTrace工具使用

  • 一、先打開XCode的Instrument找到工具system trace

instruments.png

  • 二、選擇真機->all process 所有進程,爲了不手機內應用的內存緩存,先把應用刪除,並clean項目,再run systemTrace,而後再運行Xcode的app項目。等項目運行起來啓動頁結束出現應用的第一個頁面的時候,把systemTrace中止運行。

systemTrace.png

  • 三、找到運行的項目,點下一級,再找到MainThread,選擇summary:Virtual Memory,就能夠看到下方的File Backed Page In就是缺頁中斷的次數。

啓動.png

二、查看工程的符號順序

程序在編譯的時候,會有一個默認的符號順序的列表,這個列表包含了項目中全部類的函數的邏輯地址,內存分頁就是按照該內存地址進行排列。工具

二進制重排的最終目的其實就是改變這個列表的排序,讓咱們在程序啓動的時候須要調用的函數的邏輯地址順序排列起來。oop

首先咱們先經過Xcode的配置去獲取這個符號列表。學習

  • 一、build Setting 中搜索 Link m找到 Write Link Map File 默認爲NO,此處設置爲YES。

writeLinkMap.png

  • 二、Clean項目,而後再run。Success以後咱們找到項目目錄中以下位置。

WeChat6b82286cfbce6afd371345330660f855.png

  • 三、根據下圖的目錄中找到後綴爲.txt的文件。咱們打開它。

WeChat10e02efa08404aeddb14e60dc31c19da.png

  • 四、找到# Symbols:

TEXT.png

  • 五、而後咱們對照Xcode,Target中的Build Phases點開Compile Sources

CompileSources.png

  • 對比4點的圖和5點的圖,咱們發現符號列表中函數的排列順序

就是按照CompileSources類的順序來排列的。優化

好的,既然咱們知道了Xcode編譯產生的mapFile的原樣了。咱們接下來就是須要找到app在點擊運行的時候到app第一個頁面出來的時候調用了哪些函數,並將它從新排列在這個txt裏面,使它的邏輯地址連續,從而讓他們在同一個內存分頁中連續。動畫

三、Clang靜態插樁

咱們直接上代碼,原理能夠看以前提到的原文戳戳戳大佬原文👉🏻

  • 一、開始咱們仍是先配置XCode,這個文件放的就是咱們須要重排列的符號列表,XCode會自動將文件內的內容從新排列在以前咱們生成的符號列表txt文件的前方順序排列。

lborder.png

  • 二、第一點只是聲明,可是沒有實體文件,咱們須要手動建立一下,建立lb.order ,咱們cd到項目根目錄下命令好執行 touch lb.order 生成對應的lb.order文件。

  • 三、build Setting 中直接搜索 Other C Flags 找到Apple Clang - Custom Compiler Flags 添加如下代碼配置

-fsanitize-coverage=func,trace-pc-guard
複製代碼
  • 四、添加如下代碼,並在appDelegate最後調用。

.h

@interface ClangInsertStaticPile : NSObject


+ (void)startWriteToFileOfClangInsertStaticPile;

@end

複製代碼

.m

#import "ClangInsertStaticPile.h"
#import <dlfcn.h>
#import <libkern/OSAtomic.h>

@implementation ClangInsertStaticPile

+ (void)load{
    
}

void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                         uint32_t *stop) {
    static uint64_t N;  // Counter for the guards.
    if (start == stop || *start) return;  // Initialize only once.
    printf("INIT: %p %p\n", start, stop);
    for (uint32_t *x = start; x < stop; x++)
        *x = ++N;  // Guards should start from 1.
}

+ (void)startWriteToFileOfClangInsertStaticPile{
    NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
    while (true) {
        //offsetof 就是針對某個結構體找到某個屬性相對這個結構體的偏移量
        SymbolNode * node = OSAtomicDequeue(&symboList, offsetof(SymbolNode, next));
        if (node == NULL) break;
        Dl_info info;
        dladdr(node->pc, &info);

        NSString * name = @(info.dli_sname);

        // 添加 _
        BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
        NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];

        //去重
        if (![symbolNames containsObject:symbolName]) {
            [symbolNames addObject:symbolName];
        }
    }

    //取反
    NSArray * symbolAry = [[symbolNames reverseObjectEnumerator] allObjects];
    NSLog(@"%@",symbolAry);

    //將結果寫入到文件
    NSString * funcString = [symbolAry componentsJoinedByString:@"\n"];
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"lb.order"];
    NSData * fileContents = [funcString dataUsingEncoding:NSUTF8StringEncoding];
    BOOL result = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
    if (result) {
        NSLog(@"%@",filePath);
    }else{
        NSLog(@"文件寫入出錯");
    }
}

//原子隊列
static OSQueueHead symboList = OS_ATOMIC_QUEUE_INIT;
//定義符號結構體
typedef struct{
    void * pc;
    void * next;
}SymbolNode;

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    //if (!*guard) return;  // Duplicate the guard check.

    void *PC = __builtin_return_address(0);

    SymbolNode * node = malloc(sizeof(SymbolNode));
    *node = (SymbolNode){PC,NULL};

    //入隊
    // offsetof 用在這裏是爲了入隊添加下一個節點找到 前一個節點next指針的位置
    OSAtomicEnqueue(&symboList, node, offsetof(SymbolNode, next));
}


@end
複製代碼
  • 五、成功生成lb.order以後咱們去找到這個文件

divice.png 選中項目下載包

downloaddivice.png 下載下來的包右鍵顯示包內容

tmp.png 把這個lb.order的內容拷貝到項目目錄中的lb.order裏面就完成了。

  • 六、驗證結果

clean 一下再跑一下項目,咱們再按最開始的方式去看一下 # Symbols的符號順序,發現咱們已經重排成功了!咱們對比一下:

TEXT.png result.png

再用system Trace看一下對比一下:

啓動.png result2.jpg

四、總結

因爲個人項目工程並不大,而且該二進制重排方式僅僅只能優化本體項目的分頁內容,因此其實優化效果並不明顯,咱們的二進制重排其實並不完全。

經過配置工程,DYLD_PRINT_STATISTICS 能夠看到pre-maind的啓動時間

premainSet.png premain.png

其實優化的方式多種多樣,咱們也能夠從動態庫入手,對於私有動態庫,能夠才用合併動態庫的方式進行優化等。對於類的初始化方法,咱們能夠少使用load方法,儘量使用initializer在類使用到的時候再進行初始化等等方式。

若是進行完全的二進制重排鬚要對第三方的framework也進行上述重排方式,一個一個framework進行操做會比較複雜且耗時,此次實踐就沒有作第三方的重排。靜態插樁二進制重排僅是其中一種方式,本文是經過大佬的文章進行實操作的一個記錄,也是一個學習過程的記錄。

相關文章
相關標籤/搜索