iOS啟動優(yōu)化之二進(jìn)制重排

一、二進(jìn)制重排介紹

1、App啟動

進(jìn)程如果能直接訪問物理內(nèi)存無疑是很不安全的,所以操作系統(tǒng)在物理內(nèi)存的上又建立了一層虛擬內(nèi)存。蘋果在這個基礎(chǔ)上還有 ASLR(Address Space Layout Randomization) 技術(shù)的保護(hù),不過不是這次的重點(diǎn)。

iOS系統(tǒng)中虛擬內(nèi)存到物理內(nèi)存的映射都是以頁為最小單位的。當(dāng)進(jìn)程訪問一個虛擬內(nèi)存Page而對應(yīng)的物理內(nèi)存卻不存在時,就會出現(xiàn)Page Fault缺頁中斷,然后加載這一頁。雖然本身這個處理速度是很快的,但是在一個App的啟動過程中可能出現(xiàn)上千(甚至更多)次Page Fault,這個時間積累起來會比較明顯了

另外,還有兩個重要的概念:冷啟動、熱啟動??赡苡行┩瑢W(xué)認(rèn)為殺掉再重啟App就是冷啟動了,其實(shí)是不對的。

  • 冷啟動:程序完全退出,之間加載的分頁數(shù)據(jù)被其他進(jìn)程所使用覆蓋之后,或者重啟設(shè)備、第一次安裝,才算是冷啟動。
  • 熱啟動:程序殺掉之后,馬上又重新啟動。這個時候相應(yīng)的物理內(nèi)存中仍然保留之前加載過的分頁數(shù)據(jù),可以進(jìn)行重用,不需要全部重新加載。所以熱啟動的速度比較快。

2、二進(jìn)制重排原理

編譯器在生成二進(jìn)制代碼的時候,默認(rèn)按照鏈接的Object File(.o)順序?qū)懳募?,按照Object File內(nèi)部的函數(shù)順序?qū)懞瘮?shù)。

靜態(tài)庫文件.a就是一組.o文件的ar包,可以用ar -t查看.a包含的所有.o。

簡化問題:假設(shè)我們只有兩個page:page1/page2,其中綠色的method1和method5啟動時候需要調(diào)用,為了執(zhí)行對應(yīng)的代碼,系統(tǒng)必須進(jìn)行兩個Page Fault。

但如果我們把method1和method5排布到一起,那么只需要一個Page Fault即可,這就是二進(jìn)制文件重排的核心原理。

圖1:二進(jìn)制重排原理.png

實(shí)際項(xiàng)目中的做法是將啟動時需要調(diào)用的函數(shù)放到一起 ( 比如 前10頁中 ) 以盡可能減少 page fault , 達(dá)到優(yōu)化目的 . 而這個做法就叫做 : 二進(jìn)制重排 。

注意:在iOS生產(chǎn)環(huán)境的app,在發(fā)生Page Fault進(jìn)行重新加載時,iOS系統(tǒng)還會對其做一次簽名驗(yàn)證,因此 iOS 生產(chǎn)環(huán)境的 Page Fault 比Debug環(huán)境下所產(chǎn)生的耗時更多

二、重排的order文件

1、文件順序

Build PhasesCompile Sources 列表順序決定了文件執(zhí)行的順序(可以調(diào)整)。如果不進(jìn)行重排,文件的順序決定了方法、函數(shù)的執(zhí)行順序。我們在 ViewControllerAppDelegate 中加入以下代碼:

+ (void)load {
    NSLog(@"%s", __FUNCTION__);
}

我們調(diào)整 Compile Sources 中這兩個類的順序,然后分別執(zhí)行對比??梢钥吹?,隨著 Compile Sources 中的文件順序的修改,+load 方法的執(zhí)行順序也發(fā)生了改變。

圖2:文件執(zhí)行順序.png

2、符號表順序

