日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 综合教程 >内容正文

综合教程

iOS二进制文件重排,启动速度提升超15%

發(fā)布時(shí)間:2023/12/13 综合教程 35 生活家
生活随笔 收集整理的這篇文章主要介紹了 iOS二进制文件重排,启动速度提升超15% 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

背景

啟動(dòng)是App給用戶的第一印象,對(duì)用戶體驗(yàn)至關(guān)重要。抖音的業(yè)務(wù)迭代迅速,如果放任不管,啟動(dòng)速度會(huì)一點(diǎn)點(diǎn)劣化。為此抖音iOS客戶端團(tuán)隊(duì)做了大量?jī)?yōu)化工作,除了傳統(tǒng)的修改業(yè)務(wù)代碼方式,我們還做了些開(kāi)拓性的探索,發(fā)現(xiàn)修改代碼在二進(jìn)制文件的布局可以提高啟動(dòng)性能,方案落地后在抖音上啟動(dòng)速度提高了約15%。

本文從原理出發(fā),介紹了我們是如何通過(guò)靜態(tài)掃描和運(yùn)行時(shí)trace找到啟動(dòng)時(shí)候調(diào)用的函數(shù),然后修改編譯參數(shù)完成二進(jìn)制文件的重新排布。

原理

Page Fault

進(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ì)從磁盤mmap讀人數(shù)據(jù)。

通過(guò)App Store渠道分發(fā)的App,Page Fault還會(huì)進(jìn)行簽名驗(yàn)證,所以一次Page Fault的耗時(shí)比想象的要多:

Page Fault

重排

編譯器在生成二進(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。

核心問(wèn)題

為了完成重排,有以下幾個(gè)問(wèn)題要解決:

重排效果怎么樣 - 獲取啟動(dòng)階段的page fault次數(shù)

重排成功了沒(méi) - 拿到當(dāng)前二進(jìn)制的函數(shù)布局

如何重排 - 讓鏈接器按照指定順序生成Mach-O

重排的內(nèi)容 - 獲取啟動(dòng)時(shí)候用到的函數(shù)

作為一個(gè)開(kāi)發(fā)者,有一個(gè)學(xué)習(xí)的氛圍跟一個(gè)交流圈子特別重要,這是一個(gè)我的iOS交流群:519832104 不管你是小白還是大牛歡迎入駐,分享經(jīng)驗(yàn),討論技術(shù),大家一起交流學(xué)習(xí)成長(zhǎng)!

另附上一份各好友收集的大廠面試題,需要iOS開(kāi)發(fā)學(xué)習(xí)資料、面試真題,可以添加iOS開(kāi)發(fā)進(jìn)階交流群,進(jìn)群可自行下載!

System Trace

日常開(kāi)發(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的堆棧:

System Trace

signpost

現(xiàn)在我們?cè)贗nstrument中已經(jīng)能拿到某個(gè)時(shí)間段的Page In次數(shù),那么如何和啟動(dòng)映射起來(lái)呢?

我們的答案是:os_signpost

os_signpost是iOS 12開(kāi)始引入的一組API,可以在Instruments繪制一個(gè)時(shí)間段,代碼也很簡(jiǎn)單:

1os_log_t?logger?=?os_log_create("com.bytedance.tiktok",?"performance");
2os_signpost_id_t?signPostId?=?os_signpost_id_make_with_pointer(logger,sign);
3//標(biāo)記時(shí)間段開(kāi)始
4os_signpost_interval_begin(logger,?signPostId,?"Launch","%{public}s",?"");
5//標(biāo)記結(jié)束
6os_signpost_interval_end(logger,?signPostId,?"Launch");

通常可以把啟動(dòng)分為四個(gè)階段處理:

啟動(dòng)階段

有多少個(gè)Mach-O,就會(huì)有多少個(gè)Load和C++靜態(tài)初始化階段,用signpost相關(guān)API對(duì)對(duì)應(yīng)階段打點(diǎn),方便跟蹤每個(gè)階段的優(yōu)化效果。

Linkmap

Linkmap是iOS編譯過(guò)程的中間產(chǎn)物,記錄了二進(jìn)制文件的布局,需要在Xcode的Build Settings里開(kāi)啟Write Link Map File:

Build Settings

比如以下是一個(gè)單頁(yè)面Demo項(xiàng)目的linkmap。

linkmap

linkmap主要包括三大部分:

Object Files 生成二進(jìn)制用到的link單元的路徑和文件編號(hào)

Sections 記錄Mach-O每個(gè)Segment/section的地址范圍

Symbols 按順序記錄每個(gè)符號(hào)的地址范圍

ld

Xcode使用的鏈接器件是ld,ld有一個(gè)不常用的參數(shù)-order_file,通過(guò)man ld可以看到詳細(xì)文檔:

Alters the order in which functions and data are laid out. For each section in the output file, any symbol in that section that are specified in the order file file is moved to the start of its section and laid out in the same order as in the order file file.

可以看到,order_file中的符號(hào)會(huì)按照順序排列在對(duì)應(yīng)section的開(kāi)始,完美的滿足了我們的需求。

Xcode的GUI也提供了order_file選項(xiàng):

order_file

如果order_file中的符號(hào)實(shí)際不存在會(huì)怎么樣呢?

ld會(huì)忽略這些符號(hào),如果提供了link選項(xiàng)-order_file_statistics,會(huì)以warning的形式把這些沒(méi)找到的符號(hào)打印在日志里。

獲得符號(hào)

還剩下最后一個(gè),也是最核心的一個(gè)問(wèn)題,獲取啟動(dòng)時(shí)候用到的函數(shù)符號(hào)。

我們首先排除了解析Instruments(Time Profiler/System Trace) trace文件方案,因?yàn)樗麄兌际腔谔囟▓?chǎng)景采樣的,大多數(shù)符號(hào)獲取不到。最后選擇了靜態(tài)掃描+運(yùn)行時(shí)Trace結(jié)合的解決方案。

