安卓动态链接库文件体积优化探索实践
背景介绍
应用安装包的体积影响着用户下载量、安装时长、用户磁盘占用量等多个方面,据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++语言开发中仍具有一定的通用性。
作者:尚红泽
来源:京东云开发者社区 转载请注明来源
相关文章:

安卓动态链接库文件体积优化探索实践
背景介绍 应用安装包的体积影响着用户下载量、安装时长、用户磁盘占用量等多个方面,据Google Play统计,应用体积每增加6MB,安装的转化率将下降1%。 安装包的体积受诸多方面影响,针对dex、资源文件、so文件都有不同的优化策略&…...

[Java][算法 哈希]Day 01---LeetCode 热题 100---01~03
LeetCode 热题 100---01~03 ------->哈希 第一题 两数之和 思路 最直接的理解就是 找出两个数的和等于目标数 这两个数可以相同 但是不能是同一个数字(从数组上理解就是内存上不是同一位置) 解法一:暴力法 暴力解万物 按照需求 …...

【每日一题】LeetCode——链表的中间结点
📚博客主页:爱敲代码的小杨. ✨专栏:《Java SE语法》 | 《数据结构与算法》 | 《C生万物》 ❤️感谢大家点赞👍🏻收藏⭐评论✍🏻,您的三连就是我持续更新的动力❤️ 🙏小杨水平有…...

k8s 部署java应用 基于ingress+jar包
k8 集群ingress的访问模式 先部署一个namespace 命名空间 vim namespace.yaml kind: Namespace apiVersion: v1 metadata:name: ingress-testlabels:env: ingress-test 在部署deployment deployment是pod层一层封装。可以实现多节点部署 资源分配 回滚部署等方式。 部署的…...

深度学习技巧应用36-深度学习模型训练中的超参数调优指南大全,总结相关问题与答案
大家好,我是微学AI,今天给大家介绍一下深度学习技巧应用36-深度学习模型训练中的超参数调优指南大全,总结相关问题与答案。深度学习模型训练中的调优指南大全概括了数据预处理、模型架构设计、超参数优化、正则化策略和训练技巧等多个关键方面,以提升模型性能和泛化能力。 …...
“探索AJAX:前端与后端数据交互的利器“
前言 在现代Web开发中,前端与后端之间的数据交互是一个至关重要的环节。为了实现无需刷新页面的动态更新,AJAX(Asynchronous JavaScript and XML)作为一种强大的技术被广泛应用。 AJAX的原理 AJAX通过JavaScript和XMLHttpReque…...
【5G NR】移动通讯中使用的信道编解码技术
目录 一、引言 二、信道编解码技术概述 三、移动通讯中常用的信道编解码技术 四、优缺点分析与比较 五、未来发展趋势 六、结论 本文主要介绍了移动通讯中采用的信道编解码技术,由于在5G NR终端中,通常要兼容4G LTE通讯技术,所以4G LTE…...

用Python Tkinter打造的精彩连连看小游戏【附源码】
文章目录 连连看小游戏:用Python Tkinter打造的精彩游戏体验游戏简介技术背景MainWindow类:职责:方法:Point类: 主执行部分:完整代码:总结: 连连看小游戏:用Python Tkinter打造的精彩游戏体验 在丰富多彩的游戏世界中,…...

nvm安装node后,npm无效
类似报这种问题,是因为去github下载npm时下载失败, Please visit https://github.com/npm/cli/releases/tag/v6.14.17 to download npm. 第一种方法:需要复制这里面的地址爬梯子去下载(github有时不用梯子能直接下载,有…...

spring boot(2.4.x 开始)和spring cloud项目中配置文件application和bootstrap加载顺序
在前面的文章基础上 https://blog.csdn.net/zlpzlpzyd/article/details/136060312 spring boot 2.4.x 版本之前通过 ConfigFileApplicationListener 加载配置 https://github.com/spring-projects/spring-boot/blob/v2.3.12.RELEASE/spring-boot-project/spring-boot/src/mai…...

5-2、S曲线计算【51单片机+L298N步进电机系列教程】
↑↑↑点击上方【目录】,查看本系列全部文章 摘要:本节介绍S曲线的基本变换,将基本形式的S曲线变换成为任意过两点的S曲线,为后续步进电机S曲线运动提供理论支撑 一.计算目标 ①计算经过任意不同两点的S曲线方程 ②可调节曲线平…...
SQL 注入 - http头注入之UA头注入探测
环境准备:构建完善的安全渗透测试环境:推荐工具、资源和下载链接_渗透测试靶机下载-CSDN博客 一、http头注入介绍 HTTP头注入是一种网络安全攻击手段,它利用了Web应用程序对HTTP头的处理不当或缺乏充分的验证和过滤。在这种攻击中,攻击者通过修改HTTP请求头中的某些字段,…...
学习数据结构和算法的第5天
空间复杂度及其常见案例 空间复杂度 空间复杂度也是一个数学函数表达式,是对一个算法在运行过程中临时占用存储空间大小的量度。 空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度…...
Android 11 访问 Android/data/或者getExternalCacheDir() root方式
前言: 需求要求安装三方应用ExternalCacheDir()下载下来的apk文件。 getExternalCacheDir() : /storage/emulated/0/Android/data/com../cache/ 获取访问权限 如果手机安卓版本为Android10的时候,可以在AndroidManifest.xml中添加下列代码 android:requestLegacyExt…...