Link Map 是iOS編譯過程的中間產(chǎn)物,記錄了二進(jìn)制文件的布局,需要在Xcode的Build Settings 里開啟Write Link Map File,Link Map主要包含三部分:

  • Object Files 生成二進(jìn)制用到的link單元的路徑和文件編號
  • Sections 記錄Mach-O每個Segment/section的地址范圍
  • Symbols 按順序記錄每個符號的地址范圍
1)Build Settings中修改Write Link Map FileYES
2)查找Link Map符號表txt文件

編譯后會生成一個Link Map符號表txt文件,選擇Product中的App,在Finder中打開,選擇Intermediates.noindex文件夾,找到LinkMap文件,這里是ZJHBinaryLaunchDemo-LinkMap-normal-x86_64.txt。。詳細(xì)路徑請看下圖。

圖3:查找符號表文件.png
3)查看Link Map符號表txt文件

打開文件之后來到第一部分的最后。我們可以看到這個順序和我們Compile Sources中的順序是一致的。接下來的部分:

圖4:查看符號表文件.png

可以看到,整體的順序和Compile Sources的中的順序是一樣的,并且方法是按照文件中方法的順序進(jìn)行鏈接的。ViewController中的方法添加完后,才是AppDelegate中的方法,以此類推。

  • Address? 表示文件中方法的地址。
  • Size 表示方法的大小。
  • File 表示在第幾個文件中。
  • Name 表示方法名。

3、導(dǎo)入order文件

ld是Xcode使用的鏈接器,有一個參數(shù)order_file,我們可以通過在Build Settings -> Order File配置一個后綴為order的文件路徑.在這個order文件中,將所需要的符號按照順序?qū)懺诶锩妫陧?xiàng)目編譯時,會按照這個文件的順序進(jìn)行加載,以此來達(dá)到我們的優(yōu)化

來到工程根目錄 , 新建一個文件 touch ZJHBinaryLaunchDemo.order . 隨便挑選幾個啟動時就需要加載的方法 , 例如我這里選了以下幾個 .

+[AppDelegate load]
+[ViewController load]
_main
-[ViewController someMethod]

然后在Build Settings中找到Order File,填入./ZJHBinaryLaunchDemo.order。然后重新比納音,再次查看生成符號表txt文件。

圖5:導(dǎo)入order文件.png

可以看到Link Map中的最上面幾個方法和我們在ZJHBinaryLaunchDemo.order文件中設(shè)置的方法順序一致!

Xcode的連接器ld還忽略掉了不存在的方法 -[ViewController someMethod]。如果提供了link選項(xiàng) -order_file_statistics,會以warning的形式把這些沒找到的符號打印在日志里。

注意:有部分同學(xué)可能配置完運(yùn)行會發(fā)現(xiàn)報(bào)錯說can't open 這個 order file . 是因?yàn)槲募袷降膯栴} . 不用使用 mac 自帶的文本編輯 . 使用命令工具 touch 創(chuàng)建即可 .

三、檢測啟動時方法

要真正的實(shí)現(xiàn)二進(jìn)制重排,我們需要拿到啟動時所有用到的方法、函數(shù)等符號,并保存其順序,然后寫入order文件,實(shí)現(xiàn)二進(jìn)制重排。這里我們使用Clang插樁的方式

1、Clang插樁原理

其實(shí)就是一個代碼覆蓋工具,更多信息可以查看官網(wǎng)。

1)首先 , 添加編譯設(shè)置

Build SettingsOther C Flags添加配置

-fsanitize-coverage=trace-pc-guard

編譯的話會報(bào)以下錯誤,意思是找不到這兩個函數(shù)

Undefined symbol: ___sanitizer_cov_trace_pc_guard_init
Undefined symbol: ___sanitizer_cov_trace_pc_guard
2)添加 hook 代碼

我們把面的代碼,添加到 ViewController.m

#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>

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 __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;  // Duplicate the guard check.
//  void *PC = __builtin_return_address(0);
  char PcDescr[1024];
//  __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

