iOS 啟動(dòng)優(yōu)化--二進(jìn)制重排

抖音研發(fā)實(shí)踐:基于二進(jìn)制文件重排的解決方案 APP啟動(dòng)速度提升超15%

1咒林、二進(jìn)制重排原理

當(dāng)進(jìn)程在訪問(wèn)虛擬內(nèi)存時(shí)到踏,如果對(duì)應(yīng)的物理內(nèi)存不存在磷箕,會(huì)觸發(fā)缺頁(yè)異常(pagefault),由于在啟動(dòng)的時(shí)候需要調(diào)用的方法存在不同類中,而每個(gè)page的大小是固定的逻谦,這就導(dǎo)致啟動(dòng)時(shí)需要加載的page會(huì)更多刷晋,我們可以通過(guò)手動(dòng)排列符號(hào),將啟動(dòng)時(shí)刻需要的方法排列在一起矢否,減少缺頁(yè)異常

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

查看沒(méi)有優(yōu)化前的方法編譯順序

  • 自定義demo
@implementation ViewController

void test1(){
    printf("1");
}

void test2(){
    printf("2");
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    test1();
}

+(void)load{
    printf("3");
    test2();
}
@end
  • Build Setting --> Write Link Map File設(shè)置YES

    設(shè)置

  • 運(yùn)行編譯后仲闽,在對(duì)應(yīng)的路徑下(Path to Link Map File)找到LinkMap文件打開(kāi),我們可以通過(guò)替換方法位置重復(fù)改步驟發(fā)現(xiàn)僵朗,類里面函數(shù)的加載順序是從上到下的赖欣,通過(guò)替換Build Phases -- > Compile Sources中文件的順序屑彻,可以修改LinkMap中文件的順序

    加載順序

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

Link Map

LinkMap是iOS編譯過(guò)程的中間產(chǎn)物顶吮,記錄了二進(jìn)制文件的布局社牲,通過(guò)在Xcode的Build Setting中設(shè)置Write Link Map File = YES開(kāi)啟,主要包含下面三個(gè)部分

  • Object Files :生成二進(jìn)制用到的link單元的路徑和文件編號(hào)
  • Sections:記錄Mach-O文件中的每個(gè)Segment-section地址范圍
  • Symbols:按順序記錄每個(gè)符號(hào)的地址范圍
ld

ld是Xcode鏈接器悴了,通過(guò)在Xcode的Build Setting --> Order File中設(shè)置自定義的.order后綴的文件路徑搏恤,將需要重排的符號(hào)按順序?qū)懺诶锩妫?dāng)Xcode編譯時(shí)會(huì)按照.order文件中的符號(hào)順序加載让禀,我們可以通過(guò)下面幾種方法獲得APP啟動(dòng)時(shí)的運(yùn)行函數(shù)

  • HOOK objc_msgSend:由于objc_msgSend的參數(shù)是可變的挑社,需要匯編獲取,而且只能獲取到OC方法和Swift中的@objc方法
  • 靜態(tài)掃描:掃描 Mach-O文件中的 特定段和節(jié)里面所存儲(chǔ)的符號(hào)以及函數(shù)數(shù)據(jù)
  • Clang插樁:批量100%符號(hào)獲取巡揍,OC痛阻、Swift、C都可以獲取

Clang插樁

通過(guò)LLVM內(nèi)置的工具SanitizerCoverage腮敌,可以在函數(shù)級(jí)阱当、基本快級(jí)和邊緣插入到用戶定義函數(shù)的調(diào)用,官方文檔clang 自帶代碼覆蓋工具 中有使用簡(jiǎn)介和demo

【第一步】
  • 開(kāi)啟SanitizerCoverage
    • oc項(xiàng)目中糜工,Build Settings --> Other C Flags 中添加-fsanitize-coverage=func,trace-pc-guard
      • 注意:在官方demo中的是-fsanitize-coverage=trace-pc-guard在使用while循環(huán)時(shí)會(huì)出現(xiàn)死循環(huán)
        SanitizerCoverage
    • swift項(xiàng)目中弊添,Build Settings --> Other Swift Flags中加入-sanitize-coverage=func-sanitize=undefined
      SanitizerCoverage
    • 也可以通過(guò)Podfile統(tǒng)一配置
