參考鏈接: 抖音研發(fā)實(shí)踐:基于二進(jìn)制文件重排的解決方案 APP啟動(dòng)速度提升超15%
一、原理
1痪伦、虛擬內(nèi)存和物理內(nèi)存
早期計(jì)算機(jī)沒(méi)有虛擬地址,一旦加載都會(huì)全部加載到內(nèi)存中,而且進(jìn)程都是按順序排列的谦屑,這樣別的進(jìn)程只需要把自己的地址加一些就能訪問(wèn)到別的進(jìn)程這樣就很不安全。
現(xiàn)在軟件發(fā)展的比硬件快篇梭,軟件占用的內(nèi)存越來(lái)越大氢橙,這就導(dǎo)致計(jì)算機(jī)的內(nèi)存不夠用,當(dāng)開(kāi)啟多個(gè)軟件時(shí)候恬偷,如果內(nèi)存不夠用就只能等待悍手,只有等前面的軟件關(guān)掉后才能加載打開(kāi),這就是早期計(jì)算機(jī)有時(shí)候?yàn)樯吨挥邪亚懊娴能浖P(guān)掉才能打開(kāi)新軟件的原因袍患。用戶使用軟件時(shí)候并不是使用到全部?jī)?nèi)存坦康,只會(huì)使用到一部分,如果軟件一打開(kāi)就把軟件全部加載到內(nèi)存中诡延,這樣會(huì)很浪費(fèi)內(nèi)存空間滞欠。
基于上面原因虛擬內(nèi)存技術(shù)出現(xiàn)了,軟件打開(kāi)后肆良,軟件自己以為有一大片內(nèi)存空間筛璧,但實(shí)際上是虛擬的,而虛擬內(nèi)存和物理內(nèi)存是通過(guò)一張表來(lái)關(guān)聯(lián)的惹恃,可以看下下面兩張表:
進(jìn)程1運(yùn)行時(shí)候會(huì)開(kāi)辟一塊內(nèi)存空間夭谤,但訪問(wèn)到內(nèi)存條的時(shí)候并不是這塊內(nèi)存空間,而且通過(guò)訪問(wèn)地址通過(guò)進(jìn)程1的映射表映射到不同的物理內(nèi)存空間巫糙,這個(gè)叫地址翻譯朗儒,這個(gè)過(guò)程需要CPU和操作系統(tǒng)配合,因?yàn)檫@個(gè)映射表是操作系統(tǒng)來(lái)管理的参淹,當(dāng)我們調(diào)試時(shí)候發(fā)現(xiàn)訪問(wèn)數(shù)據(jù)的內(nèi)存地址都是連續(xù)的醉锄,其實(shí)這是一個(gè)假象,在這個(gè)進(jìn)程內(nèi)部可以訪問(wèn)浙值,是因?yàn)樵L問(wèn)時(shí)候會(huì)通過(guò)該進(jìn)程的內(nèi)存映射表去拿到真正的物理內(nèi)存地址恳不,假如其他進(jìn)程訪問(wèn)的話,其他進(jìn)程沒(méi)有相應(yīng)的映射表亥鸠,自然就訪問(wèn)不到真正的物理內(nèi)存地址妆够,這樣就解決了內(nèi)存安全問(wèn)題识啦。
內(nèi)存使用率問(wèn)題:
內(nèi)存分頁(yè)管理,映射表不能以字節(jié)為單位神妹,是以頁(yè)為單位颓哮,Linux是以4K為一頁(yè),iOS是以16K位一頁(yè)鸵荠,但是mac系統(tǒng)是4K一頁(yè)冕茅,可以在mac終端輸入pageSize,發(fā)現(xiàn)返回的是4096蛹找。
為啥分頁(yè)后內(nèi)存就夠用呢姨伤,因?yàn)閼?yīng)用內(nèi)存是虛擬的,所以當(dāng)程序啟動(dòng)時(shí)候程序會(huì)認(rèn)為自己有很多的內(nèi)存:
在應(yīng)用加載時(shí)候不會(huì)把所有數(shù)據(jù)放內(nèi)存中庸疾,因?yàn)閿?shù)據(jù)是懶加載乍楚,當(dāng)進(jìn)程訪問(wèn)虛擬地址時(shí)候,首先看頁(yè)表届慈,如果發(fā)現(xiàn)該頁(yè)表數(shù)據(jù)為0徒溪,說(shuō)明該頁(yè)面數(shù)據(jù)未在物理地址上,這個(gè)時(shí)候系統(tǒng)會(huì)阻塞該進(jìn)程金顿,這個(gè)行為就叫做頁(yè)中斷(page Fault)臊泌,也叫缺頁(yè)異常,然后將磁盤(pán)中對(duì)應(yīng)頁(yè)面的數(shù)據(jù)加載到內(nèi)存中揍拆,然后讓虛擬內(nèi)存指向剛加載的物理內(nèi)存渠概,將數(shù)據(jù)加載到內(nèi)存中時(shí)候,如果有空的內(nèi)存空間嫂拴,就放空的內(nèi)存空間中播揪,如果沒(méi)有的話,就會(huì)去覆蓋其他進(jìn)程的數(shù)據(jù)顷牌,具體怎么覆蓋操作系統(tǒng)有一個(gè)算法剪芍,這樣永遠(yuǎn)都會(huì)保證當(dāng)前進(jìn)程的使用塞淹,這就是靈活管理內(nèi)存窟蓝。
但是這時(shí)候有個(gè)問(wèn)題,虛擬內(nèi)存解決了安全和效率問(wèn)題饱普,但是出現(xiàn)了另個(gè)安全問(wèn)題运挫,因?yàn)樘摂M內(nèi)存在編譯鏈接時(shí)候就確定了,那么黑客很容易通過(guò)分析拿到對(duì)應(yīng)的虛擬內(nèi)存去操作 套耕,這樣就造成所有的代碼都很好hook谁帕,代碼注入,這個(gè)時(shí)候就出現(xiàn)了新技術(shù)ASLR(Address space layout randomization 地址空間隨機(jī)化)冯袍,就是進(jìn)程每次加載的時(shí)候都會(huì)給一個(gè)隨機(jī)的偏移量匈挖,這樣就保證每次加載進(jìn)程時(shí)候虛擬內(nèi)存也在變化碾牌,iOS從iOS4就開(kāi)始了。
二進(jìn)制重拍:
因?yàn)樘摂M內(nèi)存中有個(gè)很大問(wèn)題就是缺頁(yè)中斷儡循,這個(gè)操作很耗時(shí)間舶吗,并且iOS不僅僅是將數(shù)據(jù)加載到內(nèi)存,還要對(duì)這頁(yè)做簽名認(rèn)證择膝,所以iOS耗時(shí)比較長(zhǎng)誓琼,并且每頁(yè)耗時(shí)有很大差距,0.1ms到0.8毫秒肴捉,使用過(guò)程中可能時(shí)間段感覺(jué)不到腹侣,但是啟動(dòng)時(shí)候會(huì)有很多數(shù)據(jù)要加載,這樣就會(huì)導(dǎo)致耗時(shí)很長(zhǎng)齿穗,假如我們啟動(dòng)時(shí)候在不同頁(yè)面傲隶,因?yàn)榇a在machO的位置不是根據(jù)調(diào)用瞬間,而是通過(guò)文件編譯的位置來(lái)的窃页,有可能啟動(dòng)時(shí)候在運(yùn)行時(shí)候會(huì)調(diào)用很多次page Fault伦籍,那么如果把所有啟動(dòng)時(shí)候的代碼都放在一頁(yè)或者兩頁(yè),這樣就很大程度上優(yōu)化啟動(dòng)速度腮出,這種方法就叫做二進(jìn)制重拍帖鸦。
進(jìn)程如果能直接訪問(wèn)物理內(nèi)存無(wú)疑是很不安全的,所以操作系統(tǒng)在物理內(nèi)存的上又建立了一層虛擬內(nèi)存胚嘲。為了提高效率和方便管理作儿,又對(duì)虛擬內(nèi)存和物理內(nèi)存又進(jìn)行分頁(yè)(Page)。當(dāng)進(jìn)程訪問(wèn)一個(gè)虛擬內(nèi)存Page而對(duì)應(yīng)的物理內(nèi)存卻不存在時(shí)馋劈,會(huì)觸發(fā)一次缺頁(yè)中斷(Page Fault)攻锰,分配物理內(nèi)存,有需要的話會(huì)從磁盤(pán)mmap讀人數(shù)據(jù)妓雾。
通過(guò)App Store渠道分發(fā)的App娶吞,Page Fault還會(huì)進(jìn)行簽名驗(yàn)證,所以一次Page Fault的耗時(shí)比想象的要多械姻!
編譯器在生成二進(jìn)制代碼的時(shí)候妒蛇,默認(rèn)按照鏈接的Object File(.o)順序?qū)懳募凑誒bject File內(nèi)部的函數(shù)順序?qū)懞瘮?shù)楷拳。
靜態(tài)庫(kù)文件.a就是一組.o文件的ar包绣夺,可以用ar -t查看.a包含的所有.o。
默認(rèn)布局:
簡(jiǎn)化問(wèn)題:假設(shè)我們只有兩個(gè)page:page1/page2欢揖,其中綠色的method1和method3啟動(dòng)時(shí)候需要調(diào)用陶耍,為了執(zhí)行對(duì)應(yīng)的代碼,系統(tǒng)必須進(jìn)行兩個(gè)Page Fault她混。
但如果我們把method1和method3排布到一起烈钞,那么只需要一個(gè)Page Fault即可泊碑,這就是二進(jìn)制文件重排的核心原理。
重排之后毯欣,我們的經(jīng)驗(yàn)是優(yōu)化一個(gè)Page Fault蛾狗,啟動(dòng)速度提升0.6~0.8ms
。
二仪媒、實(shí)現(xiàn)
1沉桌、System Trace調(diào)試
首先優(yōu)化,要先學(xué)會(huì)調(diào)試算吩,只有調(diào)試才能發(fā)現(xiàn)需要優(yōu)化的地方留凭,我們知道內(nèi)存分虛擬內(nèi)存和物理內(nèi)存,而內(nèi)存是通過(guò)分頁(yè)管理的偎巢,當(dāng)我們啟動(dòng)的時(shí)候調(diào)用很多方法蔼夜,假如這些方法不在同一個(gè)page上面,就會(huì)造成缺頁(yè)中斷(page fault),而這個(gè)操作是要消耗時(shí)間的压昼,所以假如啟動(dòng)的方法都在一頁(yè)上面求冷,就會(huì)很大程度上減少啟動(dòng)時(shí)間的消耗,這個(gè)就需要用到二進(jìn)制重拍來(lái)將啟動(dòng)時(shí)候調(diào)用的方法放在同一個(gè)page上
-
首先我們打開(kāi)項(xiàng)目
command + i
打開(kāi)Instruments調(diào)試工具
-
選擇System Trace窍霞,這個(gè)軟件可以看到我們項(xiàng)目中每個(gè)線程的數(shù)據(jù):
-
點(diǎn)擊開(kāi)始后這里我們搜索Main thread匠题,選擇我們的app,然后點(diǎn)擊Main thread ,再到下面選擇Main Thread --> Virtual Memory(虛擬內(nèi)存)
這里面File Backed Page In就是page fault的次數(shù)但金。
當(dāng)我們把APP殺死后里面再啟動(dòng)韭山,結(jié)果發(fā)現(xiàn)File Backed Page In這個(gè)值變得很小,說(shuō)明APP就算殺死后冷溃,在啟動(dòng)不是冷啟動(dòng)钱磅,還是有一部數(shù)據(jù)在系統(tǒng)的緩存中。
要做到真正的冷啟動(dòng)似枕,我們可以把APP殺掉后啟動(dòng)多個(gè)手機(jī)里面的APP盖淡,然后再啟動(dòng)APP,發(fā)現(xiàn)File Backed Page In又變得很大凿歼。
說(shuō)明虛擬內(nèi)存是在系統(tǒng)中的褪迟,當(dāng)系統(tǒng)內(nèi)存不夠的時(shí)候,其他APP會(huì)覆蓋老的APP的虛擬內(nèi)存毅往。
二進(jìn)制重拍是在鏈接階段生成的牵咙,重排之后生成可執(zhí)行文件,所以我們只能在編譯階段來(lái)優(yōu)化攀唯,而無(wú)法對(duì)已生成的ipa進(jìn)行優(yōu)化。
2渴丸、二進(jìn)制重排
可以在XCode配置二進(jìn)制重拍侯嘀,首先要確定符號(hào)的順序另凌,才能知道怎么重拍。XCode使用的鏈接器叫做ld戒幔,ld有個(gè)參數(shù)叫order_file
吠谢,只要有這個(gè)文件,我們可以將文件的路徑告訴XCode诗茎,在order_file
文件中把符號(hào)的順序?qū)戇M(jìn)去工坊,XCode編譯的時(shí)候就會(huì)按照文件中的符號(hào)順序打包成二進(jìn)制可執(zhí)行文件。
-
在蘋(píng)果的objc4-750源碼中找到這種文件
-
打開(kāi)后是下面這種格式:
-
里面全是函數(shù)符號(hào)敢订,打開(kāi)項(xiàng)目王污,在build setting 里面搜索order file
-
這里面指定了order的文件路徑,因?yàn)橐坏┰谶@里指定了order file的路徑楚午,XCode就會(huì)在編譯的時(shí)候按照文件里面寫(xiě)進(jìn)去的順序昭齐。
現(xiàn)在寫(xiě)一個(gè)Demo,然后編譯矾柜,我們知道XCode編譯的時(shí)候文件會(huì)有一個(gè)鏈接阱驾,鏈接是按照Build Phases的Compile SourceL里面的文件順序?qū)?m文件轉(zhuǎn)換成.o文件,然后將這些.o文件鏈接在一起生成可執(zhí)行文件:
做一個(gè)實(shí)驗(yàn)怪蔑,在ViewController和AppDelegate里面都寫(xiě)一個(gè)load方法里覆,然后運(yùn)行
+(void)load
{
NSLog(@"ViewController");
}
+(void)load
{
NSLog(@"AppDelegate");
}
-
Build Phases的Compile Source順序:
-
運(yùn)行,看下打永掳辍:
-
把Compile Source順序改一下:
-
運(yùn)行后看打印結(jié)果:
打印順序跟Compile Source文件順序一樣租谈,驗(yàn)證了上面的結(jié)論
-
如何查看整個(gè)項(xiàng)目的符號(hào)順序呢,到Build Settings搜索
link map
-
Link Map就是我們鏈接的符號(hào)表捆愁,我們把它改成
YES
割去,這樣編譯的時(shí)候就會(huì)把鏈接的符號(hào)表給我們寫(xiě)出來(lái),command + R我們運(yùn)行下昼丑,然后在Products里面的.app文件呻逆,在我們Intermediates.noindex-->項(xiàng)目名.build--->Debug-iphoneos-->項(xiàng)目名.build--->項(xiàng)目名-LinkMap-normal-arm64.txt,這個(gè)文件里面就有鏈接的符號(hào)順序表
其中 Object files:就是鏈接了哪些.o文件
Sections:中
Address:
Size:
Segment:__TEXT
代碼代碼段菩帝,只可讀咖城;__DATA
是數(shù)據(jù)段,可讀可寫(xiě)
Section:
再下面就是我們關(guān)心的符號(hào):
Symbols:
Address:
方法代碼的地址
Size:
方法占用的空間
File:
文件的編號(hào)
Name:
.o文件里面的方法符號(hào)
對(duì)于Address
呼奢,我們從.app中拿到項(xiàng)目的可執(zhí)行文件宜雀,然后用MachOView打開(kāi),然后在Section中看下Assembly
符號(hào)表里的0x100004B70在MachOView對(duì)應(yīng)的value是匯編代碼握础,也就是我們寫(xiě)的代碼轉(zhuǎn)換成的匯編辐董,所以這個(gè)地址就是代碼地址,所以二進(jìn)制重拍就是把所有的代碼順序重新排一下禀综,把啟動(dòng)時(shí)候調(diào)用的代碼排到前面去简烘,減少啟動(dòng)時(shí)候加載page的數(shù)量(沒(méi)一個(gè)page大小是16K)
-
添加order file,我們創(chuàng)建一個(gè)hank.order文件苔严,在文件中寫(xiě)入:
-
放到工程的根目錄中,然后在Build setting里面搜下order file孤澎,然后在后面將該文件地址添加進(jìn)去:
-
Xcode在編譯時(shí)候就會(huì)按照order文件中的符號(hào)順序鏈接代碼了届氢,我們編譯一下,再看一下LinkMap-normal-arm64.txt文件
結(jié)果是按照order的符號(hào)順序來(lái)的覆旭,而且如果order里面寫(xiě)了項(xiàng)目中不存在的方法符號(hào)退子,XCode會(huì)自動(dòng)過(guò)濾掉,不存在影響型将。還有一種查看符號(hào)表的方法是在終端cd到項(xiàng)目可執(zhí)行文件的目錄寂祥,然后輸入。
nm 可執(zhí)行文件名
查看全部的符號(hào)茶敏,還有查看自定義方法的符號(hào)
nm -Up TraceDemo
查看系統(tǒng)的符號(hào)
nm -up TraceDemo
3壤靶、獲取APP啟動(dòng)時(shí)候調(diào)用的所有方法
以上就是二進(jìn)制重拍的步驟,但是如何知道APP啟動(dòng)時(shí)候的調(diào)用了哪些方法呢惊搏?
第一個(gè)方式:是用
fishHook
去hook 系統(tǒng)的objc_msgSend
這個(gè)函數(shù)贮乳,因?yàn)閛c的方法都是通過(guò)發(fā)送消息的形式,但是這個(gè)函數(shù)參數(shù)是可變的參數(shù)恬惯,所以只能通過(guò)匯編形式hook向拆,但是這種情況initialize和block以及直接調(diào)用函數(shù)方式hook不到。第二種方式:clang插裝形式: 官方文檔:clang
OC方法酪耳、函數(shù)浓恳、block都能hook到!
1碗暗、首先在Build Setting里面搜索Other C Flags 在里面添加參數(shù):-fsanitize-coverage=trace-pc-guard
-fsanitize-coverage=func,trace-pc-guard
2颈将、然后編譯,發(fā)現(xiàn)會(huì)報(bào)錯(cuò):提示報(bào)錯(cuò)
Showing Recent Messages
Undefined symbol: ___sanitizer_cov_trace_pc_guard_init
提示找不到__sanitizer_cov_trace_pc_guard
和__sanitizer_cov_trace_pc_guard_init
方法言疗。
看一下文檔晴圾,發(fā)現(xiàn)有測(cè)試代碼:
把這段代碼copy到項(xiàng)目中,發(fā)現(xiàn)噪奄,錯(cuò)誤沒(méi)有了
__sanitizer_cov_trace_pc_guard_init
分析一下__sanitizer_cov_trace_pc_guard_init
函數(shù)死姚,這里面有個(gè)start
和stop
,打個(gè)斷點(diǎn)勤篮,看一下start
和stop
內(nèi)存里面的值:
start里每4個(gè)字節(jié)里面都有一個(gè)數(shù)組都毒,而且是按照1、2碰缔、3账劲、4的順序排列的,再看一下stop,按照start的規(guī)則涤垫,減4個(gè)字節(jié)看一下姑尺,發(fā)現(xiàn)是13竟终,這里面存的是我們項(xiàng)目自定義文件中符號(hào)的數(shù)量蝠猬,無(wú)論是方法、函數(shù)還是block统捶,都會(huì)統(tǒng)計(jì)進(jìn)來(lái)榆芦,我們可以多加幾個(gè)方法或者函數(shù)、block試一下坡慌,就可以驗(yàn)證:
__sanitizer_cov_trace_pc_guard
我們?cè)俜治鲆幌?code>__sanitizer_cov_trace_pc_guard
我們運(yùn)行時(shí)候發(fā)現(xiàn)打印了好多guard
實(shí)現(xiàn)個(gè)個(gè)手勢(shì)
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
}
點(diǎn)擊一下屏幕芋绸,發(fā)現(xiàn)
點(diǎn)擊一下打印一下榜跌,猜測(cè)每執(zhí)行一個(gè)函數(shù)都會(huì)調(diào)用一次,說(shuō)明該函數(shù)hook了所有的方法崎淳,為了進(jìn)一步驗(yàn)證,定義一個(gè)函數(shù)和一個(gè)block愕把,在點(diǎn)擊屏幕時(shí)候調(diào)用一個(gè)函:
void(^block1)(void) = ^(void) {
};
void test(){
block1();
}
guard: 0x100d8381c a PC ?
guard: 0x100d83814 8 PC ?
guard: 0x100d83810 7 PC
發(fā)現(xiàn)點(diǎn)擊一次拣凹,該函數(shù)調(diào)用了三次
通過(guò)匯編驗(yàn)證一下,在toubegain恨豁、函數(shù)嚣镜、block出都加上斷點(diǎn),然后打開(kāi)匯編橘蜜,運(yùn)行
bl指令代表調(diào)用一個(gè)方法或者一個(gè)函數(shù) 菊匿,過(guò)掉這個(gè)斷點(diǎn)
test也調(diào)用,再過(guò)一下
block也調(diào)用了计福,當(dāng)我們配置了chang的代碼覆蓋工具跌捆,實(shí)現(xiàn)了上面兩個(gè)函數(shù),clang會(huì)以靜態(tài)插裝形式在所有方法象颖、函數(shù)block內(nèi)部插入一行代碼佩厚,而且是在第一行一開(kāi)始插入的,做到了全局的hook
我們?cè)僭诜治鱿?code>__sanitizer_cov_trace_pc_guard的作用力麸,我們現(xiàn)在這個(gè)函數(shù)里面加一個(gè)斷點(diǎn)
再運(yùn)行
在左邊發(fā)現(xiàn)有個(gè)函數(shù)調(diào)用棧可款,并且在每次調(diào)用方法時(shí)候都會(huì)調(diào)起
__sanitizer_cov_trace_pc_guard
函數(shù),而這個(gè)函數(shù)就是相應(yīng)方法調(diào)起來(lái)的實(shí)例代碼中有個(gè)PC克蚂,我們加個(gè)斷點(diǎn)打印一下這個(gè)PC看看闺鲸,先把啟動(dòng)時(shí)候的函數(shù)都過(guò)掉再打開(kāi)斷點(diǎn),然后點(diǎn)擊一下屏幕觸發(fā)touchesBegan的方法進(jìn)行攔截:
在控制欄中輸入bt埃叭,查看一下函數(shù)調(diào)用棧
看一下0x0000000104349abc這個(gè)地址的信息
發(fā)現(xiàn)這個(gè)地址是在touchesBegan里面摸恍,但是不在touchesBegan開(kāi)頭,我們把它
減4個(gè)字節(jié)
第一個(gè)指令是bl,這時(shí)才是touchesBegan的開(kāi)頭
在touchesBegan方法里面加一個(gè)斷點(diǎn)立镶,然后跳到touchesBegan方法里面壁袄,再打開(kāi)匯編:
bl是調(diào)用的意思,我們發(fā)現(xiàn)0x104349ab8是touchesBegan方法的開(kāi)頭媚媒,0x00000001000bdabc是調(diào)用下一個(gè)函數(shù)的指令的下一個(gè)地址嗜逻,PC打印的就是0x104349abc
再來(lái)看一下函數(shù)調(diào)用棧
調(diào)用棧的左邊是上一個(gè)函數(shù)的開(kāi)始地址,最后面有個(gè)+64
缭召,最后面那個(gè)數(shù)字是偏移量栈顷,也就是說(shuō)函數(shù)的開(kāi)始位置+偏移量
才是函數(shù)的真正的位置
,這個(gè)時(shí)候touchesBegan的偏移量是44
嵌巷,我們測(cè)試一下:
這才是touchesBegan的真正實(shí)現(xiàn)萄凤,也就是匯編的這一段
說(shuō)明在
__sanitizer_cov_trace_pc_guard
里面我們能拿到下一個(gè)函數(shù)調(diào)用的首地址:看一下
__sanitizer_cov_trace_pc_guard
的匯編調(diào)用最后面有個(gè)ret也就是return返回的意思,每個(gè)函數(shù)或者方法都有一個(gè)return搪哪, 在底層實(shí)現(xiàn)靡努,每一個(gè)函數(shù)調(diào)用完成后都會(huì)返回下一個(gè)需要調(diào)用的函數(shù)的地址,也就是匯編中每次bl的時(shí)候會(huì)把下次要調(diào)用的指令的地址存在x30中晓折,當(dāng)函數(shù)執(zhí)行時(shí)候遇到ret時(shí)候就會(huì)從x30中的值返回回去 ,例如我們點(diǎn)擊屏幕時(shí)候在
__sanitizer_cov_trace_pc_guard
加個(gè)斷點(diǎn)惑朦,然后讀取x30數(shù)據(jù),就得到了touchesBegan的地址所以__sanitizer_cov_trace_pc_guard中的
拿到的是下一個(gè)要調(diào)用的函數(shù)的地址已维,因?yàn)開(kāi)_sanitizer_cov_trace_pc_guard函數(shù)都是在hook函數(shù)前執(zhí)行的行嗤,所以在這里面拿到的函數(shù)地址就是我們hook的函數(shù)地址
既然能拿到函數(shù)地址,我們可以通過(guò)這個(gè)函數(shù)去拿到函數(shù)名稱垛耳。
#import <dlfcn.h>
dladdr(<#const void *#>, <#Dl_info *#>)
- 第一個(gè)參數(shù)是函數(shù)的地址栅屏。
第二個(gè)參數(shù)是一個(gè)結(jié)構(gòu)體指針。
我們看看這個(gè)結(jié)構(gòu)體格式
typedef struct dl_info {
const char *dli_fname; /* Pathname of shared object */
void *dli_fbase; /* Base address of shared object */
const char *dli_sname; /* Name of nearest symbol */
void *dli_saddr; /* Address of nearest symbol */
} Dl_info;
void *PC = __builtin_return_address(0);
Dl_info info;
dladdr(PC, &info);
printf("fname:%s \nfbase:%p \nsname:%s \nsaddr:%p\n",
info.dli_fname,
info.dli_fbase,
info.dli_sname,
info.dli_saddr);
打犹孟省:
fname:/private/var/containers/Bundle/Application/38C6E838-7D51-4546-9882-BF5858D08C16/TraceDemo.app/TraceDemo
fbase:0x1000e0000
sname:-[ViewController touchesBegan:withEvent:]
saddr:0x1000e5a0c
- fname:文件路徑
- fbase:文件地址
- sname:函數(shù)符號(hào)名稱
- saddr:函數(shù)符號(hào)地址栈雳,也就是函數(shù)的起始地址
當(dāng)我們能拿到項(xiàng)目所有調(diào)用函數(shù)的符號(hào)時(shí)候,我們就能通過(guò)這種方法來(lái)拿到APP啟動(dòng)時(shí)候調(diào)用的所有的函數(shù)缔莲、方法哥纫、block符號(hào),然后創(chuàng)建order文件進(jìn)行自動(dòng)二進(jìn)制重拍上代碼:
//原子隊(duì)列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定義符號(hào)結(jié)構(gòu)體
typedef struct {
void *pc;
void *next;
}SYNode;
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// if (!*guard) return; // Duplicate the guard check.
/* 精確定位 哪里開(kāi)始 到哪里結(jié)束! 在這里面做判斷寫(xiě)條件!*/
void *PC = __builtin_return_address(0);
SYNode *node = malloc(sizeof(SYNode));
*node = (SYNode){PC,NULL};
//進(jìn)入,因?yàn)樵摵瘮?shù)可能在子線程中操作痴奏,所以用原子性操作蛀骇,保證線程安全
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
//
}
-(void)createOrderFile{
NSMutableArray <NSString *> * symbolNames = [NSMutableArray array];
while (YES) {
SYNode * node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
if (node == NULL) {
break;
}
Dl_info info;
dladdr(node->pc, &info);
NSString * name = @(info.dli_sname);
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:@"hank.order"];
NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
NSLog(@"%@",funcStr);
}