前言
在iOS原理 App的啟動(dòng)優(yōu)化1:優(yōu)化建議一文中已經(jīng)介紹了啟動(dòng)優(yōu)化的相關(guān)概念,我們知道,通過(guò)二進(jìn)制重排可以減少App的啟動(dòng)時(shí)間,提高程序的啟動(dòng)性能。
二進(jìn)制重排原理
CPU訪(fǎng)問(wèn)進(jìn)程數(shù)據(jù)時(shí),先訪(fǎng)問(wèn)數(shù)據(jù)對(duì)應(yīng)的虛擬內(nèi)存page,通過(guò)虛擬內(nèi)存地址找到其對(duì)應(yīng)的物理內(nèi)存地址,再通過(guò)物理地址訪(fǎng)問(wèn)到物理內(nèi)存上的數(shù)據(jù)。如果對(duì)應(yīng)的物理內(nèi)存地址不存在,說(shuō)明這部分?jǐn)?shù)據(jù)沒(méi)有加載到物理內(nèi)存中,此時(shí)會(huì)觸發(fā)缺頁(yè)中斷(Page Fault)。
物理內(nèi)存和虛擬內(nèi)存的詳細(xì)介紹可閱讀iOS原理 物理內(nèi)存&虛擬內(nèi)存
- Page Fault
Page Fault會(huì)中斷當(dāng)前進(jìn)程,需要先將訪(fǎng)問(wèn)的數(shù)據(jù)加載到物理內(nèi)存中,再讓CPU訪(fǎng)問(wèn)。而通過(guò)App Store渠道分發(fā)的App,Page Fault還會(huì)進(jìn)行簽名驗(yàn)證,所以每一個(gè)Page Fault都會(huì)帶來(lái)一定的耗時(shí)。
如果啟動(dòng)過(guò)程中觸發(fā)大量的Page Fault就會(huì)降低啟動(dòng)性能,延長(zhǎng)啟動(dòng)時(shí)間。通過(guò)System Trace可以查看App在啟動(dòng)過(guò)程中觸發(fā)的Page Fault次數(shù):
-
找到
Xcode->Open Developer Tool->Instruments,選擇并打開(kāi)System Trace。 -
選擇設(shè)備和App,啟動(dòng)
System Trace,在A(yíng)pp打開(kāi)第一個(gè)界面后停止System Trace。 -
搜索
Main Thread,選擇Virtual Memory,File Backed Page In即表示Page Fault。
可以看到,啟動(dòng)過(guò)程中觸發(fā)了60次Page Fault,總共耗時(shí)7.73ms。這個(gè)案例Demo只是新建的一個(gè)空項(xiàng)目,一般來(lái)說(shuō),工作項(xiàng)目會(huì)觸發(fā)上千次Page Fault,就拿微信來(lái)說(shuō),觸發(fā)次數(shù)達(dá)到2600多次,耗時(shí)接近700ms。因此,減少啟動(dòng)過(guò)程中Page Fault的觸發(fā)次數(shù),就能縮短啟動(dòng)時(shí)間,提高啟動(dòng)性能,而這就可以通過(guò)『二進(jìn)制重排』來(lái)實(shí)現(xiàn)。
二進(jìn)制重排實(shí)現(xiàn)方式
App啟動(dòng)過(guò)程中會(huì)調(diào)用一些方法和函數(shù),CPU需要訪(fǎng)問(wèn)相關(guān)數(shù)據(jù)。這時(shí),通過(guò)修改代碼在二進(jìn)制文件的布局,將啟動(dòng)時(shí)刻調(diào)用的方法和函數(shù)的二進(jìn)制符號(hào),排列在一起,確保在一個(gè)虛擬內(nèi)存page中,這樣就從多個(gè)Page Fault減少為一個(gè)Page Fault,這就是二進(jìn)制重排。
修改方法和函數(shù)二進(jìn)制符號(hào)的布局,需要通過(guò)Linkmap、ld以及Clang插樁來(lái)實(shí)現(xiàn)。
1. Linkmap
Linkmap是iOS編譯過(guò)程的中間產(chǎn)物,記錄了二進(jìn)制文件的布局,需要在Xcode中找到Target -> Build Settings -> Write Link Map File,并設(shè)置為Yes來(lái)開(kāi)啟。

