当前位置: 首页 > 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++语言开发中仍具有一定的通用性。

作者:尚红泽

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

相关文章:

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

背景介绍 应用安装包的体积影响着用户下载量、安装时长、用户磁盘占用量等多个方面,据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打造的精彩游戏体验 在丰富多彩的游戏世界中&#xff0c…...

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语言程序设计:组合模式的原理、应用及代码实现

文章目录 一、什么组合模式二、为什么需要组合模式三、组合模式的实现原理四、组合模式的应用场景五、组合模式的代码实现 一、什么组合模式 组合模式是一种结构型设计模式&#xff0c;它允许将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和…...

代码随想录算法训练营第25天 | 216.组合总和III ,17.电话号码的字母组合

回溯章节理论基础&#xff1a; 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 题目链接&#xff1a;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 &#x1f980;️。 在上一篇文章我们在macOS成功安装了rust。 一、vscode 安装rust相关插件 以下是一些常用的 Rust 开发插件…...

高斯消去法 | LU分解 | PA=LU分解(MatLab)

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

Nginx server_name 配置说明

Nginx 是一个高性能的反向代理和负载均衡服务器&#xff0c;其核心配置之一是 server 块中的 server_name 指令。server_name 决定了 Nginx 如何根据客户端请求的 Host 头匹配对应的虚拟主机&#xff08;Virtual Host&#xff09;。 1. 简介 Nginx 使用 server_name 指令来确定…...

关于 WASM:1. WASM 基础原理

一、WASM 简介 1.1 WebAssembly 是什么&#xff1f; WebAssembly&#xff08;WASM&#xff09; 是一种能在现代浏览器中高效运行的二进制指令格式&#xff0c;它不是传统的编程语言&#xff0c;而是一种 低级字节码格式&#xff0c;可由高级语言&#xff08;如 C、C、Rust&am…...

如何在网页里填写 PDF 表格?

有时候&#xff0c;你可能希望用户能在你的网站上填写 PDF 表单。然而&#xff0c;这件事并不简单&#xff0c;因为 PDF 并不是一种原生的网页格式。虽然浏览器可以显示 PDF 文件&#xff0c;但原生并不支持编辑或填写它们。更糟的是&#xff0c;如果你想收集表单数据&#xff…...

在web-view 加载的本地及远程HTML中调用uniapp的API及网页和vue页面是如何通讯的?

uni-app 中 Web-view 与 Vue 页面的通讯机制详解 一、Web-view 简介 Web-view 是 uni-app 提供的一个重要组件&#xff0c;用于在原生应用中加载 HTML 页面&#xff1a; 支持加载本地 HTML 文件支持加载远程 HTML 页面实现 Web 与原生的双向通讯可用于嵌入第三方网页或 H5 应…...

微软PowerBI考试 PL300-在 Power BI 中清理、转换和加载数据

微软PowerBI考试 PL300-在 Power BI 中清理、转换和加载数据 Power Query 具有大量专门帮助您清理和准备数据以供分析的功能。 您将了解如何简化复杂模型、更改数据类型、重命名对象和透视数据。 您还将了解如何分析列&#xff0c;以便知晓哪些列包含有价值的数据&#xff0c;…...

Java求职者面试指南:Spring、Spring Boot、MyBatis框架与计算机基础问题解析

Java求职者面试指南&#xff1a;Spring、Spring Boot、MyBatis框架与计算机基础问题解析 一、第一轮提问&#xff08;基础概念问题&#xff09; 1. 请解释Spring框架的核心容器是什么&#xff1f;它在Spring中起到什么作用&#xff1f; Spring框架的核心容器是IoC容器&#…...

JS手写代码篇----使用Promise封装AJAX请求

15、使用Promise封装AJAX请求 promise就有reject和resolve了&#xff0c;就不必写成功和失败的回调函数了 const BASEURL ./手写ajax/test.jsonfunction promiseAjax() {return new Promise((resolve, reject) > {const xhr new XMLHttpRequest();xhr.open("get&quo…...

(一)单例模式

一、前言 单例模式属于六大创建型模式,即在软件设计过程中,主要关注创建对象的结果,并不关心创建对象的过程及细节。创建型设计模式将类对象的实例化过程进行抽象化接口设计,从而隐藏了类对象的实例是如何被创建的,封装了软件系统使用的具体对象类型。 六大创建型模式包括…...

日常一水C

多态 言简意赅&#xff1a;就是一个对象面对同一事件时做出的不同反应 而之前的继承中说过&#xff0c;当子类和父类的函数名相同时&#xff0c;会隐藏父类的同名函数转而调用子类的同名函数&#xff0c;如果要调用父类的同名函数&#xff0c;那么就需要对父类进行引用&#…...

【LeetCode】算法详解#6 ---除自身以外数组的乘积

1.题目介绍 给定一个整数数组 nums&#xff0c;返回 数组 answer &#xff0c;其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法&#xff0c;且在 O…...