二進(jìn)制重排原理
在上一篇啟動優(yōu)化的概念中,我們理解了虛擬內(nèi)存與物理內(nèi)存,在加載APP不活躍的部分時,會訪問虛擬內(nèi)存的page(映射表),而對應(yīng)的物理內(nèi)存沒有與映射表與之關(guān)聯(lián)的話,將會觸發(fā)
缺頁異常(page fault),這個時候就必須將應(yīng)用在虛擬內(nèi)存不活躍的部分在映射表進(jìn)行與物理內(nèi)存的關(guān)聯(lián)再加載應(yīng)用到物理內(nèi)存,可以理解觸發(fā)缺頁異常會對性能有一定的影響性。一般來說,App在冷啟動的過程中,會有很多的類,分類,第三方需要加載和執(zhí)行,也就是會觸發(fā)最多次的缺頁異常,當(dāng)許多的缺頁異常一起觸發(fā)會帶來大量的耗時,如下以WeChat為例,啟動階段觸發(fā)Page Fault的次數(shù)
打開
instruments的System Trace


- 點擊啟動,為了表現(xiàn)冷啟動,需要重啟手機清除緩存數(shù)據(jù),

- 從instruments測試結(jié)果,可以看到pagefault次數(shù)有3193次,可以看到這個是非常影響性能的
優(yōu)化思路
測試
- 首先我們通過以下Demo查看方法在編譯時期的排列順序,在viewController中按下列順序定義,以下幾個方法
@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ù)對應(yīng)的路徑查找link map文件,如下所示,可以發(fā)現(xiàn)類中函數(shù)的加載順序是從上到下的。

* 而文件順序是根據(jù)Build Phases -> Compile Sources中的順序加載的。

思路
由上面的測試,可以看到,文件加載以及函數(shù)的調(diào)用的順序會影響page fault的數(shù)量,有極大的可能,在啟動時刻調(diào)用的方法是不同的page的,所以我們可以將我們在啟動時刻調(diào)用的方法排列在同一頁中,如此一來就可以減少觸發(fā)page fault的次數(shù),這也就是二進(jìn)制重排原理。

- 注意:在iOS生產(chǎn)環(huán)境的app,在發(fā)生page fault進(jìn)行重新加載時,iOS系統(tǒng)還會對其做一次
簽名驗證,因此iOS在生產(chǎn)環(huán)境的page Fault比Debug環(huán)境下所產(chǎn)生的耗時更多時間。
實現(xiàn)二進(jìn)制重排
名詞解釋
Link Map
- Linkmap是iOS編譯過程中間的產(chǎn)物,
紀(jì)錄了二進(jìn)制的文件佈局,需要再Xcode的Build Settings裡開啟Write Link Map File, Link Map 主要包含三個部分:-
Object Files生成二進(jìn)制用到的link單元的路徑和文件編號 -
Sections紀(jì)錄Mach-O每個Segment/section的地址範(fàn)圍 -
Symbols按順序紀(jì)錄每個符號的地址範(fàn)圍
-
ld
- ld是Xcode使用的鏈接器,有一個參數(shù)order_file,我們可以通過在
Build Settings -> Order File配置一個後綴為order的文件路徑。在這個order文件中,將所需要的符號按照順序?qū)懺谘e面,在項目編譯時,會按照這個文件的順序進(jìn)行加載,以此來達(dá)到我們的優(yōu)化。也就是說二進(jìn)制重排的本質(zhì)就是對啟動加載的符號進(jìn)行重新排列。 - 但是我們要如何獲取我們的函數(shù)呢?以下有幾個思路...
- hook objc_msgSend:我們知道,函數(shù)的本質(zhì)是發(fā)送消息,在底層都會來到
objc_msgSend,但是由於objc_msgSend的參數(shù)是可變的,需要通過彙編獲取,對開發(fā)人員要求較高,且也只能拿到OC和swift中@objc後的方法。 - 靜態(tài)掃描:掃描Mach-O特定段ㄉ和節(jié)裡面所存儲的符號以及函數(shù)數(shù)據(jù)
- Clang插樁:批量hook,可以實現(xiàn)100%符號覆蓋,最完全獲取swift,OC,block函數(shù)
- hook objc_msgSend:我們知道,函數(shù)的本質(zhì)是發(fā)送消息,在底層都會來到
Clang 插樁
- llvm內(nèi)置了一個簡單的代碼覆蓋率檢測(
SanitizerCoverage)。他對於基本塊級邊緣級插入對用戶定義函數(shù)的調(diào)用。接下來介紹的批量hook,就需要借助於sanitizerCoverage。 - 關(guān)於clang的插樁覆蓋的官方文檔如下:
- 文檔中有詳細(xì)描述即簡短的Demo演示。
第一步:開啟SanitizerCoverage
- OC項目:
Build Settings→Other C Flags添加fsanitize-coverage=func,trace-pc-guard - Swift項目:須額外在
Build Settings→Other Swift Flags中加入sanitize-coverage=func和sanitize=undefined - 所有鏈接到App中的二進(jìn)制都需要開啟
SanitizerCoverage,這樣才能完全覆蓋到所有調(diào)用 - 也可以通過
podfile來配置參數(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
第二步:重寫方法,新建一個OC文件ACOrderFile,重寫兩個方法
-
__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是一個指針,指向無符號int類,4個字節(jié),相當(dāng)於一個數(shù)組的起始位置,即符號的起始位置(是從高位往低位讀)
-

-
參數(shù)2:Stop也是一個指針,由於取數(shù)據(jù)是從高地址往低地址讀取的,所以如果直接讀取stop的地址並不是stop真正的地址,而是讀取到最後的地址,所以讀取stop時,由於stop占4個字節(jié),stop真實地址 = stop打印的地址-0x4

- stop內(nèi)存地址中存儲的值表示什麼呢?我們可以增加一個方法/C++/屬性的方法(多3個),發(fā)現(xiàn)其值也會增加對應(yīng)的數(shù),例如我們增加一個test1方法??梢钥吹綌?shù)據(jù)從1d變成了1e