3)運(yùn)行工程 , 查看打印
INIT: 0x1024153e0 0x102415420
guard: 0x1024153f8 7 PC 
guard: 0x1024153ec 4 PC ?
guard: 0x102415414 e PC 
guard: 0x102415418 f PC ?
guard: 0x102415414 e PC 
guard: 0x102415414 e PC 
guard: 0x1024153fc 8 PC $
guard: 0x102415414 e PC 
guard: 0x102415414 e PC ?
guard: 0x102415414 e PC \300\202\3605?

代碼命名 INIT 后面打印的兩個指針地址叫 startstop . 那么我們通過 lldb 來查看下從 startstop 這個內(nèi)存地址里面所存儲的到底是啥 .

4)驗(yàn)證 startstop 內(nèi)存地址存儲值

viewDidLoad方法中添加斷點(diǎn),執(zhí)行項(xiàng)目。在lldb分別輸入 x start地址x stop地址-0x4,讀取內(nèi)存地址。 x stop地址-0x4 是結(jié)束位置,按顯示是4位的,所以向前移動4位,打印出來的應(yīng)該就是最后一位。

圖6:驗(yàn)證start到stop內(nèi)存地址存儲值.png

發(fā)現(xiàn)存儲的是從 116(0x10) 這個序號 ,我們再新增兩個方法,再次運(yùn)行查看:

INIT: 0x102fb93f0 0x102fb9438

(lldb) x 0x102fb9438-0x4
0x102fb9434: 12 00 00 00 f0 92 0c 03 01 00 00 00 00 00 00 00  ................
0x102fb9444: 00 00 00 00 5f 33 fb 02 01 00 00 00 00 00 00 00  ...._3..........
(lldb) 

發(fā)現(xiàn)從 0x10 變成了 0x12 . 也就是說存儲的 116 這個序號變成了 118 。那么我們得到一個猜想 , 這個內(nèi)存區(qū)間保存的就是工程所有符號的個數(shù) 。

5)驗(yàn)證guard調(diào)用次數(shù)

touchesBegan 方法中,打印語句,點(diǎn)擊屏幕。每點(diǎn)擊一次就會調(diào)用一次 guard :。而且 guard :是在前面調(diào)用的。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchesBegan");
}

// 控制臺的輸出
guard: 0x1006053fc 4 PC ?
2021-04-21 13:29:51.936925+0800 ZJHBinaryLaunchDemo[13644:5077278] touchesBegan

touchesBegan 方法中,執(zhí)行調(diào)用test1方法,會發(fā)現(xiàn) guard :調(diào)用了兩次

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchesBegan");
    [self test1];
}

- (void)test1 {
    NSLog(@"test1");
}

// 控制臺的輸出
guard: 0x102d893f8 3 PC ?
2021-04-21 13:31:57.152720+0800 ZJHBinaryLaunchDemo[13646:5077923] touchesBegan
guard: 0x102d893fc 4 PC d?\330?\201\226P\314\370\223\330??
2021-04-21 13:31:57.152915+0800 ZJHBinaryLaunchDemo[13646:5077923] test1

由此,發(fā)現(xiàn)我們實(shí)際調(diào)用幾個方法 , 就會打印幾次 guard :。

6)查看匯編代碼驗(yàn)證

我們在 touchesBegan:touches withEvent: 開頭設(shè)置一個斷點(diǎn),并開啟匯編顯示(菜單欄Debug → Debug Workflow → Always Show Disassembly)。

圖7:查看匯編代碼驗(yàn)證.png

通過匯編我們發(fā)現(xiàn) , 在每個函數(shù)調(diào)用的第一句實(shí)際代碼 ( 棧平衡與寄存器數(shù)據(jù)準(zhǔn)備除外 ) , 被添加進(jìn)去了一個 bl 調(diào)用到 __sanitizer_cov_trace_pc_guard 這個函數(shù)中來 。而實(shí)際上這也是靜態(tài)插樁的原理和名稱由來 。