post_install do |installer|
 installer.pods_project.targets.each do |target|
   target.build_configurations.each do |config|
     config.build_settings['OTHER_CFLAGS'] = '-fsanitize-coverage=func,trace-pc-guard'
     config.build_settings['OTHER_SWIFT_FLAGS'] = '-sanitize-coverage=func -sanitize=undefined'
   end
 end
end
【第二步】

新建YPOrderFile文件,重寫下面兩個(gè)方法

void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,uint32_t *stop) {}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {}
  • __sanitizer_cov_trace_pc_guard_init(uint32_t *start,uint32_t *stop)方法記錄了符號(hào)數(shù)量

    • 參數(shù)1start是一個(gè)指針捌木,指向無(wú)符號(hào)的int類型油坝,占4字節(jié),相當(dāng)于一個(gè)數(shù)組的起始位置刨裆,從高位往低位讀取
    • 參數(shù)2stop也是一個(gè)指針澈圈,因?yàn)閿?shù)據(jù)是高位往低位讀取,此時(shí)的&stop的地址并不是其真實(shí)地址帆啃,因?yàn)閟top占了4個(gè)字節(jié)瞬女,所以stop真實(shí)地址=&stop-0x4(類似我們?cè)讷@取數(shù)組最后一個(gè)數(shù)據(jù)是需要減1一樣),在項(xiàng)目中新增一個(gè)方法努潘、block诽偷、c++時(shí)stop對(duì)應(yīng)會(huì)加0x4,屬性則會(huì)多0x12
  • __sanitizer_cov_trace_pc_guard (uint32_t *guard)方法疯坤,捕獲所有啟動(dòng)時(shí)刻的符號(hào)报慕,將所有符號(hào)入隊(duì)

    • 參數(shù)guard是一個(gè)哨兵,記錄當(dāng)前第幾個(gè)被調(diào)用
/原子隊(duì)列贴膘,其目的是保證寫入安全卖子,線程安全
static  OSQueueHead queue = OS_ATOMIC_QUEUE_INIT;
//定義符號(hào)結(jié)構(gòu)體,以鏈表的形式
typedef struct {
    void *pc;
    void *next;
}YPNode;

/*
 - start:起始位置
 - stop:并不是最后一個(gè)符號(hào)的地址刑峡,而是整個(gè)符號(hào)表的最后一個(gè)地址洋闽,最后一個(gè)符號(hào)的地址=stop-4(因?yàn)槭菑母叩刂吠偷刂纷x取的玄柠,且stop是一個(gè)無(wú)符號(hào)int類型,占4個(gè)字節(jié))诫舅。stop存儲(chǔ)的值是符號(hào)的
 */
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                                    uint32_t *stop) {
    static uint64_t N;
    if (start == stop || *start) return;
    printf("INIT: %p - %p\n", start, stop);
    for (uint32_t *x = start; x < stop; x++) {
        *x = ++N;
    }
    
}

/*
 可以全面hook方法羽利、函數(shù)、以及block調(diào)用刊懈,用于捕捉符號(hào)这弧,是在多線程進(jìn)行的,這個(gè)方法中只存儲(chǔ)pc虚汛,以鏈表的形式
 
 - guard 是一個(gè)哨兵匾浪,告訴我們是第幾個(gè)被調(diào)用的
 */
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//    if (!*guard) return;//將load方法過(guò)濾掉了,所以需要注釋掉
    
    //獲取PC
    /*
     - PC 當(dāng)前函數(shù)返回上一個(gè)調(diào)用的地址
     - 0 當(dāng)前這個(gè)函數(shù)地址卷哩,即當(dāng)前函數(shù)的返回地址
     - 1 當(dāng)前函數(shù)調(diào)用者的地址蛋辈,即上一個(gè)函數(shù)的返回地址
    */
    void *PC = __builtin_return_address(0);
    //創(chuàng)建node,并賦值
    YPNode *node = malloc(sizeof(YPNode));
    *node = (YPNode){PC, NULL};
    
    //加入隊(duì)列
    //符號(hào)的訪問(wèn)不是通過(guò)下標(biāo)訪問(wèn)将谊,是通過(guò)鏈表的next指針冷溶,所以需要借用offsetof(結(jié)構(gòu)體類型,下一個(gè)的地址即next)
    OSAtomicEnqueue(&queue, node, offsetof(YPNode, next));
}
【第三步】

