当前位置: 首页 > news >正文

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

背景介绍

应用安装包的体积影响着用户下载量、安装时长、用户磁盘占用量等多个方面,据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 jdmp3.2%  11.7%  9.3KiB core3.1%  11.4%  9.1KiB jni3.0%  11.0%  8.8KiB [Unknown]1.9%   6.8%  5.4KiB std0.9%   3.3%  2.6KiB alloc0.3%   1.1%    936B cesu80.3%   1.0%    792B adler320.1%   0.5%    372B aho_corasick0.1%   0.4%    316B regex_automata0.1%   0.3%    220B log0.1%   0.3%    216B hashbrown0.0%   0.1%    108B bytes0.0%   0.1%     44B combine0.0%   0.1%     44B rustc_demangle0.0%   0.0%      8B compiler_builtins0.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           OffsetSize              EntSize          Flags  Link  Info  Align[ 0]                   NULL             0000000000000000  000000000000000000000000  0000000000000000           0     0     0[ 1] .note.android.ide NOTE             0000000000000270  000002700000000000000098  0000000000000000   A       0     0     4[ 2] .dynsym           DYNSYM           0000000000000308  0000030800000000000002e8  0000000000000018   A       7     1     8[ 3] .gnu.version      VERSYM           00000000000005f0  000005f0000000000000003e  0000000000000002   A       2     0     2[ 4] .gnu.version_r    VERNEED          0000000000000630  000006300000000000000040  0000000000000000   A       7     2     4[ 5] .gnu.hash         GNU_HASH         0000000000000670  000006700000000000000024  0000000000000000   A       2     0     8[ 6] .hash             HASH             0000000000000694  000006940000000000000100  0000000000000004   A       2     0     4[ 7] .dynstr           STRTAB           0000000000000794  00000794000000000000014d  0000000000000000   A       0     0     1[ 8] .rela.dyn         RELA             00000000000008e8  000008e800000000000007f8  0000000000000018   A       2     0     8[ 9] .rela.plt         RELA             00000000000010e0  000010e000000000000002a0  0000000000000018  AI       2    19     8[10] .rodata           PROGBITS         0000000000001380  000013800000000000001d83  0000000000000000  AM       0     0     8[11] .eh_frame_hdr     PROGBITS         0000000000003104  000031040000000000002494  0000000000000000   A       0     0     4[12] .eh_frame         PROGBITS         0000000000005598  0000559800000000000078cc  0000000000000000   A       0     0     8[13] .text             PROGBITS         000000000000de64  0000ce640000000000013e0c  0000000000000000  AX       0     0     4[14] .plt              PROGBITS         0000000000021c70  00020c7000000000000001e0  0000000000000000  AX       0     0     16[15] .data.rel.ro      PROGBITS         0000000000022e50  00020e500000000000000430  0000000000000000  WA       0     0     8[16] .fini_array       FINI_ARRAY       0000000000023280  000212800000000000000010  0000000000000008  WA       0     0     8[17] .dynamic          DYNAMIC          0000000000023290  000212900000000000000180  0000000000000010  WA       7     0     8[18] .got              PROGBITS         0000000000023410  000214100000000000000048  0000000000000000  WA       0     0     8[19] .got.plt          PROGBITS         0000000000023458  0002145800000000000000f8  0000000000000000  WA       0     0     8[20] .data             PROGBITS         0000000000024550  000215500000000000000060  0000000000000000  WA       0     0     8[21] .bss              NOBITS           00000000000245b0  000215b00000000000000101  0000000000000000  WA       0     0     8[22] .comment          PROGBITS         0000000000000000  000215b000000000000000b2  0000000000000001  MS       0     0     1[23] .shstrtab         STRTAB           0000000000000000  0002166200000000000000d3  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++语言开发中仍具有一定的通用性。

作者:尚红泽

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

相关文章:

  • 大型装备制造企业案例分享——通过CRM系统管理全球业务
  • IEC61499 学习记录
  • 计算机网络——03网络核心
  • 视频融合平台EasyCVR推流成功但平台显示不在线是什么原因?
  • tee漏洞学习-翻译-2:探索 Qualcomm TrustZone的实现
  • 蓝桥杯刷题day07——斐波那契与7
  • 政安晨:示例演绎Python的函数与获取帮助的方法
  • git的奇特知识点
  • 【python】绘制爱心图案
  • mcu短时间内发生多次中断,如何解决中断丢失问题?
  • JAVA原型模式详解
  • 物联网ARM开发-STM32之RTC浅谈
  • MQ,RabbitMQ,SpringAMQP的原理与实操
  • 计算机中的缓存与内存
  • thinkphp数据批量提交(群发消息)
  • CentOS7 安装JDK
  • es6要点
  • iOS动画编程-View动画[ 1 ] 基础View动画
  • js 实现textarea输入字数提示
  • open-falcon 开发笔记(一):从零开始搭建虚拟服务器和监测环境
  • Sublime Text 2/3 绑定Eclipse快捷键
  • tweak 支持第三方库
  • zookeeper系列(七)实战分布式命名服务
  • 阿里云爬虫风险管理产品商业化,为云端流量保驾护航
  • 阿里云前端周刊 - 第 26 期
  • 纯 javascript 半自动式下滑一定高度,导航栏固定
  • 技术发展面试
  • 面试题:给你个id,去拿到name,多叉树遍历
  • 微信开放平台全网发布【失败】的几点排查方法
  • 在Docker Swarm上部署Apache Storm:第1部分
  • ​​​​​​​​​​​​​​汽车网络信息安全分析方法论
  • ​决定德拉瓦州地区版图的关键历史事件
  • # Swust 12th acm 邀请赛# [ K ] 三角形判定 [题解]
  • ###STL(标准模板库)
  • #Linux(权限管理)
  • (1)bark-ml
  • (17)Hive ——MR任务的map与reduce个数由什么决定?
  • (39)STM32——FLASH闪存
  • (C语言)fgets与fputs函数详解
  • (C语言)共用体union的用法举例
  • (定时器/计数器)中断系统(详解与使用)
  • (附源码)springboot宠物医疗服务网站 毕业设计688413
  • (附源码)ssm考试题库管理系统 毕业设计 069043
  • (附源码)计算机毕业设计ssm高校《大学语文》课程作业在线管理系统
  • (十五)Flask覆写wsgi_app函数实现自定义中间件
  • (一)eclipse Dynamic web project 工程目录以及文件路径问题
  • (转)shell调试方法
  • (转)socket Aio demo
  • *setTimeout实现text输入在用户停顿时才调用事件!*
  • .Mobi域名介绍
  • .net php 通信,flash与asp/php/asp.net通信的方法
  • @RunWith注解作用
  • @Transactional 竟也能解决分布式事务?
  • @value 静态变量_Python彻底搞懂:变量、对象、赋值、引用、拷贝
  • [1] 平面(Plane)图形的生成算法