靜態(tài)插樁總結(jié):靜態(tài)插樁實(shí)際上是在編譯期就在每一個函數(shù)內(nèi)部二進(jìn)制源數(shù)據(jù)添加 hook 代碼 ( 我們添加的 __sanitizer_cov_trace_pc_guard 函數(shù) ) 來實(shí)現(xiàn)全局的方法 hook 的效果 .

2、獲取所有函數(shù)符號

1)獲取原函數(shù)地址

我們在 __sanitizer_cov_trace_pc_guard 函數(shù)中的這一句代碼 :

void *PC = __builtin_return_address(0); 

它的作用其實(shí)就是去讀取 x30 中所存儲的要返回時下一條指令的地址 . 所以他名稱叫做 __builtin_return_address . 換句話說 , 這個地址就是我當(dāng)前這個函數(shù)執(zhí)行完畢后 , 要返回到哪里去 。也就是說 , 我們現(xiàn)在可以在 __sanitizer_cov_trace_pc_guard 這個函數(shù)中 , 通過 __builtin_return_address 數(shù)拿到原函數(shù)調(diào)用 __sanitizer_cov_trace_pc_guard 這句匯編代碼的下一條指令的地址 。及上圖中,拿到-[ViewController touchesBegan:withEvent:] 地址

2)根據(jù)內(nèi)存地址獲取函數(shù)名稱

拿到了函數(shù)內(nèi)部一行代碼的地址 , 如何獲取函數(shù)名稱呢 ? 熟悉安全攻防 , 逆向的同學(xué)可能會清楚 . 我們?yōu)榱朔乐鼓承┨囟ǖ姆椒ū粍e人使用 fishhook hook 掉 , 會利用 dlopen 打開動態(tài)庫 , 拿到一個句柄 , 進(jìn)而拿到函數(shù)的內(nèi)存地址直接調(diào)用 .是不是跟我們這個流程有點(diǎn)相似 , 只是我們好像是反過來的 . 其實(shí)反過來也是可以的 .

dlopen 相同 , 在 dlfcn.h 中有一個方法如下 :

typedef struct dl_info {
        const char      *dli_fname;     /* 所在文件 */
        void            *dli_fbase;     /* 文件地址 */
        const char      *dli_sname;     /* 符號名稱 */
        void            *dli_saddr;     /* 函數(shù)起始地址 */
} Dl_info;

//這個函數(shù)能通過函數(shù)內(nèi)部地址找到函數(shù)符號
int dladdr(const void *, Dl_info *);

緊接著我們來實(shí)驗(yàn)一下 , 先導(dǎo)入頭文件#import <dlfcn.h> , 然后修改代碼如下 :

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

    void *PC = __builtin_return_address(0);
    Dl_info info;
    dladdr(PC, &info);

    printf("fname=%s \nfbase=%p \nsname=%s\nsaddr=%p \n",info.dli_fname,info.dli_fbase,info.dli_sname,info.dli_saddr);

    char PcDescr[1024];
    printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

查看打印結(jié)果,可以看到我們要找的符號了 :

圖8:根據(jù)內(nèi)存地址獲取函數(shù)名稱.png

3、可能遇到的問題

1)多線程問題

項(xiàng)目各個方法肯定有可能會在不同的函數(shù)執(zhí)行 , 因此 __sanitizer_cov_trace_pc_guard 這個函數(shù)也有可能受多線程影響 , 所以你當(dāng)然不可能簡簡單單用一個數(shù)組來接收所有的符號就搞定了。考慮到這個方法會來特別多次 , 使用鎖會影響性能 , 這里使用蘋果底層的原子隊(duì)列 ( 底層實(shí)際上是個棧結(jié)構(gòu) , 利用隊(duì)列結(jié)構(gòu) + 原子性來保證順序 ) 來實(shí)現(xiàn) .

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    //遍歷出隊(duì)
    while (true) {
        //offsetof 就是針對某個結(jié)構(gòu)體找到某個屬性相對這個結(jié)構(gòu)體的偏移量
        SymbolNode * node = OSAtomicDequeue(&symboList, offsetof(SymbolNode, next));
        if (node == NULL) break;
        Dl_info info;
        dladdr(node->pc, &info);

        printf("%s \n",info.dli_sname);
    }
}
//原子隊(duì)列
static OSQueueHead symboList = OS_ATOMIC_QUEUE_INIT;
//定義符號結(jié)構(gòu)體
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};

    //入隊(duì)
    // offsetof 用在這里是為了入隊(duì)添加下一個節(jié)點(diǎn)找到 前一個節(jié)點(diǎn)next指針的位置
    OSAtomicEnqueue(&symboList, node, offsetof(SymbolNode, next));
}

