作者:CCO體系 尚紅澤
背景介紹
應(yīng)用安裝包的體積影響著用戶(hù)下載量、安裝時(shí)長(zhǎng)、用戶(hù)磁盤(pán)占用量等多個(gè)方面,據(jù)Google Play統(tǒng)計(jì),應(yīng)用體積每增加6MB,安裝的轉(zhuǎn)化率將下降1%。
安裝包的體積受諸多方面影響,針對(duì)dex、資源文件、so文件都有不同的優(yōu)化策略,在此不做一一展開(kāi),本文主要記錄了在研發(fā)時(shí)針對(duì)動(dòng)態(tài)鏈接庫(kù)的文件體積裁剪優(yōu)化方案。
我開(kāi)發(fā)的鏈接庫(kù)使用rust語(yǔ)言開(kāi)發(fā),通過(guò)安卓jni接口實(shí)現(xiàn)java層和native層之間的相互調(diào)用。為什么使用rust主要有以下幾個(gè)方面的考慮:
1.穩(wěn)。安卓的JNI接口調(diào)用復(fù)雜,又涉及到native層的內(nèi)存管理,隨著代碼量的增加,代碼的安全穩(wěn)定性會(huì)受到很大的挑戰(zhàn)。使用rust開(kāi)發(fā),開(kāi)發(fā)者幾乎不需要考慮GC的問(wèn)題,只要開(kāi)發(fā)的時(shí)候按照規(guī)范老老實(shí)實(shí)寫(xiě)代碼并且通過(guò)了編譯器的檢查,基本上就很難把程序?qū)懕?,這一點(diǎn)在代碼上線(xiàn)后也確實(shí)得到了驗(yàn)證。
2.安全。傳統(tǒng)使用C、C++開(kāi)發(fā)的代碼編譯完成以后,如果不加保護(hù),很容易使用反匯編工具破解,市面上比較成熟的工具如IDA、ghidra等都可以將匯編代碼還原到高級(jí)語(yǔ)言。使用rust編譯的產(chǎn)物,內(nèi)部函數(shù)間的調(diào)用規(guī)約和傳統(tǒng)都不一樣,目前市面上還沒(méi)有相對(duì)完善的反編譯工具,軟件的防破解能力直接上升一個(gè)數(shù)量級(jí)。
但是使用rust有一個(gè)非常明顯的缺點(diǎn)就是編譯產(chǎn)物體積過(guò)大。在不修改默認(rèn)的rust編譯選項(xiàng)的情況下,僅開(kāi)啟strip的情況下,我的動(dòng)態(tài)庫(kù)體積達(dá)到了495k。
優(yōu)化方案
參考網(wǎng)上前人的經(jīng)驗(yàn),依次進(jìn)行了以下優(yōu)化方式。
調(diào)整優(yōu)化等級(jí)
默認(rèn)的編譯優(yōu)化等級(jí)是O3,該優(yōu)化的目的提高代碼的運(yùn)行速度,但是與此同時(shí)會(huì)對(duì)部分循環(huán)進(jìn)行展開(kāi),體積造成膨脹。在此我們以縮減體積為目標(biāo),將優(yōu)化選項(xiàng)改為z,表示生成最小二進(jìn)制體積:
[profile.release] opt-level = 'z'
優(yōu)化后前后體積變化
編譯選項(xiàng) | 體積 |
---|---|
strip | 495k |
strip + opt-level = 'z' | 437k |
開(kāi)啟LTO
LTO(Link Time Optimization)可以在鏈接時(shí)消除冗余代碼,減小二進(jìn)制體積——代價(jià)是更長(zhǎng)的鏈接時(shí)間。
Cargo.toml [profile.release] opt-level = 'z' lto = true
優(yōu)化后前后體積變化
編譯選項(xiàng) | 體積 |
---|---|
strip | 495k |
strip + opt-level = 'z' | 437k |
strip + opt-level = 'z' + lto | 436k |
優(yōu)化效果非常不明顯,聊勝于無(wú)。
Panic立刻終止
rust默認(rèn)的panic會(huì)在崩潰時(shí)進(jìn)行?;厮荩奖愣ㄎ粏?wèn)題。然而會(huì)帶來(lái)額外的體積增加,將這一功能使用abort替代。
[profile.release] opt-level = 'z' lto = true panic = 'abort'
優(yōu)化后前后體積變化
編譯選項(xiàng) | 體積 |
---|---|
strip | 495k |
strip + opt-level = 'z' | 437k |
strip + opt-level = 'z' + lto | 436k |
strip + opt-level = 'z' + lto + panic = 'abort' | 366K |
到目前為止,常規(guī)的優(yōu)化手段已經(jīng)用完了,后續(xù)優(yōu)化需要配合一些代碼的額外變動(dòng)。
使用rust分析工具bloat對(duì)產(chǎn)物進(jìn)行分析,結(jié)果如下:
File .text Size Crate 4.1% 69.0% 192.7KiB std 1.0% 16.8% 46.9KiB jdmp 0.5% 8.1% 22.7KiB [Unknown] 0.2% 3.8% 10.5KiB jni 0.0% 0.5% 1.5KiB cesu8 0.0% 0.4% 1.1KiB adler32 0.0% 0.3% 904B bytes 0.0% 0.2% 640B aho_corasick 0.0% 0.2% 588B regex_syntax 0.0% 0.2% 572B regex_automata 0.0% 0.2% 440B log 0.0% 0.1% 304B memchr 0.0% 0.0% 52B combine 0.0% 0.0% 8B jni_sys
讓我感到驚訝的是我的核心代碼jdmp模塊只占了46.9k,為此要額外引入幾百k的額外開(kāi)銷(xiāo)!
移除一些無(wú)用字符串
在引入的第三方依賴(lài)?yán)?,開(kāi)發(fā)者自己添加了很多字符串信息,大部分是用來(lái)完善提供運(yùn)行時(shí)報(bào)錯(cuò)信息。通過(guò)修改、精簡(jiǎn)這些依賴(lài)庫(kù),刪除無(wú)用代碼,又可以省出一部分空間來(lái)。
同時(shí),上面的優(yōu)化盡管使用abort替代了panic,rust編譯器仍然會(huì)生出一些格式化的字符串,使用panic_immediate_abort這個(gè)編譯選項(xiàng)禁用這個(gè)行為。
.cargo/config.toml [unstable] build-std-features = ["panic_immediate_abort"] build-std = ["std","panic_abort"]
優(yōu)化后前后體積變化
編譯選項(xiàng) | 體積 |
---|---|
strip | 495k |
strip + opt-level = 'z' | 437k |
strip + opt-level = 'z' + lto | 436k |
strip + opt-level = 'z' + lto + panic = 'abort' + 代碼裁減 + panic_immediate_abort | 135k |
再次分析,整個(gè)文件的體積已經(jīng)降到了135k,自己開(kāi)發(fā)的核心代碼占總代碼量的52%,基本符合預(yù)期。
File .text Size Crate 14.2% 52.0% 41.3KiB jdmp 3.2% 11.7% 9.3KiB core 3.1% 11.4% 9.1KiB jni 3.0% 11.0% 8.8KiB [Unknown] 1.9% 6.8% 5.4KiB std 0.9% 3.3% 2.6KiB alloc 0.3% 1.1% 936B cesu8 0.3% 1.0% 792B adler32 0.1% 0.5% 372B aho_corasick 0.1% 0.4% 316B regex_automata 0.1% 0.3% 220B log 0.1% 0.3% 216B hashbrown 0.0% 0.1% 108B bytes 0.0% 0.1% 44B combine 0.0% 0.1% 44B rustc_demangle 0.0% 0.0% 8B compiler_builtins 0.0% 0.0% 8B jni_sys
優(yōu)化linker script
盡管目前文件體積已經(jīng)相比一開(kāi)始優(yōu)化了不少,但是還沒(méi)有達(dá)到接入要求。通過(guò)readelf進(jìn)一步分析ELF文件的各個(gè)section,我找到了一些額外的優(yōu)化空間。
$ aarch64-linux-gnu-readelf -S target/aarch64-linux-android/release/libjdmp.so There are 24 section headers, starting at offset 0x21738: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .note.android.ide NOTE 0000000000000270 00000270 0000000000000098 0000000000000000 A 0 0 4 [ 2] .dynsym DYNSYM 0000000000000308 00000308 00000000000002e8 0000000000000018 A 7 1 8 [ 3] .gnu.version VERSYM 00000000000005f0 000005f0 000000000000003e 0000000000000002 A 2 0 2 [ 4] .gnu.version_r VERNEED 0000000000000630 00000630 0000000000000040 0000000000000000 A 7 2 4 [ 5] .gnu.hash GNU_HASH 0000000000000670 00000670 0000000000000024 0000000000000000 A 2 0 8 [ 6] .hash HASH 0000000000000694 00000694 0000000000000100 0000000000000004 A 2 0 4 [ 7] .dynstr STRTAB 0000000000000794 00000794 000000000000014d 0000000000000000 A 0 0 1 [ 8] .rela.dyn RELA 00000000000008e8 000008e8 00000000000007f8 0000000000000018 A 2 0 8 [ 9] .rela.plt RELA 00000000000010e0 000010e0 00000000000002a0 0000000000000018 AI 2 19 8 [10] .rodata PROGBITS 0000000000001380 00001380 0000000000001d83 0000000000000000 AM 0 0 8 [11] .eh_frame_hdr PROGBITS 0000000000003104 00003104 0000000000002494 0000000000000000 A 0 0 4 [12] .eh_frame PROGBITS 0000000000005598 00005598 00000000000078cc 0000000000000000 A 0 0 8 [13] .text PROGBITS 000000000000de64 0000ce64 0000000000013e0c 0000000000000000 AX 0 0 4 [14] .plt PROGBITS 0000000000021c70 00020c70 00000000000001e0 0000000000000000 AX 0 0 16 [15] .data.rel.ro PROGBITS 0000000000022e50 00020e50 0000000000000430 0000000000000000 WA 0 0 8 [16] .fini_array FINI_ARRAY 0000000000023280 00021280 0000000000000010 0000000000000008 WA 0 0 8 [17] .dynamic DYNAMIC 0000000000023290 00021290 0000000000000180 0000000000000010 WA 7 0 8 [18] .got PROGBITS 0000000000023410 00021410 0000000000000048 0000000000000000 WA 0 0 8 [19] .got.plt PROGBITS 0000000000023458 00021458 00000000000000f8 0000000000000000 WA 0 0 8 [20] .data PROGBITS 0000000000024550 00021550 0000000000000060 0000000000000000 WA 0 0 8 [21] .bss NOBITS 00000000000245b0 000215b0 0000000000000101 0000000000000000 WA 0 0 8 [22] .comment PROGBITS 0000000000000000 000215b0 00000000000000b2 0000000000000001 MS 0 0 1 [23] .shstrtab STRTAB 0000000000000000 00021662 00000000000000d3 0000000000000000 0 0 1
在對(duì)這些section進(jìn)行優(yōu)化時(shí),有必要搞清楚每個(gè)section在程序運(yùn)行的作用。
section | 作用 |
---|---|
.text | 代碼段 |
.data .rodata .bss | 數(shù)據(jù)段 |
.plt .got .dynamic .dynsym .rela.dyn .rela.plt .shstrtab | 運(yùn)行時(shí)被動(dòng)態(tài)鏈接庫(kù)解析,用于動(dòng)態(tài)鏈接。 |
.eh_frame .eh_frame_hdr | 用于保存函數(shù)的棧幀偏移,方便棧回溯 |
.gnu.hash .gnu.version .gnu.version_r .hash | 保存編譯文件元信息 |
程序在正常運(yùn)行時(shí),代碼段、數(shù)據(jù)段必不可少,同時(shí)需要保留動(dòng)態(tài)鏈接需要的section。剩余的section可以移除,可以進(jìn)一步優(yōu)化文件體積。值得注意到是,刪除.eh_frame .eh_frame_hdr后,在程序崩潰時(shí)只能得到一個(gè)崩潰地址,無(wú)法進(jìn)行?;厮?。
創(chuàng)建一個(gè)linker script,只保留程序運(yùn)行最小依賴(lài)的section。
PHDRS { headers PT_PHDR PHDRS ; text PT_LOAD FILEHDR PHDRS ; data PT_LOAD ; dynamic PT_DYNAMIC ; } ENTRY(Reset); EXTERN(RESET_VECTOR); SECTIONS { . = SIZEOF_HEADERS; .text : { *(.text .text.*) } :text .rodata : { *(.rodata .rodata.*) } :text . = . + 0x1000; .data : { *(.data .data.*) *(.fini_array .fini_array.*) *(.got .got.*) *(.got.plt .got.plt.*) } : data .bss : {*(.bss .bss.*)} : data .dynamic : { *(.dynamic .dynamic.*) } :data :dynamic /DISCARD/ : { *(.ARM.exidx .ARM.exidx.*); *(.gnu.version .gnu.version.*); *(.gnu.version_r .gnu.version_r.*); *(.eh_frame_hdr .eh_frame .eh_frame_hdr.* .eh_frame.* ); *(.note.android.ident .note.android.ident.*); *(.comment .comment.*); } }
修改編譯參數(shù),替換默認(rèn)的linker script
.cargo/config.toml [build] target = ["aarch64-linux-android","armv7-linux-androideabi"] [unstable] build-std-features = ["panic_immediate_abort"] build-std = ["std","panic_abort"] [target.aarch64-linux-android] rustflags = ["-C", "link-arg=-Tlinker.lds"] [target.armv7-linux-androideabi] rustflags = ["-C", "link-arg=-Tlinker.lds"]
經(jīng)過(guò)一番操作,程序的體積最終裁減到了95k!完美符合要求。
總結(jié)
編譯選項(xiàng) | 體積 |
---|---|
strip | 495k |
strip + opt-level = 'z' | 437k |
strip + opt-level = 'z' + lto | 436k |
strip + opt-level = 'z' + lto + panic = 'abort' + 代碼裁減 + panic_immediate_abort | 135k |
strip + opt-level = 'z' + lto + panic = 'abort' + 代碼裁減 + panic_immediate_abort + 移除section | 95k |
審核編輯 黃宇
-
安卓
+關(guān)注
關(guān)注
5文章
2126瀏覽量
57144 -
動(dòng)態(tài)鏈接庫(kù)
+關(guān)注
關(guān)注
0文章
11瀏覽量
7064
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論