写点什么

安卓动态链接库文件体积优化探索实践

  • 2024-02-05
    北京
  • 本文字数:4989 字

    阅读完需:约 16 分钟

背景介绍

应用安装包的体积影响着用户下载量、安装时长、用户磁盘占用量等多个方面,据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 = truepanic = '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 Crate4.1%  69.0% 192.7KiB std1.0%  16.8%  46.9KiB jdmp0.5%   8.1%  22.7KiB [Unknown]0.2%   3.8%  10.5KiB jni0.0%   0.5%   1.5KiB cesu80.0%   0.4%   1.1KiB adler320.0%   0.3%     904B bytes0.0%   0.2%     640B aho_corasick0.0%   0.2%     588B regex_syntax0.0%   0.2%     572B regex_automata0.0%   0.2%     440B log0.0%   0.1%     304B memchr0.0%   0.0%      52B combine0.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 Crate14.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.soThere 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++语言开发中仍具有一定的通用性。


作者:尚红泽


来源:京东云开发者社区 转载请注明来源

用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
安卓动态链接库文件体积优化探索实践_京东科技开发者_InfoQ写作社区