但此方法會導(dǎo)致死循環(huán)的問題

2)死循環(huán)問題

通過匯編會查看到 一個帶有 while 循環(huán)的方法 , 會被靜態(tài)加入多次 __sanitizer_cov_trace_pc_guard 調(diào)用 , 導(dǎo)致死循環(huán).

Other C Flags 修改為如下 :

-fsanitize-coverage=func,trace-pc-guard

代表進(jìn)針對 func 進(jìn)行 hook . 再次運(yùn)行就可以了。

3) load 方法不打印問題

load 方法時 , __sanitizer_cov_trace_pc_guard 函數(shù)的參數(shù) guard 是 0。上述打印并沒有發(fā)現(xiàn) load .

解決 : 屏蔽掉 __sanitizer_cov_trace_pc_guard 函數(shù)中的

if (!*guard) return;

效果如下

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};

    //入隊(duì)
    // offsetof 用在這里是為了入隊(duì)添加下一個節(jié)點(diǎn)找到 前一個節(jié)點(diǎn)next指針的位置
    OSAtomicEnqueue(&symboList, node, offsetof(SymbolNode, next));
}

// 打印結(jié)果
INIT: 0x104d8d400 0x104d8d444
-[ViewController touchesBegan:withEvent:] 
-[SceneDelegate sceneDidBecomeActive:] 
-[SceneDelegate sceneWillEnterForeground:] 
-[ViewController viewDidLoad] 
-[SceneDelegate window] 
-[SceneDelegate window] 
-[SceneDelegate window] 
-[SceneDelegate scene:willConnectToSession:options:] 
-[SceneDelegate window] 
-[SceneDelegate window] 
-[SceneDelegate setWindow:] 
-[SceneDelegate window] 
-[AppDelegate application:didFinishLaunchingWithOptions:] 
main 
+[AppDelegate load] 
+[ViewController load] 

這樣的話,load 方法就有了。這里也為我們提供了一點(diǎn)啟示:如果我們希望從某個函數(shù)之后/之前開始優(yōu)化 , 通過一個全局靜態(tài)變量 , 在特定的時機(jī)修改其值 , 在 __sanitizer_cov_trace_pc_guard 這個函數(shù)中做好對應(yīng)的處理即可 .

4、符號信息寫入文件

完整代碼如下 :

#import "ViewController.h"
#import <dlfcn.h>
#import <libkern/OSAtomic.h>
@interface ViewController ()
@end

@implementation ViewController
+ (void)load{

}
- (void)viewDidLoad {
    [super viewDidLoad];
    testCFunc();
    [self testOCFunc];
}
- (void)testOCFunc{
    NSLog(@"oc函數(shù)");
}
void testCFunc(){
    LBBlock();
}
void(^LBBlock)(void) = ^(void){
    NSLog(@"block");
};

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)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
    while (true) {
        //offsetof 就是針對某個結(jié)構(gòu)體找到某個屬性相對這個結(jié)構(gòu)體的偏移量
        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);

    //將結(jié)果寫入到文件
    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(@"文件寫入出錯");
    }

}
//原子隊(duì)列
static OSQueueHead symboList = OS_ATOMIC_QUEUE_INIT;
//定義符號結(jié)構(gòu)體
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};

    //入隊(duì)
    // offsetof 用在這里是為了入隊(duì)添加下一個節(jié)點(diǎn)找到 前一個節(jié)點(diǎn)next指針的位置
    OSAtomicEnqueue(&symboList, node, offsetof(SymbolNode, next));
}
@end

