二進(jìn)制重排原理
在上一篇啟動(dòng)優(yōu)化的概念中侥涵,我們理解了虛擬內(nèi)存與物理內(nèi)存步氏,在加載APP不活躍的部分時(shí),會(huì)訪問(wèn)虛擬內(nèi)存的page(映射表)集索,而對(duì)應(yīng)的物理內(nèi)存沒(méi)有與映射表與之關(guān)聯(lián)的話轨奄,將會(huì)觸發(fā)
缺頁(yè)異常(page fault)
,這個(gè)時(shí)候就必須將應(yīng)用在虛擬內(nèi)存不活躍的部分在映射表進(jìn)行與物理內(nèi)存的關(guān)聯(lián)再加載應(yīng)用到物理內(nèi)存腕让,可以理解觸發(fā)缺頁(yè)異常會(huì)對(duì)性能有一定的影響性。一般來(lái)說(shuō)歧斟,App在冷啟動(dòng)的過(guò)程中纯丸,會(huì)有很多的類偏形,分類,第三方需要加載和執(zhí)行觉鼻,也就是會(huì)觸發(fā)最多次的缺頁(yè)異常俊扭,當(dāng)許多的缺頁(yè)異常一起觸發(fā)會(huì)帶來(lái)大量的耗時(shí),如下以WeChat為例滑凉,啟動(dòng)階段觸發(fā)Page Fault的次數(shù)
打開(kāi)
instruments
的System Trace
- 點(diǎn)擊啟動(dòng)统扳,為了表現(xiàn)冷啟動(dòng),需要重啟手機(jī)清除緩存數(shù)據(jù)畅姊,
- 從instruments測(cè)試結(jié)果,可以看到pagefault次數(shù)有3193次吹由,可以看到這個(gè)是非常影響性能的
優(yōu)化思路
測(cè)試
- 首先我們通過(guò)以下Demo查看方法在編譯時(shí)期的排列順序若未,在viewController中按下列順序定義,以下幾個(gè)方法
@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
倾鲫。
- linkmap路徑 (或是上一張圖片的path to Link Map File)粗合。
- CMD+B編譯demo,依據(jù)對(duì)應(yīng)的路徑查找link map文件,如下所示乌昔,可以發(fā)現(xiàn)類中函數(shù)的加載順序是從上到下的隙疚。
* 而文件順序是根據(jù)Build Phases -> Compile Sources中的順序加載的。
思路
由上面的測(cè)試磕道,可以看到供屉,文件加載以及函數(shù)的調(diào)用的順序會(huì)影響page fault的數(shù)量,有極大的可能溺蕉,在啟動(dòng)時(shí)刻調(diào)用的方法是不同的page的伶丐,所以我們可以將我們?cè)趩?dòng)時(shí)刻調(diào)用的方法排列在同一頁(yè)中,如此一來(lái)就可以減少觸發(fā)page fault的次數(shù)疯特,這也就是二進(jìn)制重排原理哗魂。
- 注意:在iOS生產(chǎn)環(huán)境的app,在發(fā)生page fault進(jìn)行重新加載時(shí)漓雅,iOS系統(tǒng)還會(huì)對(duì)其做一次
簽名驗(yàn)證
录别,因此iOS在生產(chǎn)環(huán)境的page Fault比Debug環(huán)境下所產(chǎn)生的耗時(shí)更多時(shí)間。
實(shí)現(xiàn)二進(jìn)制重排
名詞解釋
Link Map
- Linkmap是iOS編譯過(guò)程中間的產(chǎn)物邻吞,
紀(jì)錄了二進(jìn)制的文件佈局
组题,需要再Xcode的Build Settings
裡開(kāi)啟Write Link Map File
, Link Map 主要包含三個(gè)部分:-
Object Files
生成二進(jìn)制用到的link單元的路徑和文件編號(hào) -
Sections
紀(jì)錄Mach-O每個(gè)Segment/section的地址範(fàn)圍 -
Symbols
按順序紀(jì)錄每個(gè)符號(hào)的地址範(fàn)圍
-
ld
- ld是Xcode使用的鏈接器,有一個(gè)參數(shù)order_file吃衅,我們可以通過(guò)在
Build Settings -> Order File
配置一個(gè)後綴為order的文件路徑往踢。在這個(gè)order文件中,將所需要的符號(hào)按照順序?qū)懺谘e面徘层,在項(xiàng)目編譯時(shí)峻呕,會(huì)按照這個(gè)文件的順序進(jìn)行加載利职,以此來(lái)達(dá)到我們的優(yōu)化。也就是說(shuō)二進(jìn)制重排的本質(zhì)就是對(duì)啟動(dòng)加載的符號(hào)進(jìn)行重新排列瘦癌。 - 但是我們要如何獲取我們的函數(shù)呢猪贪?以下有幾個(gè)思路...
- hook objc_msgSend:我們知道,函數(shù)的本質(zhì)是發(fā)送消息讯私,在底層都會(huì)來(lái)到
objc_msgSend
热押,但是由於objc_msgSend的參數(shù)是可變的,需要通過(guò)彙編獲取斤寇,對(duì)開(kāi)發(fā)人員要求較高桶癣,且也只能拿到OC和swift中@objc後的方法。 - 靜態(tài)掃描:掃描Mach-O特定段ㄉ和節(jié)裡面所存儲(chǔ)的符號(hào)以及函數(shù)數(shù)據(jù)
- Clang插樁:批量hook娘锁,可以實(shí)現(xiàn)100%符號(hào)覆蓋牙寞,最完全獲取swift,OC莫秆,block函數(shù)
- hook objc_msgSend:我們知道,函數(shù)的本質(zhì)是發(fā)送消息讯私,在底層都會(huì)來(lái)到
Clang 插樁
- llvm內(nèi)置了一個(gè)簡(jiǎn)單的代碼覆蓋率檢測(cè)(
SanitizerCoverage
)间雀。他對(duì)於基本塊級(jí)邊緣級(jí)插入對(duì)用戶定義函數(shù)的調(diào)用。接下來(lái)介紹的批量hook镊屎,就需要借助於sanitizerCoverage
惹挟。 - 關(guān)於clang的插樁覆蓋的官方文檔如下:
- 文檔中有詳細(xì)描述即簡(jiǎn)短的Demo演示。
第一步:開(kāi)啟SanitizerCoverage
- OC項(xiàng)目:
Build Settings
→Other C Flags
添加fsanitize-coverage=func,trace-pc-guard
- Swift項(xiàng)目:須額外在
Build Settings
→Other Swift Flags
中加入sanitize-coverage=func
和sanitize=undefined
- 所有鏈接到App中的二進(jìn)制都需要開(kāi)啟
SanitizerCoverage
缝驳,這樣才能完全覆蓋到所有調(diào)用 - 也可以通過(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
第二步:重寫(xiě)方法连锯,新建一個(gè)OC文件ACOrderFile
,重寫(xiě)兩個(gè)方法
-
__sanitizer_cov_trace_pc_guard_init
方法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. }
-
參數(shù)1:start
是一個(gè)指針党巾,指向無(wú)符號(hào)int類萎庭,4個(gè)字節(jié),相當(dāng)於一個(gè)數(shù)組的起始位置
齿拂,即符號(hào)的起始位置(是從高位往低位讀)
-
-
參數(shù)2:Stop
也是一個(gè)指針驳规,由於取數(shù)據(jù)是從高地址往低地址讀取的,所以如果直接讀取stop的地址並不是stop真正的地址署海,而是讀取到最後的地址吗购,所以讀取stop時(shí),由於stop占4個(gè)字節(jié)砸狞,stop真實(shí)地址 = stop打印的地址-0x4
- stop內(nèi)存地址中存儲(chǔ)的值表示什麼呢捻勉?我們可以增加一個(gè)方法/C++/屬性的方法(多3個(gè)),發(fā)現(xiàn)其值也會(huì)增加對(duì)應(yīng)的數(shù)刀森,例如我們?cè)黾右粋€(gè)test1方法踱启。可以看到數(shù)據(jù)從1d變成了1e
-
__sanitizer_cov_trace_pc_guard
方法,主要是捕獲所有的啟動(dòng)時(shí)刻的符號(hào)埠偿,將所有符號(hào)入隊(duì) - 參數(shù)guard是一個(gè)哨兵透罢,告訴我們是第幾個(gè)被調(diào)用的
- 符號(hào)的存儲(chǔ)需要借助於鏈表,所以需要定義鏈表節(jié)點(diǎn)ACNode
- 通過(guò)
OSQueueHead
創(chuàng)建原子隊(duì)列冠蒋,其目的是保證讀寫(xiě)的安全 - 通過(guò)
OSAtomicEnqueue
方法將node入隊(duì)羽圃,通過(guò)鏈表的next指針可以訪問(wèn)下一個(gè)符號(hào)
//原子隊(duì)列,其目的是保證寫(xiě)入安全抖剿,線程安全
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定義符號(hào)結(jié)構(gòu)體朽寞,以鏈表的形式
typedef struct {
void *pc;
void *next;
}ACNode;
/*
- start:起始位置
- stop:並不是最後一個(gè)符號(hào)地址,而是整個(gè)符號(hào)表的最後一個(gè)地址斩郎,
最後一個(gè)符號(hào)的地址=stop-4
(因?yàn)樽x取數(shù)據(jù)是從高地址往低地址讀取的脑融,且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) {
//將load方法過(guò)濾掉了史简,所以需要注釋掉
// if (!*guard) return;
//獲取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圆兵,並賦值
ACNode *node = malloc(sizeof(ACNode));
*node = (ACNode){PC, NULL};
//加入隊(duì)列
//符號(hào)的訪問(wèn)不是通過(guò)下標(biāo)訪問(wèn)跺讯,是通過(guò)鏈表的next指針,
//所以需要借用offsetof(結(jié)構(gòu)體類型殉农,下一個(gè)的地址即next)
OSAtomicEnqueue(&queue, node, offsetof(ACNode, next));
}
第三步:獲取所有符號(hào)並寫(xiě)入文件
- while循環(huán)從隊(duì)列中取出符號(hào)刀脏,處理非OC方法的前綴,存入數(shù)組中
- 數(shù)組取反超凳,因?yàn)槿腙?duì)儲(chǔ)存的順序是反序的愈污。
- 數(shù)組去重,並移除本身方法的符號(hào)
- 將數(shù)組中的符號(hào)轉(zhuǎn)成字符串並寫(xiě)入到AC.oder文件中
- 另外也可以在第一個(gè)
didFinishLaunchingWithOptions
根視圖轮傍,獲取符號(hào)(這部分可以由自己決定暂雹,以下範(fàn)例是在touchesBegan方法調(diào)用時(shí)獲取)
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSMutableArray <NSString *> * symbolNames = [NSMutableArray array];
//while 循環(huán)取符號(hào)
while (YES) {
//出隊(duì)
ACNode * node = OSAtomicDequeue(&symbolList, offsetof(ACNode, next));
if (node == NULL) {
break;
}
//取出PC存入info
Dl_info info;
dladdr(node->pc, &info);
//printf("%s \\n", info.dli_sname);
NSString * name = @(info.dli_sname);
//判斷是不是OC方法,如果不是需要加下滑線创夜,反之杭跪,則直接存儲(chǔ)
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString * symbolName = isObjc ? name: [@"_" stringByAppendingString:name];
[symbolNames addObject:symbolName];
}
//取反 (隊(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:[NSString stringWithFormat:@"%s",__FUNCTION__]];
//將數(shù)組變成字符串
NSString * funcStr = [funcs componentsJoinedByString:@"\\n"];
//字符串寫(xiě)入文件
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"AC.order"];
NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
NSLog(@"%@",funcStr);
}
注意點(diǎn):避免死循環(huán)
-
Build Settings -> Other C Flags
的如果配置的是-fsanitize-coverage=trace-pc-guard
,在while部分會(huì)出現(xiàn)死循環(huán)(我們?cè)趖ouchBegin方法中調(diào)適)
- 我們透過(guò)彙編查看,發(fā)現(xiàn)有三個(gè)地方調(diào)用
__sanitizer_cov_trace_pc_guard
的調(diào)用 - 第一次 touchBegin調(diào)用
__sanitizer_cov_trace_pc_guard
- 第二次bl跳轉(zhuǎn)跳轉(zhuǎn)因?yàn)閣hile循環(huán)涧尿,只要跳轉(zhuǎn)就會(huì)被hook系奉,即有b,bl指令现斋,就會(huì)被hook
- 第三次bl是printf
解決方式是將BuildSetting中的other C Flags
改成-fsanitize-coverage=func,trace-pc-guard
第四步 拷貝文件喜最,放入指定位置 並配置路徑
- 一般將該文件放入主項(xiàng)目的路徑下,並在
Build Settings -> Order File
中配置./AC.order庄蹋,下面是配置前後的對(duì)比 - 沒(méi)有配置瞬内,照序加載
-
有配置,依照啟動(dòng)時(shí)時(shí)刻所需(自己配置的.oder文件)加載