Android C++ 系列:NDK 减少 so 库体积方法总结
1. 背景
基于亚马逊 AVS Device SDK 改造的全链路语音 SDK 最终编译的动态库有几十个,单架构动态库大小有几十兆,之前在 Iot 设备中勉强跑着,但是这个体积对于手机应用来说是致命的,各个模块费事费力能优化个几 K 的体积就不错了,我这直接给上个几十兆的,APP 平台方肯定无法接受。但是一是有业务需求,二是自己又想把 SDK 推到手机 APP,提高用户量,验证 SDK 的稳定性和交互体验,所以开始了漫长的瘦身过程,最后单架构压缩到了五兆一下,虽然还是有点大,但是比起之前有了很大的提升。
2. 删除无用模块
AVS Device SDK 是主要应用在音响的控制台程序,而且代码是跨平台的,所以一是有很多为了跨平台做的冗余,二是有很多我们根本用不到的模块。比如为了做本地存储引入了一个 Sqlite 的动态库,我们本身也用不到本地存储,像闹钟设置之类的放到 APP 层即可,而且就算是需要存储也完全可以使用 Android 和 iOS 平台提供的 Sqlite。删除用不到的模块是包体积优化空间最大最快的。
3. 第三方库替换为 Android/iOS 平台提供能力
AVS Device SDK 在 Android 平台基于 ffmpeg 做解码实现了音频播放器,对于我们的场景主要使用用播放器来播放 TTS,而 TTS 是和服务协商好固定的 mp3 格式,完全没有必要为了一个 mp3 解码引入一个庞大的 ffmpeg 库。这里我们使用 Android 平台提供的 Jni 层的媒体库来做音频解码。而且即使是 Android 平台 JNI 层不支持,也可以单独依赖一个 mp3 解码库,而不是庞大的 ffmpeg。对于整个包体积来说,第三方模块往往相对来说是比较大的。
4. 使用 strip
使用 NDK toolchain 可以把调试的 C++ 符号表(Symbol Table)中数据删除,我们一般我们打成 APK 会自动帮我们做这个工作,当然也可以手动设置:
手动的在链接选项中加入 strip 参数,配置如下所示:
也可以手动执行 ndk 提供的aarch64-linux-android-strip
命令移除动态库中的调试信息,这种方式除了前面方法外优化体积最高的方式,比如 libLibSampleApp.so 从 48M 直接优化到了 992k。
4. 设置编译器的优化 flag
编译器有个优化 flag 可以设置,分别是-Os(体积最小),-O3(性能最优)等。这里将编译器的优化 flag 设置为-Os,以便减少体积。
CMake:
Android.mk
除了直接删除占用体积较大的模块外,编译器优化是排下来优化空间最大的方法。设置完-Os
后占用提交较大的前几个库体积对比:
5. 使用 gc-sections 去除没有用到的函数
有些时候代码量比较大的时候我们没办法手动发现无用的函数,这个时候可以可以开启编译器的 gc-sections 选项,让编译器自动的帮你做到这一点。
编译器可以配置自动去除未使用的函数和变量,以下是配置方式:
CMake:
Android.mk:
6. 设置编译器的 Visibility Feature
Visibility Feature 就是用来控制在哪些函数可以在符号表中被输入,由于 C++并不是完全面向对象的,非类的方法并没有 public 这种修饰符,因此,要用 Visibility Feature 来控制哪些函数可以被外部调用。而 JNI 提供了一个宏-JNIEXPORT 来控制这点。所以只要对函数加上这个宏,像这样:
然后在编译器的 FLAGS 选项开启 -fvisibility = hidden 就可以。这样,不仅可以控制函数的可见性,并且可以减少包体的大小。
7. 去除 C++代码中的 iostream 等直接 IO 相关代码
使用 STL 中的 iostream 相关库会明显的增加包的体积,而 Android 本身是有预编译库(android/log.h)可以代替输入到控制台的工具的。在我们的 SDK 中由于之前是控制台程序所以用到了输入输出,编译的时候没有把这块排除出去,造成了一定的体积冗余。
8. STL 的使用方式
对于 C++的 library,引用方式有 2 种:
静态方式(static)
动态方式(shared)
其中,静态方式在编译时会将用到的相关代码直接复制到目的文件中;而动态方式则会将相关的代码打成 so 文件,以便多次引用。由于编译器在编译时并不能知道所有被引用的地方,所以同时会打入了很多不相关的代码。
所以,如果项目中引用 library 的函数较多时,用动态方式可以避免多次拷贝,节省空间。相反,则直接使用静态方式会更节省空间。由于我们 SDK 的模块特别多,再加上整体 APK 里面已经有其他业务引入了动态库,所以我们用动态库的方式。
9. 不使用 Exception 和 RTTI
关于这两点在网上看到的没有实践过,不过拿过来可以作为包体积持续优化的参考。
RTTI
通过 RTTI,能够通过基类的指针或引用来检索其所指对象的实际类型,即运行时获取对象的实际类型。C++通过下面两个操作符提供 RTTI。
(1)typeid:返回指针或引用所指对象的实际类型。
(2)dynamic_cast:将基类类型的指针或引用安全的转换为派生类型的指针或引用。
RTTI 的选项是默认关闭的的,而代码中其实并没有用到相关的功能,这里可以直接关闭。
Exception
使用 C++的 exception 会增加包的大小,而目前 JNI 对 C++的 exception 的支持是有 bug 的,比如下面这段代码就会引起程序的 crash(对于低版本的 android NDK)。因此要在程序中引入 exception 要自己实现相关逻辑,但是这样又会增加包体大小。对于开发者来说,exception 可以帮助快速定位问题,而对于使用者并不是那么重要,这里可以去掉。
10 总结
本文介绍了删除无用模块,平台能力替代第三方库,使用 strip,设置编译器优化的 flag,使用 gc-sections 去除没有用到的函数,设置可见性,去除 iostream 等有助于动态库体积优化的方法。
版权声明: 本文为 InfoQ 作者【轻口味】的原创文章。
原文链接:【http://xie.infoq.cn/article/e670ae4bd73cf424ea6b4940f】。文章转载请联系作者。
评论