文件寫入到了 tmp 路徑下 , 運(yùn)行 , 打開手機(jī)下載查看 :

圖9:符號信息寫入文件.png

5、swift 工程 / 混編工程問題

通過如上方式適合純 OC 工程獲取符號方式 .由于 swift 的編譯器前端是自己的 swift 編譯前端程序 , 因此配置稍有不同 .

搜索 Other Swift Flags , 添加兩條配置即可 :

  • -sanitize-coverage=func
  • -sanitize=undefined

swift 類通過上述方法同樣可以獲取符號 .

四、驗(yàn)證

1、打印啟動時間

在系統(tǒng)執(zhí)行應(yīng)用程序的main函數(shù)并調(diào)用應(yīng)用程序委托函數(shù)(applicationWillFinishLaunching)之前,會發(fā)生很多事情。我們可以通過添加環(huán)境變量可以打印處APP的啟動分析(Edit scheme -> Run -> Argument).
DYLD_PRINT_STATISTICS設(shè)置為1(dyld_print_statistics)。如果需要更詳細(xì)的信息,那就將DYLD_PRINT_STATISTICS_DETAILS設(shè)置為1

運(yùn)行一下,對比控制臺的輸出(因筆者是在demo驗(yàn)證,時間優(yōu)化的效果不明顯,這里就不做對比展示了):

Total pre-main time:  97.73 milliseconds (100.0%)
         dylib loading time:  28.02 milliseconds (28.6%)
        rebase/binding time:  21.70 milliseconds (22.2%)
            ObjC setup time:   5.16 milliseconds (5.2%)
           initializer time:  42.85 milliseconds (43.8%)
           slowest intializers :
             libSystem.B.dylib :   6.26 milliseconds (6.4%)
   libBacktraceRecording.dylib :   9.88 milliseconds (10.1%)
    libMainThreadChecker.dylib :  22.10 milliseconds (22.6%)
           ZJHBinaryLaunchDemo :   2.81 milliseconds (2.8%)

2、System Trace

日常開發(fā)中性能分析是用最多的工具無疑是Time Profiler,但Time Profiler是基于采樣的,并且只能統(tǒng)計(jì)線程實(shí)際在運(yùn)行的時間,而發(fā)生Page Fault的時候線程是被blocked,所以我們需要用一個不常用但功能卻很強(qiáng)大的工具:System Trace。

選中主線程,在VM Activity中的File Backed Page In次數(shù)就是Page Fault次數(shù),并且雙擊還能按時序看到引起Page Fault的堆棧:

圖10:System Trace.jpeg

五、CocoaPods相關(guān)

對于 cocoapod 工程引入的庫 , 由于針對不同的 target . 那么我們在主程序中的 target 添加的編譯設(shè)置 Write Link Map File , -fsanitize-coverage=func,trace-pc-guard 以及 order file 等設(shè)置肯定是不會生效的 . 解決方法就是針對需要的 target 去做對應(yīng)的設(shè)置即可 .

對于直接手動導(dǎo)入到工程里的 sdk , 不管是 靜態(tài)庫 .a 還是 動態(tài)庫 , 默認(rèn)主工程的設(shè)置就可以了 , 是可以拿到符號的 .

更多CocoaPods相關(guān)問題,可參考這篇文章:https://juejin.cn/post/6844904192193085448


參考鏈接:

iOS 啟動優(yōu)化之Clang插樁實(shí)現(xiàn)二進(jìn)制重排
我是如何讓微博綠洲的啟動速度提升30%的
懶人版二進(jìn)制重排
抖音研發(fā)實(shí)踐:基于二進(jìn)制文件重排的解決方案 APP啟動速度提升超15%
懶人版二進(jìn)制重排
手淘架構(gòu)組最新實(shí)踐 | iOS基于靜態(tài)庫插樁的?進(jìn)制重排啟動優(yōu)化

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容