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

背景介绍

应用安装包的体积影响着用户下载量、安装时长、用户磁盘占用量等多个方面,据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++语言开发中仍具有一定的通用性。

作者:尚红泽

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

相关推荐
zhangphil40 分钟前
Android简洁缩放Matrix实现图像马赛克,Kotlin
android·kotlin
m0_5127446441 分钟前
极客大挑战2024-web-wp(详细)
android·前端
lw向北.1 小时前
Qt For Android之环境搭建(Qt 5.12.11 Qt下载SDK的处理方案)
android·开发语言·qt
不爱学习的啊Biao1 小时前
【13】MySQL如何选择合适的索引?
android·数据库·mysql
Clockwiseee2 小时前
PHP伪协议总结
android·开发语言·php
mmsx8 小时前
android sqlite 数据库简单封装示例(java)
android·java·数据库
众拾达人11 小时前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言
吃着火锅x唱着歌12 小时前
PHP7内核剖析 学习笔记 第四章 内存管理(1)
android·笔记·学习
_Shirley13 小时前
鸿蒙设置app更新跳转华为市场
android·华为·kotlin·harmonyos·鸿蒙
hedalei15 小时前
RK3576 Android14编译OTA包提示java.lang.UnsupportedClassVersionError问题
android·android14·rk3576