安卓動態鏈接庫文件體積優化探索實踐

背景介紹

應用安裝包的體積影響着用戶下載量、安裝時長、用戶磁盤佔用量等多個方面,據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 | 用於保存函數的棧幀偏移,方便棧回溯 | | .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 |

本文記錄了我進行編譯體積優化的各種操作,其中的一些策略在使用C、C++語言開發中仍具有一定的通用性。

作者:尚紅澤

來源:京東雲開發者社區 轉載請註明來源

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章