◆ 背景
基于亚马逊 AVS Device SDK 改造的全链路语音 SDK 最终编译的动态库有几十个,单架构动态库大小有几十兆,之前在 Iot 设备中勉强跑着,但是这个体积对于手机应用来说是致命的,各个模块费事费力能优化个几 K 的体积就不错了,我这直接给上个几十兆的,APP 平台方肯定无法接受。但是一是有业务需求,二是自己又想把 SDK 推到手机 APP,提高用户量,验证 SDK 的稳定性和交互体验,所以开始了漫长的瘦身过程,最后单架构压缩到了五兆一下,虽然还是有点大,但是比起之前有了很大的提升。
◆ 删除无用模块
AVS Device SDK 是主要应用在音响的控制台程序,而且代码是跨平台的,所以一是有很多为了跨平台做的冗余,二是有很多我们根本用不到的模块。比如为了做本地存储引入了一个 Sqlite 的动态库,我们本身也用不到本地存储,像闹钟设置之类的放到 APP 层即可,而且就算是需要存储也完全可以使用 Android 和 iOS 平台提供的 Sqlite。删除用不到的模块是包体积优化空间最大最快的。
◆ 第三方库替换为 Android/iOS 平台提供能力
AVS Device SDK 在 Android 平台基于 ffmpeg 做解码实现了音频播放器,对于我们的场景主要使用用播放器来播放 TTS,而 TTS 是和服务协商好固定的 mp3 格式,完全没有必要为了一个 mp3 解码引入一个庞大的 ffmpeg 库。这里我们使用 Android 平台提供的 Jni 层的媒体库来做音频解码。而且即使是 Android 平台 JNI 层不支持,也可以单独依赖一个 mp3 解码库,而不是庞大的 ffmpeg。对于整个包体积来说,第三方模块往往相对来说是比较大的。
◆ 使用 strip
使用 NDK toolchain 可以把调试的 C 符号表(Symbol Table)中数据删除,我们一般我们打成 APK 会自动帮我们做这个工作,当然也可以手动设置:
手动的在链接选项中加入 strip 参数,配置如下所示:
代码语言:javascript复制SET_TARGET_PROPERTIES(yoga PROPERTIES LINK_FLAGS "-Wl,-s")
也可以手动执行 ndk 提供的aarch64-linux-android-strip
命令移除动态库中的调试信息,这种方式除了前面方法外优化体积最高的方式,比如 libLibSampleApp.so 从 48M 直接优化到了 992k。
◆ 设置编译器的优化 flag
编译器有个优化 flag 可以设置,分别是-Os(体积最小),-O3(性能最优)等。这里将编译器的优化 flag 设置为-Os,以便减少体积。
CMake:
代码语言:javascript复制set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Os")set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}")
Android.mk
代码语言:javascript复制LOCAL_CPPFLAGS = -OsLOCAL_CFLAGS = -Os
除了直接删除占用体积较大的模块外,编译器优化是排下来优化空间最大的方法。设置完-Os
后占用提交较大的前几个库体积对比:
◆ 使用 gc-sections 去除没有用到的函数
有些时候代码量比较大的时候我们没办法手动发现无用的函数,这个时候可以可以开启编译器的 gc-sections 选项,让编译器自动的帮你做到这一点。
编译器可以配置自动去除未使用的函数和变量,以下是配置方式:
CMake:
代码语言:javascript复制# 去除未使用函数与变量set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ffunction-sections -fdata-sections")set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}")# 设置去除未使用代码的链接flagSET_TARGET_PROPERTIES(yoga PROPERTIES LINK_FLAGS "-Wl,--gc-sections")
Android.mk:
代码语言:javascript复制OCAL_CPPFLAGS = -ffunction-sections -fdata-sectionsLOCAL_CFLAGS = -ffunction-sections -fdata-sections LOCAL_LDFLAGS = -Wl,--gc-sections
◆ 设置编译器的 Visibility Feature
Visibility Feature 就是用来控制在哪些函数可以在符号表中被输入,由于 C 并不是完全面向对象的,非类的方法并没有 public 这种修饰符,因此,要用 Visibility Feature 来控制哪些函数可以被外部调用。而 JNI 提供了一个宏-JNIEXPORT 来控制这点。所以只要对函数加上这个宏,像这样:
代码语言:javascript复制// JNIEXPORT就是控制可见的宏// JNICALL在NDK这里没有什么意义,只是个标识宏JNIEXPORT void JNICALL Java_ClassName_MethodName(JNIEnv *env, jobject obj, jstring javaString)
然后在编译器的 FLAGS 选项开启 -fvisibility = hidden 就可以。这样,不仅可以控制函数的可见性,并且可以减少包体的大小。
◆ 去除 C 代码中的 iostream 等直接 IO 相关代码
使用 STL 中的 iostream 相关库会明显的增加包的体积,而 Android 本身是有预编译库(android/log.h)可以代替输入到控制台的工具的。在我们的 SDK 中由于之前是控制台程序所以用到了输入输出,编译的时候没有把这块排除出去,造成了一定的体积冗余。
◆ STL 的使用方式
对于 C 的 library,引用方式有 2 种:
- 静态方式(static)
- 动态方式(shared)
其中,静态方式在编译时会将用到的相关代码直接复制到目的文件中;而动态方式则会将相关的代码打成 so 文件,以便多次引用。由于编译器在编译时并不能知道所有被引用的地方,所以同时会打入了很多不相关的代码。
所以,如果项目中引用 library 的函数较多时,用动态方式可以避免多次拷贝,节省空间。相反,则直接使用静态方式会更节省空间。由于我们 SDK 的模块特别多,再加上整体 APK 里面已经有其他业务引入了动态库,所以我们用动态库的方式。
◆ 不使用 Exception 和 RTTI
关于这两点在网上看到的没有实践过,不过拿过来可以作为包体积持续优化的参考。
RTTI
通过 RTTI,能够通过基类的指针或引用来检索其所指对象的实际类型,即运行时获取对象的实际类型。C 通过下面两个操作符提供 RTTI。
(1)typeid:返回指针或引用所指对象的实际类型。
(2)dynamic_cast:将基类类型的指针或引用安全的转换为派生类型的指针或引用。
RTTI 的选项是默认关闭的的,而代码中其实并没有用到相关的功能,这里可以直接关闭。
Exception
使用 C 的 exception 会增加包的大小,而目前 JNI 对 C 的 exception 的支持是有 bug 的,比如下面这段代码就会引起程序的 crash(对于低版本的 android NDK)。因此要在程序中引入 exception 要自己实现相关逻辑,但是这样又会增加包体大小。对于开发者来说,exception 可以帮助快速定位问题,而对于使用者并不是那么重要,这里可以去掉。
◆ 总结
本文介绍了删除无用模块,平台能力替代第三方库,使用 strip,设置编译器优化的 flag,使用 gc-sections 去除没有用到的函数,设置可见性,去除 iostream 等有助于动态库体积优化的方法。
来源:
https://xie.infoq.cn/article/e670ae4bd73cf424ea6b4940f
“IT大咖说”欢迎广大技术人员投稿,投稿邮箱:aliang@itdks.com
来都来了,走啥走,留个言呗~
IT大咖说 | 关于版权
由“IT大咖说(ID:itdakashuo)”原创的文章,转载时请注明作者、出处及微信公众号。投稿、约稿、转载请加微信:ITDKS10(备注:投稿),茉莉小姐姐会及时与您联系!
感谢您对IT大咖说的热心支持!
- 相关推荐
- 推荐文章
- Docker看完即掌握
- [开源]多应用、多租户、多终端的SaaS平台开发框架,SaaS服务平台
- 如何在断开连接后保持远程 SSH 会话运行
- 还在用维恩图可视化SQL的Join连接吗?你该看看这个
- Dubbo扩展点开发指南
- Avue - 更加贴合企业开发的数据驱动前端开发框架
- 用ClickHouse近乎实时地进行欺诈检测
- 2022年 6 款适用于 Linux 的最佳免费杀毒软件
- kafka2.x常用命令笔记(一)创建topic,查看topic列表、分区、副本详情,删除topic,测试topic发送与消费