虛擬內(nèi)存
????在了解二進(jìn)制重排之前,我們先了解虛擬內(nèi)存趟薄,詳細(xì)的可以查看iOS 系統(tǒng)是怎么管理內(nèi)存的。
????電腦中所運(yùn)行的程序均需經(jīng)由內(nèi)存執(zhí)行荷并,若執(zhí)行的程序占用內(nèi)存很大或很多赏枚,則會(huì)導(dǎo)致內(nèi)存消耗殆盡。為解決該問題脓规,Windows 中運(yùn)用了虛擬內(nèi)存技術(shù)栽连,即勻出一部分硬盤空間來(lái)充當(dāng)內(nèi)存使用。主要用于解決當(dāng)多個(gè)進(jìn)程同時(shí)存在時(shí),對(duì)物理內(nèi)存的管理秒紧。提高了CPU的利用率绢陌,使多個(gè)進(jìn)程可以同時(shí)、按需加載熔恢。所以脐湾,虛擬內(nèi)存其本質(zhì)就是一張?zhí)摂M地址和物理地址對(duì)應(yīng)關(guān)系的映射表。
Page Fault
????在虛擬內(nèi)存部分叙淌,當(dāng)進(jìn)程訪問一個(gè)虛擬內(nèi)存page秤掌,而對(duì)應(yīng)的物理內(nèi)存不存在時(shí),會(huì)觸發(fā)缺頁(yè)中斷(Page Fault)鹰霍,因此阻塞進(jìn)程闻鉴。此時(shí)就需要先加載數(shù)據(jù)到物理內(nèi)存,然后再繼續(xù)訪問衅谷。這個(gè)對(duì)性能是有一定影響的椒拗。基于Page Fault获黔,App在冷啟動(dòng)過程中蚀苛,會(huì)有大量的類、分類玷氏、三方等需要加載和執(zhí)行堵未,此時(shí)的產(chǎn)生的 Page Fault 所帶來(lái)的的耗時(shí)是很大的。
二進(jìn)制重排原理
????編譯器在生成二進(jìn)制代碼的時(shí)候盏触,默認(rèn)按照鏈接的 Object File(.o)
順序(也就是 Targets->Build Phases->Compile Sources 中的文件順序)寫文件渗蟹,按照 Object File
內(nèi)部的函數(shù)順序?qū)懞瘮?shù)。
靜態(tài)庫(kù)文件.a就是一組.o文件的ar包赞辩,可以用ar -t查看.a包含的所有.o雌芽。
????如下圖,page1 和 page2辨嗽,其中 method1
和 method3
啟動(dòng)時(shí)候需要調(diào)用世落,為了執(zhí)行對(duì)應(yīng)的代碼,系統(tǒng)必須進(jìn)行兩次Page Fault糟需。
????但如果把
method1
和method3
排布到一起屉佳,那么只需要一個(gè) Page Fault 即可,這就是二進(jìn)制文件重排的核心原理: 將所有啟動(dòng)時(shí)刻需要調(diào)用的方法排列在一起獲取啟動(dòng)階段的 page fault 次數(shù)
通過 System Trace 拿到某個(gè)時(shí)間段的 page fault 次數(shù)
????日常開發(fā)中性能分析是用最多的工具無(wú)疑是Time Profiler洲押,但Time Profiler是基于采樣的武花,并且只能統(tǒng)計(jì)線程實(shí)際在運(yùn)行的時(shí)間,而發(fā)生Page Fault的時(shí)候線程是被blocked杈帐,所以我們需要用一個(gè)不常用但功能卻很強(qiáng)大的工具:System Trace体箕。
????選中主線程,在 VM Activity 中的 File Backed Page In 次數(shù)就是 Page Fault 次數(shù),并且雙擊還能按時(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 單元的路徑和文件編號(hào)
Sections 記錄 Mach-O 每個(gè) Segment/section 的地址范圍
Symbols 按順序記錄每個(gè)符號(hào)的地址范圍
????通過 Link map 文件的查看,我們可以看到 在 Symbols
中有著二進(jìn)制的函數(shù)布局争群。
通過 Order File 讓鏈接器按照指定順序生成 Mach-O
????Xcode 使用的鏈接器件是 ld ,ld 有一個(gè)不常用的參數(shù) -order_file
大年,我們可以通過在 Build Settings -> Order File 配置一個(gè)后綴為 .order
的文件路徑换薄。 通過 Order File 我們可以更改函數(shù)和數(shù)據(jù)布局的順序。當(dāng)然翔试,我們最好不要在調(diào)試或開發(fā)配置中指定 Order File轻要,因?yàn)檫@會(huì)使鏈接的二進(jìn)制文件對(duì)調(diào)試器的可讀性降低。僅在發(fā)布時(shí)使用 Order File 垦缅。
不需要擔(dān)心 Order File 中的符號(hào)是不存在的冲泥,因?yàn)?ld 會(huì)忽略這些符號(hào)
那么,如何編寫自己的 .order
文件呢壁涎?可以參考下面的示例:
test //函數(shù)
[ViewController orderTest]//方法
當(dāng)我們編寫完 Order File 文件后凡恍,重新編譯工程就可以在 LinkMap 文件中查看到已經(jīng)調(diào)整后的二進(jìn)制函數(shù)布局
Clang 插樁
還剩下最后一個(gè),也是最核心的一個(gè)問題怔球,獲取啟動(dòng)時(shí)候用到的函數(shù)符號(hào)嚼酝。LLVM 內(nèi)置了一個(gè)簡(jiǎn)單的代碼覆蓋率檢測(cè) SanitizerCoverage。它在函數(shù)級(jí)竟坛、基本塊級(jí)和邊緣級(jí)插入對(duì)用戶定義函數(shù)的調(diào)用闽巩。提供了這些回調(diào)的默認(rèn)實(shí)現(xiàn),并實(shí)現(xiàn)了簡(jiǎn)單的覆蓋率報(bào)告和可視化担汤。
配置 SanitizerCoverage
-
工程 Target 配置
在 Targets->Build Settings -> Other C Flags 中添加-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
實(shí)現(xiàn) SanitizerCoverage 的方法
實(shí)現(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ù)符號(hào)
使用-fsanitize-coverage=func,trace-pc-guard
編譯器將會(huì)在每個(gè)函數(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ù)符號(hào)信息隅很,我們可以將 App 啟動(dòng)過程中將這些信息去重存儲(chǔ)到數(shù)組中,啟動(dòng)完成后在沙盒中新建 .order 文件驾荣,并將數(shù)據(jù)寫入外构。
最后,將 .order
文件從沙盒中取出播掷,配置到工程 Order File 审编。