前段时间作完咱们的 SDK 项目,没有关注 so 库大小这块,如今慢慢稳定了就须要追求 so 库体积了。小团队通常可能不会在乎这个东西,毕竟如今流量已经不是几年前的奢侈品了。可是要知道so库的大小不只影响的是应用商店app的大小,还有一个很大的影响就是在广告页面渠道要求的秒下载,太大的app下载速度慢用户会不耐烦,直接影响了这部分用户的转化。html
armeabi 第5/6代 ARM v5TE,使用软件浮点运算,兼容全部ARM设备,通用性强,速度慢java
armeabi-v7a 第7代 ARM v7,使用硬件浮点运算,具备高级扩展功能(目前大部分手机都是这个架构)linux
arm64-v8a 第8代,64位,包含AArch3二、AArch64两个执行状态对应3二、64bitandroid
x86 intel 32位,通常用于平板c++
x86_64 intel 64位,通常用于平板(支持 x86 和 x86_64)web
mips 基本没见过(支持 mips)算法
mips64 基本没见过(支持 mips 和 mips_64)windows
对于手机来讲,目前市面上占到 99% 的设备都是 armeabi 或者armeabi-v7a 和 arm64-v8a。虽说 arm64-v8a 架构的手机慢慢发展起来了,可是其中 armeabi-v7a 仍是占到绝大多数位置,可是随着如今手机更新换代的加速,arm64-v8a 慢慢的就会成为主流。微信
通常来讲咱们编译的 ABI 为 armeabi-v7a 的包已经能基本上能适配市面上绝大多数手机了,能够保证运行在 armeabi-v7a 架构上效率确定是最高的,而在其余的架构上因为增长了模拟层,致使性能会有所损失。好比64位设备(arm64-v8a)可以运行32位的函数库,可是以32位模式运行,将丢失专为64位优化过的性能(ART,webview,media等)。架构
即意味着 arm64-v8a 架构的 so 库是能够运行在 arm64-v8a、armeabi-v7a 和 armeabi 设备上的。armeabi-v7a 架构的 so 库是能够运行在 armeabi-v7a 和 armeabi 设备上的。
这块的内容不少文章没有说清楚,我根据实测案例描述一遍(测试环境:小米10,android studio 3.1.3,NDK:r20):
Android 加载 so 库时是从当前手机支持的最高 CPU 架构文件夹开始:
java.lang.UnsatisfiedLinkError: Unable to load library 'soTest'
因此最好的状况即是分别编译不一样 abi 架构的 so 库。
总结下来,有两种解决方案去优化 App 大小:
另外还发现个小彩蛋,抖音和QQ尚未使用 flutter 开发。
从 abi 架构去优化 so 库体积,其实不是咱们想要的方案,由于如今大多数应用已经不会附带 3 个以上的 abi 架构 so 库。所以这方面的优化程度有限。所以咱们要从另外的方向,即编译指令上优化生成的 so 库体积。
原本想直接说使用哪些指令优化,优化的效果是什么的,可是里面又牵扯一些其余知识,好比这个优化指令是谁的指令,编译器仍是 ndk?那不一样的编译器能使用相同的指令吗?若是不从头理一下这个流程,就感受来的很突兀,容易让人摸不着头脑。
在此以前咱们须要理清楚一个概念,即 Cmake、MakeFile、nmake、make 这些概念的联系和本质:
nmake makefiles
生成器(见下图),则最后编译的时候咱们就须要选择 nmake 工具。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-23VjDSJ4-1592311644999)(DED91F803B8C469281C1B0AD01A7D385)]
Native Development Kit
,是一个属于 Android 的开发工具包,和 Java 无关,有了它,让 Android 程序能够和 C/C++ 交互,它里面提供的工具能够将 so 库和 Android 代码一块儿打包成 APK。而且 NDK 里面提供的各类交叉编译器,能够生成不一样 CPU 架构的动态库。Java Native Interface
,Java 本地接口,顾名思义,是接口定义,JNI 代码能够在 Java 代码里调用 C、C++ 等语言的代码 或 C、C++ 代码调用 Java 代码。因为 Java 语言的跨平台性,使得它和本地代码的交互能力很弱,所以才有了 JNI 能够加强 Java 和 本地代码交互的能力。看懂了上面的释义,而后咱们再理一下一个 so 库从编译到能够在 Android 中运行所经历的过程(基于windows平台):
因为 NDK 从 r17 已经废弃了gcc,推荐使用 clang 编译,所以本文基于 cmake + clang + ndk r20 构建 so 库。
GCC特性:除支持C/C++/ Objective-C/Objective-C++语言外,还支持Java/Ada/Fortran/Go等;支持更多平台;更流行,普遍使用,支持完备。
Clang特性:编译速度快;内存占用小;兼容GCC;设计清晰简单、容易理解,易于扩展加强;基于库的模块化设计,易于IDE集成;出错提示更友好。
所以推荐之后无论是学习测试仍是项目都使用 clang 进行编译。
上面提一嘴 gcc 与 clang 的缘由是有一个容易让人陷入误区的地方。在 cmake 中有两个参数是:CMAKE_C_FLAGS 和 CMAKE_CXX_FLAGS
,用来设置编译器选项。可是咱们知道 CFLAGS 参数和 CPPFLAGS 参数是 gcc 编译器才有的指令,clang 是没有这个指令的。那在 cmake 中设置了CMAKE_CXX_FLAGS
还会有效果吗?
重点:CMAKE_CXX_FLAGS != CXXFLAGS
即 cmake 中的 CMAKE_CXX_FLAGS 并非 gcc 编译指令中的 CXXFLAGS。
CMAKE_CXX_FLAGS 只是 cmake 用来告诉编译器(无论是gcc仍是clang)的编译指令,即 cmake 会解析 CMAKE_CXX_FLAGS 参数中的内容传递给具体的编译器。
所以对于 clang 编译器来讲,cmake 中设置 CMAKE_CXX_FLAGS 也是生效的。只是说有可能 CMAKE_CXX_FLAGS 中的某些 gcc 指令 clang 不识别,或者说某些 clang 指令 gcc 不识别。好比说:-lz
指令在 clang 下编译会出现警告:
或者说出现错误;
有了上面的内容,终于能够进入正题说下那些参数能够帮助咱们减少 so 库的体积。
因为咱们使用 ndk 编译时,编译器是 ndk 自带的,好比下面的编译器:
clang 是在 ndk 目录下,下面的参数都是 gcc 或者 clang 编译参数。
1.异常与运行时(gcc 和 clang)
-fno-exceptions -fno-rtti
开启异常和运行时:3998kb
关闭异常和运行时:3998kb
默认状况下,ndk 中的 C++ 异常和运行时是被关闭的,若是项目打开这个选项了,能够考虑关闭,由于 ndk 对 C++ 异常支持的不够友好,因此大多数状况下异常是起不到实质做用的。 可是从上面咱们的测试能够看出,so 库大小没变,可能和代码有关,可是也能够看出这两个选项对 so 库的大小影响有限,所以重要程度并不高。
2.导出函数可见性(gcc 和 clang)
-fvisibility=hidden
默认时:3998kb
设置 hidden 后:3933kb,减少了 0.01%
默认状况下,该选项是 default 的,即so库中大部分的函数或者全局变量都会被导出,且是可见的,-fvisibility=hidden能够显著地提升连接和加载共享库的性能,生成更加优化的代码,保证只有 export 修饰的函数才会导出。建议在编译共享库的时候使用它。
3.丢弃未使用的函数(只有gcc)
set(CMAKE_SHARED_LINKER_FLAGS "-Wl,--gc-sections") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ffunction-sections -fdata-sections")
编译的时候,加入-ffunction-sections, -fdata-sections 选项,在连接的时候,加入–gc-sections选项。
编译的时候,把每一个函数做为一个section,每一个数据(应该是指全局变量之类的吧)也做为一个section,这样连接的时候,–gc-sections会把没用到的section丢弃掉,最终的可执行文件就只包含用到了的函数和数据。
4. 产生与位置无关代码,避免so库加载重定位(gcc)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC")
-fPIC 做用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code),则产生的代码中,没有绝对地址,所有使用相对地址,故而代码能够被加载器加载到内存的任意位置,均可以正确的执行。若是不加 -fPIC,则加载 so 文件的代码段时,代码段引用的数据对象须要重定位, 重定位会修改代码段的内容,这就形成每一个使用这个 so 文件代码段的进程在内核里都会生成这个 so 文件代码段的 copy。
5. O1(gcc 和 clang)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O1")
目的是在不影响编译速度的前提下,尽可能采用一些优化算法下降代码大小和可执行代码的运行速度。
6.O2(gcc 和 clang)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2")
该优化选项会牺牲部分编译速度,除了执行 -O1 所执行的全部优化以外,还会采用几乎全部的目标配置支持的优化算法,用以提升目标代码的运行速度。
7.O3(gcc 和 clang)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3")
该选项除了执行 -O2 全部的优化选项以外,通常都是采起不少向量化算法,提升代码的并行执行程度,利用现代CPU中的流水线,Cache 等。
8. Os(gcc 和 clang)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Os")
这个优化标识和-O3有殊途同归之妙,固然二者的目标不同,-O3的目标是宁愿增长目标代码的大小,也要拼命的提升运行速度,可是这个选项是在-O2的基础之上,尽可能的下降目标代码的大小,这对于存储容量很小的设备来讲很是重要。
9. Ofast(gcc 和 clang)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Ofast")
该选项将不会严格遵循语言标准,除了启用全部的-O3优化选项以外,也会针对某些语言启用部分优化。如:-ffast-math。
10. -s(gcc 和 clang)
set(CMAKE_SHARED_LINKER_FLAGS "-Wl,-s")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -s")
添加 -s 前:
添加 -s 后:
清除符号表信息,-s和-S的区别在于-S移除调试符号信息,而-s移除全部符号信息。
参考:Clang 11 documentation-Clang Compiler User’s Manual
参考:Using the GNU Compiler Collection (GCC)-Options That Control Optimization
参考:Using the GNU Compiler Collection (GCC)-Options Controlling C++ Dialect
参考:Android NDK: How to Reduce Binaries Size – The Algolia Blog
参考:GCC中-O1 -O2 -O3 优化的原理是什么?
若有帮助,请多多点赞支持,谢谢。