Load

Objective C的符號(hào)名是+-[Class_name(category_name) method:name:],其中+表示類方法,-表示實(shí)例方法。

剛剛提到linkmap里記錄了所有的符號(hào)名,所以只要掃一遍linkmap的__TEXT,__text,正則匹配("^+[.* load]$")既可以拿到所有的load方法符號(hào)。

C++靜態(tài)初始化

C++并不像Objective C方法那樣,大部分方法調(diào)用編譯后都是objc_msgSend,也就沒(méi)有一個(gè)入口函數(shù)去運(yùn)行時(shí)hook。

但是可以用-finstrument-functions在編譯期插樁“hook”,但由于抖音的很多依賴由其他團(tuán)隊(duì)提供靜態(tài)庫(kù),這套方案需要修改依賴的構(gòu)建過(guò)程。二進(jìn)制文件重排在沒(méi)有業(yè)界經(jīng)驗(yàn)可供參考,不確定收益的情況下,選擇了并不完美但成本最低的靜態(tài)掃描方案。

1//__mod_init_func
20x100008060????0x00000008??[??5]?ltmp7
3//[??5]對(duì)應(yīng)的文件
4[??5]?.../Build/Products/Debug-iphoneos/libStaticLibrary.a(StaticLibrary.o)

2. 通過(guò)文件號(hào),解壓出.o。

1???lipo?libStaticLibrary.a?-thin?arm64?-output?arm64.a
2???ar?-x?arm64.a?StaticLibrary.o

3. 通過(guò).o,獲得靜態(tài)初始化的符號(hào)名_demo_constructor

1???objdump?-r?-section=__mod_init_func?StaticLibrary.o
2
3StaticLibrary.o:????file?format?Mach-O?arm64
4
5RELOCATION?RECORDS?FOR?[__mod_init_func]:
60000000000000000?ARM64_RELOC_UNSIGNED?_demo_constructor

4. 通過(guò)符號(hào)名,文件號(hào),在linkmap中找到符號(hào)在二進(jìn)制中的范圍:

10x100004A30????0x0000001C??[??5]?_demo_constructor

5. 通過(guò)起始地址,對(duì)代碼進(jìn)行反匯編:

1???objdump?-d?--start-address=0x100004A30?--stop-address=0x100004A4B?demo_arm64?
2
3_demo_constructor:
4100004a30:????fd?7b?bf?a9?????stp?x29,?x30,?[sp,?#-16]!
5100004a34:????fd?03?00?91?????mov?x29,?sp
6100004a38:????20?0c?80?52?????mov?w0,?#97
7100004a3c:????da?06?00?94?????bl??#7016?
8100004a40:????40?0c?80?52?????mov?w0,?#98
9100004a44:????fd?7b?c1?a8?????ldp?x29,?x30,?[sp],?#16
10100004a48:????d7?06?00?14?????b???#7004

6. 通過(guò)掃描bl指令掃描子程序調(diào)用,子程序在二進(jìn)制的開(kāi)始地址為:100004a3c +1b68(對(duì)應(yīng)十進(jìn)制的7016)。

1100004a3c:????da?06?00?94?????bl??#7016

7. 通過(guò)開(kāi)始地址,可以找到符號(hào)名和結(jié)束地址,然后重復(fù)5~7,遞歸的找到所有的子程序調(diào)用的函數(shù)符號(hào)。

小坑

STL里會(huì)針對(duì)string生成初始化函數(shù),這樣會(huì)導(dǎo)致多個(gè).o里存在同名的符號(hào),例如:

1__ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEC1IDnEEPKc

類似這樣的重復(fù)符號(hào)的情況在C++里有很多,所以C/C++符號(hào)在order_file里要帶著所在的.o信息:

1//order_file.txt
2libDemoLibrary.a(object.o):__GLOBAL__sub_I_demo_file.cpp

局限性

branch系列匯編指令除了bl/b,還有br/blr,即通過(guò)寄存器的間接子程序調(diào)用,靜態(tài)掃描無(wú)法覆蓋到這種情況。

Local符號(hào)

在做C++靜態(tài)初始化掃描的時(shí)候,發(fā)現(xiàn)掃描出了很多類似l002的符號(hào)。經(jīng)過(guò)一番調(diào)研,發(fā)現(xiàn)是依賴方輸出靜態(tài)庫(kù)的時(shí)候裁剪了local符號(hào)。導(dǎo)致__GLOBAL__sub_I_demo_file.cpp?變成了l002。

需要靜態(tài)庫(kù)出包的時(shí)候保留local符號(hào),CI腳本不要執(zhí)行strip -x,同時(shí)Xcode對(duì)應(yīng)target的Strip Style修改為Debugging symbol:

Strip Style

靜態(tài)庫(kù)保留的local符號(hào)會(huì)在宿主App生成IPA之前裁剪掉,所以不會(huì)對(duì)最后的IPA包大小有影響。宿主App的Strip Style要選擇All Symbols,宿主動(dòng)態(tài)庫(kù)選擇Non-Global Symbols。

Objective C方法

絕大部分Objective C的方法在編譯后會(huì)走objc_msgSend,所以通過(guò)fishhook(https://github.com/facebook/fishhook)?hook這一個(gè)C函數(shù)即可獲得Objective C符號(hào)。由于objc_msgSend是變長(zhǎng)參數(shù),所以hook代碼需要用匯編來(lái)實(shí)現(xiàn):

1//代碼參考InspectiveC
2__attribute__((naked))
3static?void?hook_Objc_msgSend()?{
4????save()
5????__asm?volatile?("mov?x2,?lr
");
6????__asm?volatile?("mov?x3,?x4
");
7????call(blr,?&before_objc_msgSend)
8????load()
9????call(blr,?orig_objc_msgSend)
10????save()
11????call(blr,?&after_objc_msgSend)
12????__asm?volatile?("mov?lr,?x0
");
13????load()
14????ret()
15}

子程序調(diào)用時(shí)候要保存和恢復(fù)參數(shù)寄存器,所以save和load分別對(duì)x0~x9, q0~q9入棧/出棧。call則通過(guò)寄存器來(lái)間接調(diào)用函數(shù):

1#define?save()?
2__asm?volatile?(?
3"stp?q6,?q7,?[sp,?#-32]!
"
4...
5
6#define?load()?
7__asm?volatile?(?
8"ldp?x0,?x1,?[sp],?#16
"?
9...
10
11#define?call(b,?value)?
12__asm?volatile?("stp?x8,?x9,?[sp,?#-16]!
");?
13__asm?volatile?("mov?x12,?%0
"?::?"r"(value));?
14__asm?volatile?("ldp?x8,?x9,?[sp],?#16
");?
15__asm?volatile?(#b?"?x12
");

before_objc_msgSend中用棧保存lr,在after_objc_msgSend恢復(fù)lr。由于要生成trace文件,為了降低文件的大小,直接寫(xiě)入的是函數(shù)地址,且只有當(dāng)前可執(zhí)行文件的Mach-O(app和動(dòng)態(tài)庫(kù))代碼段才會(huì)寫(xiě)入:

iOS中,由于ALSR(https://en.wikipedia.org/wiki/Address_space_layout_randomization)的存在,在寫(xiě)入之前需要先減去偏移量slide:

1IMP?imp?=?(IMP)class_getMethodImplementation(object_getClass(self),?_cmd);
2unsigned?long?imppos?=?(unsigned?long)imp;
3unsigned?long?addr?=?immpos?-?macho_slide

獲取一個(gè)二進(jìn)制的__text段地址范圍:

1unsigned?long?size?=?0;
2unsigned?long?start?=?(unsigned?long)getsectiondata(mhp,??"__TEXT",?"__text",?&size);
3unsigned?long?end?=?start?+?size;

獲取到函數(shù)地址后,反查linkmap既可找到方法的符號(hào)名。

Block

block是一種特殊的單元,block在編譯后的函數(shù)體是一個(gè)C函數(shù),在調(diào)用的時(shí)候直接通過(guò)指針調(diào)用,并不走objc_msgSend,所以需要單獨(dú)hook。

通過(guò)Block的源碼可以看到block的內(nèi)存布局如下:

1struct?Block_layout?{
2????void?*isa;
3????int32_t?flags;?//?contains?ref?count
4????int32_t?reserved;
5????void??*invoke;
6????struct?Block_descriptor1?*descriptor;
7};
8struct?Block_descriptor1?{
9????uintptr_t?reserved;
10????uintptr_t?size;
11};

其中invoke就是函數(shù)的指針,hook思路是將invoke替換為自定義實(shí)現(xiàn),然后在reserved保存為原始實(shí)現(xiàn)。

1//參考?https://github.com/youngsoft/YSBlockHook
2if?(layout->descriptor?!=?NULL?&&?layout->descriptor->reserved?==?NULL)
3{
4????if?(layout->invoke?!=?(void?*)hook_block_envoke)
5????{
6????????layout->descriptor->reserved?=?layout->invoke;
7????????layout->invoke?=?(void?*)hook_block_envoke;
8????}
9}

由于block對(duì)應(yīng)的函數(shù)簽名不一樣,所以這里仍然采用匯編來(lái)實(shí)現(xiàn)hook_block_envoke

1__attribute__((naked))
2static?void?hook_block_envoke()?{
3????save()
4????__asm?volatile?("mov?x1,?lr
");
5????call(blr,?&before_block_hook);
6????__asm?volatile?("mov?lr,?x0
");
7????load()
8????//調(diào)用原始的invoke,即resvered存儲(chǔ)的地址
9????__asm?volatile?("ldr?x12,?[x0,?#24]
");
10????__asm?volatile?("ldr?x12,?[x12]
");
11????__asm?volatile?("br?x12
");
12}

before_block_hook中獲得函數(shù)地址(同樣要減去slide)。

1intptr_t?before_block_hook(id?block,intptr_t?lr)
2{
3????Block_layout?*?layout?=?(Block_layout?*)block;
4????//layout->descriptor->reserved即block的函數(shù)地址
5????return?lr;
6}

同樣,通過(guò)函數(shù)地址反查linkmap既可找到block符號(hào)。

瓶頸

基于靜態(tài)掃描+運(yùn)行時(shí)trace的方案仍然存在少量瓶頸:

initialize hook不到

部分block hook不到

C++通過(guò)寄存器的間接函數(shù)調(diào)用靜態(tài)掃描不出來(lái)

目前的重排方案能夠覆蓋到80%~90%的符號(hào),未來(lái)我們會(huì)嘗試編譯期插樁等方案來(lái)進(jìn)行100%的符號(hào)覆蓋,讓重排達(dá)到最優(yōu)效果。

整體流程

流程

設(shè)置條件觸發(fā)流程

工程注入Trace動(dòng)態(tài)庫(kù),選擇release模式編譯出.app/linkmap/中間產(chǎn)物

運(yùn)行一次App到啟動(dòng)結(jié)束,Trace動(dòng)態(tài)庫(kù)會(huì)在沙盒生成Trace log

以Trace Log,中間產(chǎn)物和linkmap作為輸入,運(yùn)行腳本解析出order_file

總結(jié)

目前,在缺少業(yè)界經(jīng)驗(yàn)參考的情況下,我們成功驗(yàn)證了二進(jìn)制文件重排方案在iOS APP開(kāi)發(fā)中的可行性和穩(wěn)定性。基于二進(jìn)制文件重排,我們?cè)卺槍?duì)抖音的iOS客戶端上的優(yōu)化工作中,獲得了約15%的啟動(dòng)速度提升。

抽象來(lái)看,APP開(kāi)發(fā)中大家會(huì)遇到這樣一個(gè)通用的問(wèn)題,即在某些情況下,APP運(yùn)行需要進(jìn)行大量的Page Fault,這會(huì)影響代碼執(zhí)行速度。而二進(jìn)制文件重排方案,目前看來(lái)是解決這一通用問(wèn)題比較好的方案。

未來(lái)我們會(huì)進(jìn)行更多的嘗試,讓二進(jìn)制文件重排在更多的業(yè)務(wù)場(chǎng)景落地。

點(diǎn)擊此處,立即與iOS大牛交流學(xué)習(xí)

總結(jié)

以上是生活随笔為你收集整理的iOS二进制文件重排,启动速度提升超15%的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。