-
__sanitizer_cov_trace_pc_guard方法,主要是捕獲所有的啟動時刻的符號,將所有符號入隊 - 參數(shù)guard是一個哨兵,告訴我們是第幾個被調(diào)用的
- 符號的存儲需要借助於鏈表,所以需要定義鏈表節(jié)點ACNode
- 通過
OSQueueHead創(chuàng)建原子隊列,其目的是保證讀寫的安全 - 通過
OSAtomicEnqueue方法將node入隊,通過鏈表的next指針可以訪問下一個符號
//原子隊列,其目的是保證寫入安全,線程安全
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定義符號結(jié)構(gòu)體,以鏈表的形式
typedef struct {
void *pc;
void *next;
}ACNode;
/*
- start:起始位置
- stop:並不是最後一個符號地址,而是整個符號表的最後一個地址,
最後一個符號的地址=stop-4
(因為讀取數(shù)據(jù)是從高地址往低地址讀取的,且stop是一個無符號int類型,占4個字節(jié))。
stop存儲的值是符號
*/
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)用,用於捕捉符號,是多線程進(jìn)行的,
這個方式只儲存pc,以鏈表的形式
- guard 是一個哨兵,告訴我們是第幾個被調(diào)用的
*/
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//將load方法過濾掉了,所以需要注釋掉
// if (!*guard) return;
//獲取PC
/*
- PC 當(dāng)前函數(shù)返回上一個調(diào)用的地址
- 0 當(dāng)前這個函數(shù)地址,即當(dāng)前函數(shù)的返回地址
- 1 當(dāng)前函數(shù)調(diào)用者的地址,即上一個函數(shù)的返回地址
*/
void *PC = __builtin_return_address(0);
//創(chuàng)建node,並賦值
ACNode *node = malloc(sizeof(ACNode));
*node = (ACNode){PC, NULL};
//加入隊列
//符號的訪問不是通過下標(biāo)訪問,是通過鏈表的next指針,
//所以需要借用offsetof(結(jié)構(gòu)體類型,下一個的地址即next)
OSAtomicEnqueue(&queue, node, offsetof(ACNode, next));
}
第三步:獲取所有符號並寫入文件
- while循環(huán)從隊列中取出符號,處理非OC方法的前綴,存入數(shù)組中
- 數(shù)組取反,因為入隊儲存的順序是反序的。
- 數(shù)組去重,並移除本身方法的符號
- 將數(shù)組中的符號轉(zhuǎn)成字符串並寫入到AC.oder文件中
- 另外也可以在第一個
didFinishLaunchingWithOptions根視圖,獲取符號(這部分可以由自己決定,以下範(fàn)例是在touchesBegan方法調(diào)用時獲取)
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSMutableArray <NSString *> * symbolNames = [NSMutableArray array];
//while 循環(huán)取符號
while (YES) {
//出隊
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方法,如果不是需要加下滑線,反之,則直接存儲
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString * symbolName = isObjc ? name: [@"_" stringByAppendingString:name];
[symbolNames addObject:symbolName];
}
//取反 (隊列的存儲是反序的)
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"];
//字符串寫入文件
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"AC.order"];
NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
NSLog(@"%@",funcStr);
}
注意點:避免死循環(huán)
-
Build Settings -> Other C Flags的如果配置的是-fsanitize-coverage=trace-pc-guard,在while部分會出現(xiàn)死循環(huán)(我們在touchBegin方法中調(diào)適)

- 我們透過彙編查看,發(fā)現(xiàn)有三個地方調(diào)用
__sanitizer_cov_trace_pc_guard的調(diào)用 - 第一次 touchBegin調(diào)用
__sanitizer_cov_trace_pc_guard

- 第二次bl跳轉(zhuǎn)跳轉(zhuǎn)因為while循環(huán),只要跳轉(zhuǎn)就會被hook,即有b,bl指令,就會被hook
- 第三次bl是printf

解決方式是將BuildSetting中的other C Flags改成-fsanitize-coverage=func,trace-pc-guard
第四步 拷貝文件,放入指定位置 並配置路徑
- 一般將該文件放入主項目的路徑下,並在
Build Settings -> Order File中配置./AC.order,下面是配置前後的對比 - 沒有配置,照序加載

-
有配置,依照啟動時時刻所需(自己配置的.oder文件)加載