獲取所有符號(hào)并寫入文件保存

  • 循環(huán)取出所有符號(hào)
  • 數(shù)組取反尊浓,因?yàn)槭侨腙?duì)存儲(chǔ)是反序的
  • 數(shù)組去重
  • 符號(hào)保存到yp.order文件中
extern void getOrderFile(void(^completion)(NSString *orderFilePath)){
    
    __sync_synchronize();
    NSString *functionExclude = [NSString stringWithFormat:@"_%s", __FUNCTION__];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //創(chuàng)建符號(hào)數(shù)組
        NSMutableArray<NSString *> *symbolNames = [NSMutableArray array];
        
        //while循環(huán)取符號(hào)
        while (YES) {
            //出隊(duì)
            YPNode *node = OSAtomicDequeue(&queue, offsetof(YPNode, next));
            if (node == NULL) break;
            
            //取出PC,存入info
            Dl_info info;
            dladdr(node->pc, &info);
//            printf("%s \n", info.dli_sname);
            
            if (info.dli_sname) {
                //判斷是不是OC方法逞频,如果不是,需要加下劃線存儲(chǔ)栋齿,反之苗胀,則直接存儲(chǔ)
                NSString *name = @(info.dli_sname);
                BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
                NSString *symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
                [symbolNames addObject:symbolName];
            }
           
        }
        
        if (symbolNames.count == 0) {
            if (completion) {
                completion(nil);
            }
            return;
        }
        
        //取反(隊(duì)列的存儲(chǔ)是反序的)
        NSEnumerator *emt = [symbolNames reverseObjectEnumerator];
        
        //去重
        NSMutableArray<NSString *> *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
        NSString *name;
        while (name = [emt nextObject]) {
            if (![funcs containsObject:name]) {
                [funcs addObject:name];
            }
        }
        
        //去掉自己
        [funcs removeObject:functionExclude];
        
        //將數(shù)組變成字符串
        NSString *funcStr = [funcs componentsJoinedByString:@"\n"];
        NSLog(@"Order:\n%@", funcStr);
        
        //字符串寫入文件
        NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"yp.order"];
        NSData *fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
        BOOL success = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
        if (completion) {
            completion(success ? filePath : nil);
        }
    });
}
【第四步】

在合適的地方調(diào)用方法

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    getOrderFile(^(NSString *orderFilePath) {
        NSLog(@"OrderFilePath:%@", orderFilePath);
    });
    return YES;
}

【第五步】

yp.order文件拷貝,放入主目錄路徑中瓦堵,并在Build Settings --> Order File中配./yp.order柒巫,也可以放在別的目錄,只要在order File中配置對(duì)應(yīng).order文件的路徑即可

二進(jìn)制重排前后對(duì)比

完整文件

#import "YPOrderFile.h"
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#import <dlfcn.h>
#import <libkern/OSAtomic.h>


@implementation YPOrderFile


//原子隊(duì)列谷丸,其目的是保證寫入安全,線程安全
static  OSQueueHead queue = OS_ATOMIC_QUEUE_INIT;
//定義符號(hào)結(jié)構(gòu)體应结,以鏈表的形式
typedef struct {
    void *pc;
    void *next;
}YPNode;

/*
 - start:起始位置
 - stop:并不是最后一個(gè)符號(hào)的地址刨疼,而是整個(gè)符號(hào)表的最后一個(gè)地址,最后一個(gè)符號(hào)的地址=stop-4(因?yàn)槭菑母叩刂吠偷刂纷x取的鹅龄,且stop是一個(gè)無(wú)符號(hào)int類型揩慕,占4個(gè)字節(jié))。stop存儲(chǔ)的值是符號(hào)的
 */
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                                    uint32_t *stop) {
    static uint64_t N;
    if (start == stop || *start) return;
    printf("INIT: %p - %p\n", start, stop);
    for (uint32_t *x = start; x < stop; x++) {
        *x = ++N;
    }
    
}