Linkmap主要包括三大部分:
-
Object Files生成二進(jìn)制用到的link單元的路徑和文件編號(hào) -
Sections記錄Mach-O每個(gè)Segment/section的地址范圍 -
Symbols按順序記錄每個(gè)符號(hào)的地址范圍
編譯器在生成二進(jìn)制代碼的時(shí)候,默認(rèn)按照鏈接的Object File(.o)順序?qū)懳募?,按照Object File內(nèi)部的函數(shù)順序?qū)懞瘮?shù),因此方法和函數(shù)編譯后的二進(jìn)制符號(hào),默認(rèn)先按照.o文件(Object File)的鏈接順序,再按照文件里的編寫(xiě)順序來(lái)排列。
以案例Demo為例,查看編譯后方法和函數(shù)在Linkmap里面的排列順序。
- 在
ViewController里編寫(xiě)幾個(gè)方法和函數(shù)
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
void test1(){
printf("1");
}
void test2(){
printf("2");
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
test1();
}
+(void)load{
printf("3");
test2();
}
@end
-
查看
.o文件(Object File)鏈接順序,順序可以任意改變。
-
Command + B編譯Demo,根據(jù)路徑找到Link Map Fileimage.png -
打開(kāi)文件,查看方法和函數(shù)編譯后的二進(jìn)制符號(hào)在文件中的排列順序
從Link Map File里的布局情況可以印證,方法和函數(shù)編譯后的二進(jìn)制符號(hào),是先按照.o文件(Object File)的鏈接順序,再按照文件里的編寫(xiě)順序來(lái)排列。由于啟動(dòng)過(guò)程中調(diào)用的方法和函數(shù)可能存在于不同的類(lèi)里,它們編譯后的符號(hào)默認(rèn)在二進(jìn)制文件里分散排列,調(diào)用時(shí)就會(huì)觸發(fā)大量的Page Fault。
2. ld
ld是Xcode使用的鏈接器,寫(xiě)入其參數(shù)order_file中的符號(hào),會(huì)按照寫(xiě)入順序排列在二進(jìn)制文件中符號(hào)區(qū)域的頂部。因此,在Xcode中,通過(guò)Target -> Build Settings -> Order File來(lái)配置一個(gè)后綴為.order的文件路徑,并在這個(gè)order文件中,將啟動(dòng)過(guò)程中調(diào)用的方法和函數(shù)以符號(hào)格式寫(xiě)在里面,在項(xiàng)目編譯后,這些符號(hào)就會(huì)按照文件里的順序排列在二進(jìn)制文件中。若order文件中的符號(hào)對(duì)應(yīng)的方法實(shí)際不存在,ld則會(huì)忽略這些符號(hào)。