Linux探秘之旅:透彻理解路径、命令与系统概念
目录 如何远程连接 远程登录简明指南 linux区别 1.严格区分大小写 2.linux的命令返回结果判断 3.如何查看网络信息 4.关于后缀名(Linux不关心文件后缀) 4.1 需要记忆的后缀 5.echo命令 6.linux一切皆文件 6.1比如磁盘的文件 6.2可执行文件 …...
哈希算法 c语言
#include <stdio.h> #include <stdlib.h> #include <string.h> // 哈希函数 unsigned int hash_function(const char *str) { unsigned int hash 0; while (*str) { hash (hash * 31 *str) % 1000; str; } return hash;…...
新版MQL语言程序设计:组合模式的原理、应用及代码实现
文章目录 一、什么组合模式二、为什么需要组合模式三、组合模式的实现原理四、组合模式的应用场景五、组合模式的代码实现 一、什么组合模式 组合模式是一种结构型设计模式,它允许将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和…...

代码随想录算法训练营第25天 | 216.组合总和III ,17.电话号码的字母组合
回溯章节理论基础: https://programmercarl.com/%E5%9B%9E%E6%BA%AF%E7%AE%97%E6%B3%95%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html 216.组合总和III 题目链接:https://leetcode.cn/problems/combination-sum-iii/ 思路: 本题就是在[1,2,3,4,5,6,7,…...

Rust 第一个rust程序Hello Rust️
文章目录 前言一、vscode 安装rust相关插件二、Cargo New三、vscode调试rustLLDB 前言 Rust学习系列。今天就让我们掌握第一个rust程序。Hello Rust 🦀️。 在上一篇文章我们在macOS成功安装了rust。 一、vscode 安装rust相关插件 以下是一些常用的 Rust 开发插件…...

高斯消去法 | LU分解 | PA=LU分解(MatLab)
一、问题描述 利用高斯消去法,LU 分解及PALU 分解求解非线性方程组。 二、实验目的 掌握高斯消去法、LU 分解、PALU 分解的算法原理;编写代码实现利用高斯消去法、LU 分解、PALU 分解来求解线性方程组。 三、实验内容及要求 1. 利用顺序高斯消去法求…...

利用最小二乘法找圆心和半径
#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …...

CTF show Web 红包题第六弹
提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框,很难让人不联想到SQL注入,但提示都说了不是SQL注入,所以就不往这方面想了 先查看一下网页源码,发现一段JavaScript代码,有一个关键类ctfs…...
IGP(Interior Gateway Protocol,内部网关协议)
IGP(Interior Gateway Protocol,内部网关协议) 是一种用于在一个自治系统(AS)内部传递路由信息的路由协议,主要用于在一个组织或机构的内部网络中决定数据包的最佳路径。与用于自治系统之间通信的 EGP&…...

sipsak:SIP瑞士军刀!全参数详细教程!Kali Linux教程!
简介 sipsak 是一个面向会话初始协议 (SIP) 应用程序开发人员和管理员的小型命令行工具。它可以用于对 SIP 应用程序和设备进行一些简单的测试。 sipsak 是一款 SIP 压力和诊断实用程序。它通过 sip-uri 向服务器发送 SIP 请求,并检查收到的响应。它以以下模式之一…...
JS设计模式(4):观察者模式
JS设计模式(4):观察者模式 一、引入 在开发中,我们经常会遇到这样的场景:一个对象的状态变化需要自动通知其他对象,比如: 电商平台中,商品库存变化时需要通知所有订阅该商品的用户;新闻网站中࿰…...

Windows安装Miniconda
一、下载 https://www.anaconda.com/download/success 二、安装 三、配置镜像源 Anaconda/Miniconda pip 配置清华镜像源_anaconda配置清华源-CSDN博客 四、常用操作命令 Anaconda/Miniconda 基本操作命令_miniconda创建环境命令-CSDN博客...
【学习笔记】erase 删除顺序迭代器后迭代器失效的解决方案
目录 使用 erase 返回值继续迭代使用索引进行遍历 我们知道类似 vector 的顺序迭代器被删除后,迭代器会失效,因为顺序迭代器在内存中是连续存储的,元素删除后,后续元素会前移。 但一些场景中,我们又需要在执行删除操作…...

pgsql:还原数据库后出现重复序列导致“more than one owned sequence found“报错问题的解决
问题: pgsql数据库通过备份数据库文件进行还原时,如果表中有自增序列,还原后可能会出现重复的序列,此时若向表中插入新行时会出现“more than one owned sequence found”的报错提示。 点击菜单“其它”-》“序列”,…...
写一个shell脚本,把局域网内,把能ping通的IP和不能ping通的IP分类,并保存到两个文本文件里
写一个shell脚本,把局域网内,把能ping通的IP和不能ping通的IP分类,并保存到两个文本文件里 脚本1 #!/bin/bash #定义变量 ip10.1.1 #循环去ping主机的IP for ((i1;i<10;i)) doping -c1 $ip.$i &>/dev/null[ $? -eq 0 ] &&am…...
React父子组件通信:Props怎么用?如何从父组件向子组件传递数据?
系列回顾: 在上一篇《React核心概念:State是什么?》中,我们学习了如何使用useState让一个组件拥有自己的内部数据(State),并通过一个计数器案例,实现了组件的自我更新。这很棒&#…...