虛擬內(nèi)存
????在了解二進(jìn)制重排之前,我們先了解虛擬內(nèi)存,詳細(xì)的可以查看iOS 系統(tǒng)是怎么管理內(nèi)存的。
????電腦中所運行的程序均需經(jīng)由內(nèi)存執(zhí)行,若執(zhí)行的程序占用內(nèi)存很大或很多,則會導(dǎo)致內(nèi)存消耗殆盡。為解決該問題,Windows 中運用了虛擬內(nèi)存技術(shù),即勻出一部分硬盤空間來充當(dāng)內(nèi)存使用。主要用于解決當(dāng)多個進(jìn)程同時存在時,對物理內(nèi)存的管理。提高了CPU的利用率,使多個進(jìn)程可以同時、按需加載。所以,虛擬內(nèi)存其本質(zhì)就是一張?zhí)摂M地址和物理地址對應(yīng)關(guān)系的映射表。
Page Fault
????在虛擬內(nèi)存部分,當(dāng)進(jìn)程訪問一個虛擬內(nèi)存page,而對應(yīng)的物理內(nèi)存不存在時,會觸發(fā)缺頁中斷(Page Fault),因此阻塞進(jìn)程。此時就需要先加載數(shù)據(jù)到物理內(nèi)存,然后再繼續(xù)訪問。這個對性能是有一定影響的。基于Page Fault,App在冷啟動過程中,會有大量的類、分類、三方等需要加載和執(zhí)行,此時的產(chǎn)生的 Page Fault 所帶來的的耗時是很大的。
二進(jìn)制重排原理
????編譯器在生成二進(jìn)制代碼的時候,默認(rèn)按照鏈接的 Object File(.o)
順序(也就是 Targets->Build Phases->Compile Sources 中的文件順序)寫文件,按照 Object File
內(nèi)部的函數(shù)順序?qū)懞瘮?shù)。
靜態(tài)庫文件.a就是一組.o文件的ar包,可以用ar -t查看.a包含的所有.o。
????如下圖,page1 和 page2,其中 method1
和 method3
啟動時候需要調(diào)用,為了執(zhí)行對應(yīng)的代碼,系統(tǒng)必須進(jìn)行兩次Page Fault。
????但如果把
method1
和method3
排布到一起,那么只需要一個 Page Fault 即可,這就是二進(jìn)制文件重排的核心原理: 將所有啟動時刻需要調(diào)用的方法排列在一起獲取啟動階段的 page fault 次數(shù)
通過 System Trace 拿到某個時間段的 page fault 次數(shù)
????日常開發(fā)中性能分析是用最多的工具無疑是Time Profiler,但Time Profiler是基于采樣的,并且只能統(tǒng)計線程實際在運行的時間,而發(fā)生Page Fault的時候線程是被blocked,所以我們需要用一個不常用但功能卻很強(qiáng)大的工具:System Trace。
????選中主線程,在 VM Activity 中的 File Backed Page In 次數(shù)就是 Page Fault 次數(shù),并且雙擊還能按時序看到引起 Page Fault 的堆棧:
通過 LinkMap 拿到當(dāng)前二進(jìn)制的函數(shù)布局
????LinkMap 是 iOS 編譯過程的中間產(chǎn)物,記錄了二進(jìn)制文件的布局,需要將 Xcode 的 Build Settings -> Write Link Map File 設(shè)置為 YES
linkmap主要包括三大部分:
Object Files 生成二進(jìn)制用到的 link 單元的路徑和文件編號
Sections 記錄 Mach-O 每個 Segment/section 的地址范圍
Symbols 按順序記錄每個符號的地址范圍
????通過 Link map 文件的查看,我們可以看到 在 Symbols
中有著二進(jìn)制的函數(shù)布局。
通過 Order File 讓鏈接器按照指定順序生成 Mach-O
????Xcode 使用的鏈接器件是 ld ,ld 有一個不常用的參數(shù) -order_file
,我們可以通過在 Build Settings -> Order File 配置一個后綴為 .order
的文件路徑。 通過 Order File 我們可以更改函數(shù)和數(shù)據(jù)布局的順序。當(dāng)然,我們最好不要在調(diào)試或開發(fā)配置中指定 Order File,因為這會使鏈接的二進(jìn)制文件對調(diào)試器的可讀性降低。僅在發(fā)布時使用 Order File 。
不需要擔(dān)心 Order File 中的符號是不存在的,因為 ld 會忽略這些符號
那么,如何編寫自己的 .order
文件呢?可以參考下面的示例:
test //函數(shù)
[ViewController orderTest]//方法
當(dāng)我們編寫完 Order File 文件后,重新編譯工程就可以在 LinkMap 文件中查看到已經(jīng)調(diào)整后的二進(jìn)制函數(shù)布局
Clang 插樁
還剩下最后一個,也是最核心的一個問題,獲取啟動時候用到的函數(shù)符號。LLVM 內(nèi)置了一個簡單的代碼覆蓋率檢測 SanitizerCoverage。它在函數(shù)級、基本塊級和邊緣級插入對用戶定義函數(shù)的調(diào)用。提供了這些回調(diào)的默認(rèn)實現(xiàn),并實現(xiàn)了簡單的覆蓋率報告和可視化。
配置 SanitizerCoverage
-
工程 Target 配置
在 Targets->Build Settings -> Other C Flags 中添加-fsanitize-coverage=func,trace-pc-guard
-fsanitize-coverage=func,trace-pc-guard -
Podfile 配置
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' end end end
實現(xiàn) SanitizerCoverage 的方法
實現(xiàn) __sanitizer_cov_trace_pc_guard_init
和 __sanitizer_cov_trace_pc_guard
方法
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);
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return; // Duplicate the guard check.
void *PC = __builtin_return_address(0);
printf("guard: %p %x PC\n", guard, *guard);
}
獲取函數(shù)符號
使用-fsanitize-coverage=func,trace-pc-guard
編譯器將會在每個函數(shù)的邊緣插入 __sanitizer_cov_trace_pc_guard
函數(shù)。 __builtin_return_address(0)
返回當(dāng)前函數(shù)返回地址也就是當(dāng)前函數(shù)的調(diào)用者。我們通過 dladdr()函數(shù)根據(jù) PC 指針可以獲取到其相關(guān)信息。
#import <dlfcn.h>
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
void *PC = __builtin_return_address(0);
Dl_info info;
dladdr(PC, &info);
printf("sname:%s \nsaddr:%p \n",info.dli_sname,info.dli_saddr);
}
可以查看到
info.dli_sname
是我們想要的函數(shù)符號信息,我們可以將 App 啟動過程中將這些信息去重存儲到數(shù)組中,啟動完成后在沙盒中新建 .order 文件,并將數(shù)據(jù)寫入。
最后,將 .order
文件從沙盒中取出,配置到工程 Order File 。