作者:CCO體系 尚紅澤
背景介紹
應用安裝包的體積影響著用戶下載量、安裝時長、用戶磁盤占用量等多個方面,據Google Play統計,應用體積每增加6MB,安裝的轉化率將下降1%。
安裝包的體積受諸多方面影響,針對dex、資源文件、so文件都有不同的優化策略,在此不做一一展開,本文主要記錄了在研發時針對動態鏈接庫的文件體積裁剪優化方案。
我開發的鏈接庫使用rust語言開發,通過安卓jni接口實現java層和native層之間的相互調用。為什么使用rust主要有以下幾個方面的考慮:
1.穩。安卓的JNI接口調用復雜,又涉及到native層的內存管理,隨著代碼量的增加,代碼的安全穩定性會受到很大的挑戰。使用rust開發,開發者幾乎不需要考慮GC的問題,只要開發的時候按照規范老老實實寫代碼并且通過了編譯器的檢查,基本上就很難把程序寫崩,這一點在代碼上線后也確實得到了驗證。
2.安全。傳統使用C、C++開發的代碼編譯完成以后,如果不加保護,很容易使用反匯編工具破解,市面上比較成熟的工具如IDA、ghidra等都可以將匯編代碼還原到高級語言。使用rust編譯的產物,內部函數間的調用規約和傳統都不一樣,目前市面上還沒有相對完善的反編譯工具,軟件的防破解能力直接上升一個數量級。
但是使用rust有一個非常明顯的缺點就是編譯產物體積過大。在不修改默認的rust編譯選項的情況下,僅開啟strip的情況下,我的動態庫體積達到了495k。
優化方案
參考網上前人的經驗,依次進行了以下優化方式。
調整優化等級
默認的編譯優化等級是O3,該優化的目的提高代碼的運行速度,但是與此同時會對部分循環進行展開,體積造成膨脹。在此我們以縮減體積為目標,將優化選項改為z,表示生成最小二進制體積:
[profile.release] opt-level = 'z'
優化后前后體積變化
編譯選項 | 體積 |
---|---|
strip | 495k |
strip + opt-level = 'z' | 437k |
開啟LTO
LTO(Link Time Optimization)可以在鏈接時消除冗余代碼,減小二進制體積——代價是更長的鏈接時間。
Cargo.toml [profile.release] opt-level = 'z' lto = true
優化后前后體積變化
編譯選項 | 體積 |
---|---|
strip | 495k |
strip + opt-level = 'z' | 437k |
strip + opt-level = 'z' + lto | 436k |
優化效果非常不明顯,聊勝于無。
Panic立刻終止
rust默認的panic會在崩潰時進行?;厮荩奖愣ㄎ粏栴}。然而會帶來額外的體積增加,將這一功能使用abort替代。
[profile.release] opt-level = 'z' lto = true panic = 'abort'
優化后前后體積變化
編譯選項 | 體積 |
---|---|
strip | 495k |
strip + opt-level = 'z' | 437k |
strip + opt-level = 'z' + lto | 436k |
strip + opt-level = 'z' + lto + panic = 'abort' | 366K |
到目前為止,常規的優化手段已經用完了,后續優化需要配合一些代碼的額外變動。
使用rust分析工具bloat對產物進行分析,結果如下:
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的額外開銷!
移除一些無用字符串
在引入的第三方依賴里,開發者自己添加了很多字符串信息,大部分是用來完善提供運行時報錯信息。通過修改、精簡這些依賴庫,刪除無用代碼,又可以省出一部分空間來。
同時,上面的優化盡管使用abort替代了panic,rust編譯器仍然會生出一些格式化的字符串,使用panic_immediate_abort這個編譯選項禁用這個行為。
.cargo/config.toml [unstable] build-std-features = ["panic_immediate_abort"] build-std = ["std","panic_abort"]
優化后前后體積變化
編譯選項 | 體積 |
---|---|
strip | 495k |
strip + opt-level = 'z' | 437k |
strip + opt-level = 'z' + lto | 436k |
strip + opt-level = 'z' + lto + panic = 'abort' + 代碼裁減 + panic_immediate_abort | 135k |
再次分析,整個文件的體積已經降到了135k,自己開發的核心代碼占總代碼量的52%,基本符合預期。
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
優化linker script
盡管目前文件體積已經相比一開始優化了不少,但是還沒有達到接入要求。通過readelf進一步分析ELF文件的各個section,我找到了一些額外的優化空間。
$ 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
在對這些section進行優化時,有必要搞清楚每個section在程序運行的作用。
section | 作用 |
---|---|
.text | 代碼段 |
.data .rodata .bss | 數據段 |
.plt .got .dynamic .dynsym .rela.dyn .rela.plt .shstrtab | 運行時被動態鏈接庫解析,用于動態鏈接。 |
.eh_frame .eh_frame_hdr | 用于保存函數的棧幀偏移,方便?;厮?/td> |
.gnu.hash .gnu.version .gnu.version_r .hash | 保存編譯文件元信息 |
程序在正常運行時,代碼段、數據段必不可少,同時需要保留動態鏈接需要的section。剩余的section可以移除,可以進一步優化文件體積。值得注意到是,刪除.eh_frame .eh_frame_hdr后,在程序崩潰時只能得到一個崩潰地址,無法進行棧回溯。
創建一個linker script,只保留程序運行最小依賴的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.*); } }
修改編譯參數,替換默認的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"]
經過一番操作,程序的體積最終裁減到了95k!完美符合要求。
總結
編譯選項 | 體積 |
---|---|
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 |
審核編輯 黃宇
-
安卓
+關注
關注
5文章
2134瀏覽量
57367 -
動態鏈接庫
+關注
關注
0文章
11瀏覽量
7072
發布評論請先 登錄
相關推薦
評論