作者: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,表示生成最小二进制体积:
优化后前后体积变化
编译选项 | 体积 |
strip | 495k |
strip + opt-level = 'z' | 437k |
开启LTO
LTO(Link Time Optimization)可以在链接时消除冗余代码,减小二进制体积——代价是更长的链接时间。
优化后前后体积变化
编译选项 | 体积 |
strip | 495k |
strip + opt-level = 'z' | 437k |
strip + opt-level = 'z' + lto | 436k |
优化效果非常不明显,聊胜于无。
Panic立刻终止
rust默认的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对产物进行分析,结果如下:
让我感到惊讶的是我的核心代码jdmp模块只占了46.9k,为此要额外引入几百k的额外开销!
移除一些无用字符串
在引入的第三方依赖里,开发者自己添加了很多字符串信息,大部分是用来完善提供运行时报错信息。通过修改、精简这些依赖库,删除无用代码,又可以省出一部分空间来。
同时,上面的优化尽管使用abort替代了panic,rust编译器仍然会生出一些格式化的字符串,使用panic\_immediate\_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%,基本符合预期。
优化linker script
尽管目前文件体积已经相比一开始优化了不少,但是还没有达到接入要求。通过readelf进一步分析ELF文件的各个section,我找到了一些额外的优化空间。
在对这些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。
修改编译参数,替换默认的linker script
经过一番操作,程序的体积最终裁减到了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 |