/*
 可以全面hook方法扮休、函數(shù)迎卤、以及block調(diào)用,用于捕捉符號(hào)玷坠,是在多線程進(jìn)行的蜗搔,這個(gè)方法中只存儲(chǔ)pc劲藐,以鏈表的形式
 
 - guard 是一個(gè)哨兵,告訴我們是第幾個(gè)被調(diào)用的
 */
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//    if (!*guard) return;//將load方法過(guò)濾掉了樟凄,所以需要注釋掉
    
    //獲取PC
    /*
     - PC 當(dāng)前函數(shù)返回上一個(gè)調(diào)用的地址
     - 0 當(dāng)前這個(gè)函數(shù)地址聘芜,即當(dāng)前函數(shù)的返回地址
     - 1 當(dāng)前函數(shù)調(diào)用者的地址,即上一個(gè)函數(shù)的返回地址
    */
    void *PC = __builtin_return_address(0);
    //創(chuàng)建node缝龄,并賦值
    YPNode *node = malloc(sizeof(YPNode));
    *node = (YPNode){PC, NULL};
    
    //加入隊(duì)列
    //符號(hào)的訪問(wèn)不是通過(guò)下標(biāo)訪問(wèn)汰现,是通過(guò)鏈表的next指針,所以需要借用offsetof(結(jié)構(gòu)體類型叔壤,下一個(gè)的地址即next)
    OSAtomicEnqueue(&queue, node, offsetof(YPNode, next));
}


extern void getOrderFile(void(^completion)(NSString *orderFilePath)){
    
    __sync_synchronize();
    NSString *functionExclude = [NSString stringWithFormat:@"_%s", __FUNCTION__];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //創(chuàng)建符號(hào)數(shù)組
        NSMutableArray<NSString *> *symbolNames = [NSMutableArray array];
        
        //while循環(huán)取符號(hào)
        while (YES) {
            //出隊(duì)
            YPNode *node = OSAtomicDequeue(&queue, offsetof(YPNode, next));
            if (node == NULL) break;
            
            //取出PC,存入info
            Dl_info info;
            dladdr(node->pc, &info);
//            printf("%s \n", info.dli_sname);
            
            if (info.dli_sname) {
                //判斷是不是OC方法瞎饲,如果不是,需要加下劃線存儲(chǔ)炼绘,反之嗅战,則直接存儲(chǔ)
                NSString *name = @(info.dli_sname);
                BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
                NSString *symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
                [symbolNames addObject:symbolName];
            }
           
        }
        
        if (symbolNames.count == 0) {
            if (completion) {
                completion(nil);
            }
            return;
        }
        
        //取反(隊(duì)列的存儲(chǔ)是反序的)
        NSEnumerator *emt = [symbolNames reverseObjectEnumerator];
        
        //去重
        NSMutableArray<NSString *> *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
        NSString *name;
        while (name = [emt nextObject]) {
            if (![funcs containsObject:name]) {
                [funcs addObject:name];
            }
        }
        
        //去掉自己
        [funcs removeObject:functionExclude];
        
        //將數(shù)組變成字符串
        NSString *funcStr = [funcs componentsJoinedByString:@"\n"];
        NSLog(@"Order:\n%@", funcStr);
        
        //字符串寫入文件
        NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"yp.order"];
        NSData *fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
        BOOL success = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
        if (completion) {
            completion(success ? filePath : nil);
        }
    });
}
@end

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市饭望,隨后出現(xiàn)的幾起案子仗哨,更是在濱河造成了極大的恐慌,老刑警劉巖铅辞,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件厌漂,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡斟珊,警方通過(guò)查閱死者的電腦和手機(jī)苇倡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)囤踩,“玉大人旨椒,你說(shuō)我怎么就攤上這事《率” “怎么了综慎?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)勤庐。 經(jīng)常有香客問(wèn)我示惊,道長(zhǎng),這世上最難降的妖魔是什么愉镰? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任米罚,我火速辦了婚禮,結(jié)果婚禮上丈探,老公的妹妹穿的比我還像新娘录择。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布隘竭。 她就那樣靜靜地躺著塘秦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪货裹。 梳的紋絲不亂的頭發(fā)上嗤形,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音弧圆,去河邊找鬼赋兵。 笑死,一個(gè)胖子當(dāng)著我的面吹牛搔预,可吹牛的內(nèi)容都是我干的霹期。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼拯田,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼历造!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起船庇,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤吭产,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后鸭轮,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體臣淤,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年窃爷,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了邑蒋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡按厘,死狀恐怖医吊,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情逮京,我是刑警寧澤卿堂,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站懒棉,受9級(jí)特大地震影響御吞,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜漓藕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望挟裂。 院中可真熱鬧享钞,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至狐肢,卻和暖如春添吗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背份名。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工碟联, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人僵腺。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓鲤孵,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親辰如。 傳聞我的和親對(duì)象是個(gè)殘疾皇子普监,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容