抖音研發(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è)異常
查看沒(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
-
注意:在官方demo中的是
- swift項(xiàng)目中弊添,
Build Settings --> Other Swift Flags
中加入-sanitize-coverage=func
和-sanitize=undefined
SanitizerCoverage - 也可以通過(guò)
Podfile
統(tǒng)一配置
- oc項(xià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ù)1
start
是一個(gè)指針捌木,指向無(wú)符號(hào)的int類型油坝,占4字節(jié),相當(dāng)于一個(gè)數(shù)組的起始位置刨裆,從高位往低位讀取 - 參數(shù)2
stop
也是一個(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
- 參數(shù)1
-
__sanitizer_cov_trace_pc_guard (uint32_t *guard)
方法疯坤,捕獲所有啟動(dòng)時(shí)刻的符號(hào)报慕,將所有符號(hào)入隊(duì)- 參數(shù)
guard
是一個(gè)哨兵,記錄當(dāng)前第幾個(gè)被調(diào)用
- 參數(shù)
/原子隊(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文件的路徑即可
完整文件
#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