| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 移动开发 -> Android对so体积优化的探索与实践 -> 正文阅读 |
|
[移动开发]Android对so体积优化的探索与实践 |
1. 背景应用安装包的体积影响着用户的下载时长、安装时长、磁盘占用空间等诸多方面,因此减小安装包的体积对于提升用户体验和下载转化率都大有益处。Android 应用安装包其实是一个 zip 文件,主要由 dex、assets、resource、so 等各类型文件压缩而成。目前业内常见的包体积优化方案大体分为以下几类:
随着动态化、端智能等技术的广泛应用,在采用上述优化手段后, so 在安装包体积中的比重依然很高,我们开始思索这部分体积是否能进一步优化。 经过一段时间的调研、分析和验证,我们逐渐摸索出一套可以将应用安装包中 so 体积进一步减小 30%~60% 的方案。该方案包含一系列纯技术优化手段,对业务侵入性低,通过简单的配置,可以快速部署生效,目前美团 App 已在线上部署使用。为让大家能知其然,也能知其所以然,本文将先从 so 文件格式讲起,结合文件格式分析哪些内容可以优化。 2. so 文件格式分析so 即动态库,本质上是 ELF(Executable and Linkable Format)文件。可以从两个维度查看 so 文件的内部结构:链接视图(Linking View)和执行视图(Execution View)。链接视图将 so 主体看作多个 section 的组合,该视图体现的是 so 是如何组装的,是编译链接的视角。而执行视图将 so 主体看作多个 segment 的组合,该视图告诉动态链接器如何加载和执行该 so,是运行时的视角。鉴于对 so 优化更侧重于编译链接角度,并且通常一个 segment 包含多个 section(即链接视图对 so 的分解粒度更小),因此我们这里只讨论 so 的链接视图。 通过
在进行优化之前,我们需要对这些 section 以及它们之间的关系有一个清晰的认识,下图较直观地展示了 so 中各个 section 之间的关系(这里只绘制了本文涉及的 section): 结合上图,我们从另一个角度来理解 so 文件的结构:想象一下,我们把所有的函数实现体都放到 我们知道想要执行一个函数,只要跳转到它的地址就行了。那外界调用者(该 so 之外的模块)怎样知道它想要调用函数的地址呢?这里就涉及一个函数 ID 的问题:外部调用者给出需要调用的函数的 ID,而动态链接器(Linker)根据该 ID 查找目标函数的地址并告知外部调用者。所以 so 文件还需要一个结构去存储“ID-地址”的映射关系,这个结构就是动态符号表的所有导出符号。 具体到动态符号表的实现,ID 的类型是“字符串”,可以说动态符号表的所有导出符号构成了一个“字符串-地址“的映射表。调用者获取目标函数的地址后,准备好参数跳转到该地址就可以执行这个函数了。另一方面,当前 so 可能也需要调用其他 so 中的函数(例如 libc.so 中的 read、write 等),动态符号表的导入符号记录了这些函数的信息,在 so 内函数执行之前动态链接器会将目标函数的地址填入到相应位置,供该 so 使用。所以动态符号表是连接当前 so 与外部环境的“桥梁”:导出符号供外部使用,导入符号声明了该 so 需要使用的外部符号(注:实际上 结合 so 文件结构,接下来我们开始分析 so 中有哪些内容可以优化。 3. so 可优化内容分析在讨论 so 可优化内容之前,我们先了解一下 Android 构建工具(Android Gradle Plugin,下文简称 AGP)对 so 体积做的 strip 优化(移除调试信息和符号表)。AGP 编译 so 时,首先产生的是带调试信息和符号表的 so(任务名为 externalNativeBuildRelease),之后对刚产生的带调试信息和符号表的 so 进行 strip,就得到了最终打包到 apk 或 aar 中的 so(任务名为 stripReleaseDebugSymbols)。 strip 优化的作用就是删除输入 so 中的调试信息和符号表。这里说的符号表与上文中的“动态符号表”不同,符号表所在 section 名通常为 .symtab,它通常包含了动态符号表中的全部符号,并且额外还有很多符号。调试信息顾名思义就是用于调试该 so 的信息,主要是各种名字以
AGP 通过开启 strip 优化,可以大幅缩减 so 的体积,甚至可以达到十倍以上。以一个测试 so 为例,其最终 so 大小为14 KB,但是对应的带调试信息和符号表的 so 大小为 136 KB。不过在使用中,我们需要注意的是,如果 AGP 找不到对应的 strip 命令,就会把带调试信息和符号表的 so 直接打包到 apk 或 aar 中,并不会打包失败。例如缺少 armeabi 架构对应的 strip 命令时提示信息如下:
除了上述 Android 构建工具默认为 so 体积做的优化,我们还能做哪些优化呢?首先明确我们优化的原则:
基于以上原则,可以从以下三个方面对 so 继续进行深入优化:
so 可优化内容如下图所示(可删除部分用红色背景标出,可优化部分是 在确定了 so 中可以优化的内容后,我们还需要考虑优化时机的问题:是直接修改 so 文件,还是控制其生成过程?考虑到直接修改 so 文件的风险与难度较大,控制 so 的生成过程显然更稳妥。为了控制 so 的生成过程,我们先简要介绍一下 so 的生成过程: 如上图所示,so 的生成过程可以分为四个阶段:
可以看出,预处理和汇编阶段对特定输入产生的输出基本是固定的,优化空间较小。所以我们的优化方案主要是针对编译和链接阶段进行优化。 4. 优化方案介绍我们对所有能控制最终 so 体积的方案都进行调研,并验证了其效果,最后总结出较为通用的可行方案。 4.1 精简动态符号表使用 visibility 和 attribute 控制符号可见性 可以通过给编译器传递
CMake 项目的配置方式:
ndk-build 项目的配置方式:
另一方面,针对单个变量或函数,可以通过 attribute 方式指定其符号可见性,示例如下:
其常用值也是 default 和 hidden,与 visibility 方式意义类似,这里不再赘述。 attribute 方式指定的符号可见性的优先级,高于 visibility 方式指定的可见性,相当于 visibility 是全局符号可见性开关,attribute 方式是针对单个符号的可见性开关。这两种方式结合就能控制源码中每个符号的可见性。 需要注意的是上面这两种方式,只能控制变量或函数是否存在于动态符号表中(即是否删除其动态符号表项),而不会删除其实现体。 使用 static 关键字控制符号可见性 在C/C++语言中,static 关键字在不同场景下有不同意义,当使用 static 表示“该函数或变量仅在本文件可见”时,那么这个函数或变量就不会出现在动态符号表中,但只会删除其动态符号表项,而不会删除其实现体。static 关键字相当于是增强的 hidden(因为 static 声明的函数或变量编译时只对当前文件可见,而 hidden 声明的函数或变量只是在动态符号表中不存在,在编译期间对其他文件还是可见的)。在项目开发中,使用 static 关键字声明一个函数或变量“仅在本文件可见”是很好的习惯,但是不建议使用 static 关键字控制符号可见性:无法使用 static 关键字控制一个多文件可见的函数或变量的符号可见性。 使用 exclude libs 移除静态库中的符号 上述 visibility 方式、attribute 方式和 static 关键字,都是控制项目源码中符号的可见性,而无法控制依赖的静态库中的符号在最终 so 中是否存在。exclude libs 就是用来控制依赖的静态库中的符号是否可见,它是传递给链接器的参数,可以使依赖的静态库的符号在动态符号表中不存在。同样,也是只能删除符号表项,实现体仍然会存在于产生的 so 文件中。 CMake 项目的配置方式:
ndk-build 项目的配置方式:
使用 version script 控制符号可见性 version script 是传递给链接器的参数,用来指定动态库导出哪些符号以及符号的版本。该参数会影响到上面“so 文件格式”一节中
然后将上述文件的路径传递给链接器即可(假定上述文件名为 CMake 项目的配置方式:
ndk-build 项目的配置方式:
看上去,version script 是明确地指定需要保留的符号,如果通过 visibility 结合 attribute 的方式控制每个符号是否导出,也能达到 version script 的效果,但是 version script 方式有一些额外的好处:
综上所述,version script 方式优于 visibility 结合 attribute 的方式。同时,使用了 version script 方式,就不需要使用 exclude libs 方式控制依赖的静态库中的符号是否导出了。 4.2 移除无用代码开启 LTO LTO 是 Link Time Optimization 的缩写,即链接期优化。LTO 能够在链接目标文件时检测出 DeadCode 并删除它们,从而减小编译产物的体积。DeadCode 举例:某个 if 条件永远为假,那么 if 为真下的代码块就可以移除。进一步地,被移除代码块所调用的函数也可能因此而变为 DeadCode,它们又可以被移除。能够在链接期做优化的原因是,在编译期很多信息还不能确定,只有局部信息,无法执行一些优化。但是链接时大部分信息都确定了,相当于获取了全局信息,所以可以进行一些优化。GCC 和 Clang 均支持 LTO。LTO 方式编译的目标文件中存储的不再是具体机器的指令,而是机器无关的中间表示(GCC 采用的是 GIMPLE 字节码,Clang 采用的是 LLVM IR 比特码)。 CMake 项目的配置方式:
ndk-build 项目的配置方式:
使用 LTO 时需要注意几点:
开启 GC sections 这是传递给链接器的参数,GC 即 Garbage Collection(垃圾回收),也就是对无用的 section 进行回收。注意,这里的 section 不是指最终 so 中的 section,而是作为链接器的输入的目标文件中的 section。 简要介绍一下目标文件,目标文件(扩展名 .o )也是 ELF 文件,所以也是由 section 组成的,只不过它只包含了相应源文件的内容:函数会放到 GC sections 参数通知链接器:仅保留动态符号(及 CMake 项目的配置方式:
ndk-build 项目的配置方式:
4.3 优化指令长度使用 Oz/Os 优化级别 编译器根据输入的 -Ox 参数决定编译的优化级别,其中 O0 表示不开启优化(这种情况主要是为了便于调试以及更快的编译速度),从 O1 到 O3,优化程度越来越强。Clang 和 GCC 均提供了 Os 的优化级别,其与 O2 比较接近,但是优化了生成产物的体积。而 Clang 还提供了 Oz 优化级别,在 Os 的基础上能进一步优化产物体积。 综上,编译器是 Clang,可以开启 Oz 优化。如果编译器是 GCC,则只能开启 Os 优化(注:NDK 从 r13 开始默认编译器从 GCC 变为 Clang,r18 中正式移除了 GCC。GCC 不支持 Oz 是指 Android 最后使用的 GCC4.9 版本不支持 Oz 参数)。Oz/Os 优化相比于 O3 优化,优化了产物体积,性能上可能有一定损失,因此如果项目原本使用了 O3 优化,可根据实际测试结果以及对性能的要求,决定是否使用 Os/Oz 优化级别,如果项目原本未使用 O3 优化级别,可直接使用 Os/Oz 优化。 CMake 项目的配置方式(如果使用 GCC,应将 Oz 改为 Os):
ndk-build 项目的配置方式(如果使用 GCC,应将 Oz 改为 Os):
4.4 其他措施禁用 C++ 的异常机制 如果项目中没有使用 C++ 的异常机制(例如 CMake 项目的配置方式:
ndk-build 默认会禁用 C++ 的异常机制,因此无需特意禁用(如果现有项目开启了 C++ 的异常机制,说明确有需要,需仔细确认后才能禁用)。 禁用 C++ 的 RTTI 机制 如果项目中没有使用 C++ 的 RTTI 机制(例如 typeid 和 dynamic_cast 等),可以通过禁用 C++ 的 RTTI ,来减小 so 的体积。 CMake 项目的配置方式:
ndk-build 默认会禁用 C++ 的 RTTI 机制,因此无需特意禁用(如果现有项目开启了 C++ 的 RTTI 机制,说明确有需要,需仔细确认后才能禁用)。 合并 so 以上都是针对单个 so 的优化方案,对单个 so 进行优化后,还可以考虑对 so 进行合并,能够进一步减小 so 的体积。具体来讲,当安装包内某些 so 仅被另外一个 so 动态依赖时,可以将这些 so 合并为一个 so。例如 liba.so 和 libb.so 仅被 libx.so 动态依赖,可以将这三个 so 合并为一个新的 libx.so。合并 so 有以下好处:
可以在不修改项目源码的情况下,在编译层面实现 so 的合并。 提取多 so 共同依赖库 上面“合并 so”是减小 so 总个数,而这里是增加 so 总个数。当多个 so 以静态方式依赖了某个相同的库时,可以考虑将此库提取成一个单独的 so,原来的几个 so 改为动态依赖该 so。例如 liba.so 和 libb.so 都静态依赖了 libx.a,可以优化为 liba.so 和 libb.so 均动态依赖 libx.so。提取多 so 共同依赖库,可以对不同 so 内的相同代码进行合并,从而减小总的 so 体积。 这里典型的例子是 libc++ 库:如果存在多个 so 都静态依赖 libc++ 库的情况,可以优化为这些 so 都动态依赖于 4.5 整合后的通用方案通过上述分析,我们可以整合出普通项目均可使用的通用的优化方案,CMake 项目的配置方式(如果使用 GCC,应将 Oz 改为 Os):
ndk-build 项目的配置方式(如果使用 GCC,应将 Oz 改为 Os):
其中
说明:version script 方式指定所有需要导出的符号,不再需要 visibility 方式、attribute 方式、static 关键字和 exclude libs 方式控制导出符号。是否禁用 C++ 的异常机制和 RTTI 机制、合并 so 以及提取多 so 共同依赖库取决于具体项目,不具有通用性。 至此,我们总结出一套可行的 so 体积优化方案。但在工程实践中,还有一些问题要解决。 5. 工程实践支持多种构建工具美团有众多业务使用了 so,所使用的构建工具也不尽相同,除了上述常见的 CMake 和 ndk-build,也有项目在使用 Make、Automake、Ninja、GYP 和 GN 等各种构建工具。不同构建工具应用 so 优化方案的方式也不相同,尤其对大型工程而言,配置复杂性较高。 基于以上原因,每个业务自行配置 so 优化方案会消耗较多的人力成本,并且有配置无效的可能。为了降低配置成本、加快优化方案的推进速度、保证配置的有效性和正确性,我们在构建平台上统一支持了 so 的优化(支持使用任意构建工具的项目)。业务只需进行简单的配置即可开启 so 的体积优化。 配置导出符号的注意事项注意事项有以下两点:
要确定 start 函数真正的符号可以对未优化的 libexample.so 执行以下命令。因为 C++ 对符号修饰后,函数名是符号的一部分,所以可以通过 grep 加快查找: 可以看到 start 函数真正的符号是 第二种方式是在
上述配置可以导出 MyClass 的 start 和 stop 函数。其原理是,链接时链接器对每个符号进行 demangle(解构,即把修饰后的符号还原为可读的表示),然后与 extern "C++" 中的条目进行匹配,如果能与任一条目匹配成功就保留该符号。匹配的规则是:有双引号的条目不能使用通配符,需要全字符串完全匹配才可以(例如 stop 条目,如果括号之间多一个空格就会匹配失败)。对于没有双引号的条目能够使用通配符(例如 start 条目)。 查看优化后 so 的导出符号业务对 so 进行优化之后,需要查看最终的 so 文件中保留了哪些导出符号,验证优化效果是否符合预期。在 Mac 和 Linux 下均可使用下述命令查看 so 保留了哪些导出符号:
例如: 可以看出,libexample.so 的导出符号有两个: 解析崩溃堆栈本文的优化方案会移除非必要导出的动态符号,那 so 如果发生崩溃的话是不是就无法解析崩溃堆栈了呢?答案是完全不会影响崩溃堆栈的解析结果。 “so 可优化内容分析”一节已经提过,使用带调试信息和符号表的 so 解析线上崩溃,是分析 so 崩溃的标准方式(这也是 Google 解析 so 崩溃的方式)。本文的优化方案并未修改调试信息和符号表,所以可以使用带调试信息和符号表的 so 对崩溃堆栈进行完整的还原,解析出崩溃堆栈每个栈帧对应的源码文件、行号和函数名等信息。业务编译出 release 版的 so 后将相应的带调试信息和符号表的 so 上传到 crash 平台即可。 6. 方案收益优化 so 对安装包体积和安装后占用的本地存储空间有直接收益,收益大小取决于原 so 冗余代码数量和导出符号数量等具体情况,下面是部分 so 优化前后占用安装包体积的对比: 下面是上述 so 优化前后占用本地存储空间的对比: 7. 总结与规划 对 so 体积进行优化不仅能够减小安装包体积,而且能获得以下收益:
我们对后续工作做了如下的规划:
8. 参考资料
9. 本文作者 洪凯、常强,来自美团平台/App技术部。 ----------? END? ---------- 也许你还想看 ? |?Android视频技术探索之旅:美团外卖商家端的实践 阅读更多 --- |
|
移动开发 最新文章 |
Vue3装载axios和element-ui |
android adb cmd |
【xcode】Xcode常用快捷键与技巧 |
Android开发中的线程池使用 |
Java 和 Android 的 Base64 |
Android 测试文字编码格式 |
微信小程序支付 |
安卓权限记录 |
知乎之自动养号 |
【Android Jetpack】DataStore |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/25 1:40:55- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |