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

news2024/11/14 3:30:59

背景介绍

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

作者:尚红泽

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1436472.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

麒麟信安战略投资湖南超能机器人技术有限公司,加速布局无人智能系统、自主可控机器人操作系统赛道

为进一步完善产业布局,推进战略规划稳步实施,近日,麒麟信安与湖南超能机器人技术有限公司(简称“超能机器人”)及其原股东签署了《增资协议》及相关配套协议,麒麟信安成为超能机器人股东。 战略投资超能机…

打包 iOS 的 IPA 文件

目录 摘要 引言 准备 选择证书类型 创建应用程序 设置应用程序标识和证书 配置构建设置 构建应用程序 导出IPA 签名和导出 代码案例演示 完成 总结 参考资料 摘要 本篇博客将为您介绍如何打包iOS的IPA文件。从APP提交、创建应用程序、设置应用程序标识和证书、配…

【详解】斗地主随机发牌项目

目录 前言: 1.初始化牌 2.洗牌 3.揭牌 总代码: Card类: CardGame类: Main类: 结语: 前言: 斗地主是全国范围内的一种桌面游戏,本节我们来实现一下斗地主中的简单初始化牌、…

20240202在WIN10下部署faster-whisper

20240202在WIN10下部署faster-whisper 2024/2/2 12:15 前提条件,可以通过技术手段上外网!^_ 首先你要有一张NVIDIA的显卡,比如我用的PDD拼多多的二手GTX1080显卡。【并且极其可能是矿卡!】800¥ 2、请正确安装好NVIDIA最…

BEV感知算法学习

BEV感知算法学习 3D目标检测系列 Mono3D(Monocular 3D Object Detection for Autonomous Driving) 流程: 通过在地平面上假设先验,在3D空间中对具有典型物理尺寸的候选边界框进行采样;然后我们将这些方框投影到图像平面上,从而避…

【Vitis】HLS高层次综合的优势

高层次综合 (HLS) 是自动设计进程, 利用数字系统的抽的象行为规范来生成寄存器传输级结构, 以实现给定行为。 使用 HLS 的典型流程包含下列步骤: 1. 围绕给定架构在高抽象层次使用 C/C 编写算法 2. 在行为级别验证功能 3. 使用 HLS 工具为…

(十二)springboot实战——SSE服务推送事件案例实现

前言 SSE(Server-Sent Events,服务器推送事件)是一种基于HTTP协议的服务器推送技术。它允许服务器向客户端发送异步的、无限长的数据流,而无需客户端不断地轮询或发起请求。这种技术可以用来实现实时通信、在线聊天、即时更新等功…

LeetCode、790. 多米诺和托米诺平铺【中等,二维DP,可转一维】

文章目录 前言LeetCode、790. 多米诺和托米诺平铺【中等,二维DP,可转一维】题目与分类思路二维解法二维转一维 资料获取 前言 博主介绍:✌目前全网粉丝2W,csdn博客专家、Java领域优质创作者,博客之星、阿里云平台优质…

【VSTO开发-WPS】下调试

重点2步: 1、注册表添加 Windows Registry Editor Version 5.00[HKEY_CURRENT_USER\Software\kingsoft\Office\WPP\AddinsWL] "项目名称"""2、visual studio 运行后,要选中附加到调试,并指定启动项目。 如PPT输入WPP搜…

在 CentOS 7上使用 Apache 和 mod_wsgi 部署 Django 应用的方法

简介 Django 是一个强大的 Web 框架,可以帮助您快速启动 Python 应用程序或网站。Django 包括一个简化的开发服务器,用于在本地测试代码,但对于任何与生产相关的事情,都需要一个更安全和功能强大的 Web 服务器。 在本指南中&…

Python学习路线 - Python高阶技巧 - 拓展

Python学习路线 - Python高阶技巧 - 拓展 闭包闭包注意事项 装饰器装饰器的一般写法(闭包写法)装饰器的语法糖写法 设计模式单例模式工厂模式 多线程进程、线程并行执行多线程编程threading模块 网络编程Socket客户端和服务端Socket服务端编程实现服务端并结合客户端进行测试 S…

毅速集团2023年度总结暨表彰大会圆满举行

2024年2月2日,毅速集团2023年度总结暨表彰大会在上海总部举行,本次年会以“加速世界向增材制造的转变”为主题,全面总结了毅速集团2023年取得的成绩,明确了2024年的发展战略,并对过去一年中表现突出的个人进行了隆重表…

Redis学习及总结

Redis 快速入门 Redis属于非关系型数据库 SQL应用场景 数据结构固定相关业务对数据安全性一致性要求高 NoSQL应用场景 数据结构不固定对一致性,安全性要求不高性能要求高 🎯需要使用Xftp 传输压缩包到虚拟机上 安装好Redis后, 执行命令…

第七届西湖论剑·中国杭州网络安全技能大赛 AI 回声海螺 WP

第七届西湖论剑中国杭州网络安全技能大赛-AI-回声海螺 开题,提示输入密码给FLAG。 这个回声海螺应该是个AI,就是复读机,应该是想办法从中骗出密码。 感觉这题不像是AI,也没用啥模型,应该是WEB。或者是说类似于AI的提示…

GLSL ES 1.0

GLSL ES 概述 写在前面 程序是大小写敏感的每一个语句都应该以英文分号结束一个shader必须包含一个main函数,该函数不接受任何参数,并且返回voidvoid main() { }数据值类型 GLSL支持三种数据类型: 整型浮点型:必须包含小数点&…

posix_memalign 与 malloc 对比

1. 原因原理 编程中的类型对齐问题主要是处于性能考虑,如果不做对齐,那么单个数据元素的访问很容易跨在多个时钟周期上,从而导致性能下降。 内建数据类型的对齐,是由编译器和C语言库的API实现中自动完成的,这对于用户是…

LeetCode-第876题-链表的中间结点

1.题目描述 给你单链表的头结点 head ,请你找出并返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。 2.样例描述 3.思路描述 创建两个快慢指针 slow , fast ,起始共同指向头节点,slow 每次走一步,fas…

LabVIEW双光子荧光显微成像系统开发

双光子显微成像是一种高级荧光显微技术,广泛用于生物学和医学研究,尤其是用于活体组织的深层成像。在双光子成像过程中,振镜(Galvo镜)扮演了非常关键的角色,它负责精确控制激光束在样本上的扫描路径。以下是…

leetcode9. 回文数|详细深入讲解算法

前往题目有 反转一半数字 思路 映入脑海的第一个想法是将数字转换为字符串,并检查字符串是否为回文。但是,这需要额外的非常量空间来创建问题描述中所不允许的字符串。 第二个想法是将数字本身反转,然后将反转后的数字与原始数字进行比较&…

总结:图像生成网络

1、最新的几款图像生成网络 eCNN 文献:Bahrami A, Karimian A, Fatemizadeh E, et al. A new deep convolutional neural network design with efficient learning capability: Application to CT image synthesis from MRI[J]. Medical physics, 2020, 47(10): 515…