3. Clang插樁
Clang插樁,即批量hook,借助SanitizerCoverage(llvm內(nèi)置的一個(gè)簡(jiǎn)單的代碼覆蓋率檢測(cè)),實(shí)現(xiàn)100%符號(hào)覆蓋,獲取到所有的swift、OC、C、block函數(shù)。
Clang插樁覆蓋的官方文檔 : clang 自帶代碼覆蓋工具 。
實(shí)現(xiàn)步驟如下:
-
Step1:開(kāi)啟SanitizerCoverage
方法一:找到
Target->Build Settings->Other C Flags,添加-fsanitize-coverage=func,trace-pc-guard,如果是swift項(xiàng)目,還需在Other Swift Flags中加入-sanitize-coverage=func和-sanitize=undefined方法二:通過(guò)
podfile來(lái)配置參數(shù)
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
-
Step2:重寫(xiě)下面兩個(gè)函數(shù),捕獲所有調(diào)用的方法、函數(shù)以及block
-
__sanitizer_cov_trace_pc_guard_init函數(shù)
/** * start:是一個(gè)指針,指向無(wú)符號(hào)int類(lèi)型,4個(gè)字節(jié),相當(dāng)于一個(gè)數(shù)組的起始位置,即符號(hào)的起始位置。 * stop:標(biāo)記的最后的地址,通過(guò)stop的地址-4,獲取到最后一個(gè)符號(hào)的真實(shí)地址,真實(shí)地址里的值就代表這符號(hào)的總數(shù)。 */ void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) {}這個(gè)是初始化函數(shù),可以獲取到所有符號(hào)(方法、函數(shù)、block、屬性)的數(shù)量。在捕獲方法和函數(shù)時(shí),這個(gè)函數(shù)里面可以不做任何處理。
-
__sanitizer_cov_trace_pc_guard函數(shù)
/** * 這個(gè)方法可以捕獲所有調(diào)用的方法、函數(shù)以及block * guard:哨兵,告知是第幾次被調(diào)用 */ void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {}這個(gè)函數(shù)可以捕獲所有調(diào)用的方法、函數(shù)以及block。每當(dāng)調(diào)用一個(gè)方法、函數(shù)或者block,都會(huì)執(zhí)行一次這個(gè)函數(shù),在這里面,通過(guò)
__builtin_return_address(0)可以拿到當(dāng)前調(diào)用的方法(/函數(shù)/block)的地址,再通過(guò)Dl_info可以拿到方法地址和方法名。 -
二進(jìn)制重排的案例Demo
通過(guò)上面的學(xué)習(xí)可知,二進(jìn)制重排的實(shí)現(xiàn)步驟如下:
- 通過(guò)
Clang插樁獲取啟動(dòng)時(shí)刻調(diào)用的全部方法、函數(shù)、block。 - 將
方法、函數(shù)、block以符號(hào)的格式寫(xiě)入Order文件 - 配置
Order文件。
接下來(lái)通過(guò)案例Demo來(lái)詳細(xì)講解實(shí)現(xiàn)步驟,在案例中把Clang插樁相關(guān)代碼封裝在一個(gè)文件中,方便后續(xù)使用。
-
Step1:新建一個(gè)
OrderFileTool工具類(lèi),所有捕獲符號(hào)的相關(guān)代碼均在這里面實(shí)現(xiàn)。由于
__sanitizer_cov_trace_pc_guard函數(shù)執(zhí)行太頻繁,所以在函數(shù)里面只保存調(diào)用函數(shù)的地址,后面再統(tǒng)一解析。因此,打算用單向鏈表來(lái)保存這些地址,考慮到線(xiàn)程安全,決定用原子隊(duì)列OSQueueHead來(lái)保存。捕獲的代碼邏輯如下:#import "OrderFileTool.h" #import <dlfcn.h> #import <libkern/OSAtomic.h> #include <stdlib.h> @implementation OrderFileTool //原子隊(duì)列,保證多線(xiàn)程下的寫(xiě)入安全 static OSQueueHead symbolQueue = OS_ATOMIC_QUEUE_INIT; //定義符號(hào)結(jié)構(gòu)體,鏈表的節(jié)點(diǎn) typedef struct { void *pc; void *next; }SYNode; //初始化,里面不做任何處理 void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) {} //捕獲方法、函數(shù)、block,這里只保存地址,不做解析處理 void __sanitizer_cov_trace_pc_guard(uint32_t *guard) { //1.獲取方法、函數(shù)、block的地址 /** * __builtin_return_address:返回函數(shù)的地址 * 0:表示返回當(dāng)前函數(shù)的地址 * 1:表示返回當(dāng)前函數(shù)調(diào)用者的地址 */ void *PC = __builtin_return_address(0); //2.創(chuàng)建node //將地址賦值給node結(jié)構(gòu)體里的pc指針 SYNode *node = malloc(sizeof(SYNode)); *node = (SYNode){PC, NULL}; //3.加入隊(duì)列 //將node添加到隊(duì)列中,并將下一個(gè)node的地址賦值給當(dāng)前node結(jié)構(gòu)體里的next指針 OSAtomicEnqueue(&symbolQueue, node, offsetof(SYNode, next)); } +(void)writeOrderFile{ //創(chuàng)建一個(gè)符號(hào)數(shù)組 NSMutableArray *mArr = [NSMutableArray array]; //while循環(huán)獲取所有符號(hào) while (YES) { //取出節(jié)點(diǎn) SYNode *node = OSAtomicDequeue(&symbolQueue, offsetof(SYNode, next)); if(node==NULL){ break; } //解析PC,獲取符號(hào) Dl_info info; dladdr(node->pc, &info); NSString *name = @(info.dli_sname); //如果不是OC方法,需要在前面加上_ BOOL isObjC = [name hasPrefix:@"-["]||[name hasPrefix:@"+["]; NSString *symbol = isObjC?name:[@"_" stringByAppendingString:name]; //去重判斷,如果符號(hào)存在,就不添加 if(![mArr containsObject:symbol]){ //隊(duì)列的存儲(chǔ)是反序的,所以這里逆序保存在數(shù)組中,當(dāng)然,不逆序也不影響 [mArr insertObject:symbol atIndex:0]; } } //這里要去掉自己本身 NSString *currentFunc = @"+[OrderFileTool writeOrderFile]"; if([mArr containsObject:currentFunc]){ [mArr removeObject:currentFunc]; } //將數(shù)組轉(zhuǎn)換成字符串,并寫(xiě)入Order文件 NSString *symbolStr = [mArr componentsJoinedByString:@"\n"]; NSLog(@" ===== symbolStr = \n%@", symbolStr); //寫(xiě)入文件 NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"rewrite.order"]; NSData *fileContents = [symbolStr dataUsingEncoding:NSUTF8StringEncoding]; BOOL success = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil]; if (success) { NSLog(@" ==== rewrite success:%@", filePath); } } @end這里就實(shí)現(xiàn)了整個(gè)邏輯,只需要在外部調(diào)用
+(void)writeOrderFile方法就可完成所有符號(hào)的捕獲,并寫(xiě)入到order文件。 -
Step2:開(kāi)啟
SanitizerCoverage,在程序啟動(dòng)結(jié)束后執(zhí)行捕獲方法。一般來(lái)說(shuō),在首界面的
ViewDidLoad方法里執(zhí)行就能捕獲到程序啟動(dòng)過(guò)程的所有符號(hào)。這里也是在案例Demo的ViewController.m文件里執(zhí)行。#import "ViewController.h" #import "OrderFileTool.h" @interface ViewController () @end @implementation ViewController void test1(){ printf("1"); } void test2(){ printf("2"); } - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. test1(); //獲取啟動(dòng)過(guò)程所有方法和函數(shù)的符號(hào) [OrderFileTool writeOrderFile]; } +(void)load{ printf("3"); test2(); } @end通過(guò)輸出的Order文件路徑,找到文件并改成
.txt后綴打開(kāi),可以看到符號(hào)的排列順序如下:可以看到,啟動(dòng)過(guò)程的最后一個(gè)函數(shù)
test1的符號(hào)排在最后一個(gè),至此,完成了所有符號(hào)的捕獲。 -
Step3:配置
Order文件,然后Command + B編譯。將生成的
rewrite.order文件添加到項(xiàng)目中,再在Target->Build Settings->Order File中配置order文件的路徑,然后編譯。
查看編譯后新生成的
LinkMap File。可以看到,啟動(dòng)過(guò)程的方法和函數(shù)符號(hào),均按照
order文件里的順序排列在一起,并置于二進(jìn)制文件前面。至此,整個(gè)二進(jìn)制重排的過(guò)程就完成了。
溫馨提示:獲取到啟動(dòng)過(guò)程的全部符號(hào)后,就關(guān)掉
SanitizerCoverage,并刪除OrderFileTool工具類(lèi),若以后App啟動(dòng)相關(guān)業(yè)務(wù)發(fā)生變更后,再重新排列一次就可以了。
推薦閱讀
1. iOS原理 App的啟動(dòng)優(yōu)化1:優(yōu)化建議
2. 抖音研發(fā)實(shí)踐:基于二進(jìn)制文件重排的解決方案 APP啟動(dòng)速度提升超15%
3. Clang插樁覆蓋的官方文檔
4. iOS調(diào)優(yōu) | 深入理解Link Map File








