查看原文
其他

Android NDK减少so库体积方法总结

哆啦安全 2022-07-23

Android包体积优化(常规、进阶、极致)

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参数,配置如下所示:

SET_TARGET_PROPERTIES(yoga PROPERTIES LINK_FLAGS "-Wl,-s")

也可以手动执行ndk提供的aarch64-linux-android-strip命令移除动态库中的调试信息,这种方式除了前面方法外优化体积最高的方式,比如libLibSampleApp.so从48M直接优化到了992k。

5.设置编译器的优化flag

编译器有个优化flag可以设置,分别是-Os(体积最小),-O3(性能最优)等。这里将编译器的优化flag设置为-Os,以便减少体积。

CMake:

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Os")
set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}")

Android.mk

LOCAL_CPPFLAGS += -Os
LOCAL_CFLAGS += -Os

除了直接删除占用体积较大的模块外,编译器优化是排下来优化空间最大的方法。设置完-Os后占用提交较大的前几个库体积对比:

库名优化前体积优化后体积
libLibSampleApp.so48M33M
libAVSCommon.so28M22M
libDefaultClient.so14M9.9M

6.使用 gc-sections去除没有用到的函数

有些时候代码量比较大的时候我们没办法手动发现无用的函数,这个时候可以可以开启编译器的gc-sections选项,让编译器自动的帮你做到这一点。

编译器可以配置自动去除未使用的函数和变量,以下是配置方式:

CMake:

# 去除未使用函数与变量
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ffunction-sections -fdata-sections")
set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}")
# 设置去除未使用代码的链接flag
SET_TARGET_PROPERTIES(yoga PROPERTIES LINK_FLAGS "-Wl,--gc-sections")        

Android.mk:

OCAL_CPPFLAGS += -ffunction-sections -fdata-sections
LOCAL_CFLAGS += -ffunction-sections -fdata-sections 
LOCAL_LDFLAGS += -Wl,--gc-sections

7.设置编译器的 Visibility Feature

Visibility Feature就是用来控制在哪些函数可以在符号表中被输入,由于C++并不是完全面向对象的,非类的方法并没有public这种修饰符,因此,要用Visibility Feature来控制哪些函数可以被外部调用。而JNI提供了一个宏-JNIEXPORT来控制这点。所以只要对函数加上这个宏,像这样:

// JNIEXPORT就是控制可见的宏
// JNICALL在NDK这里没有什么意义,只是个标识宏
JNIEXPORT void JNICALL Java_ClassName_MethodName(JNIEnv *env, jobject obj, jstring javaString)

然后在编译器的FLAGS选项开启 -fvisibility = hidden 就可以。这样,不仅可以控制函数的可见性,并且可以减少包体的大小。

8.去除C++代码中的iostream等直接IO相关代码

使用STL中的iostream相关库会明显的增加包的体积,而Android本身是有预编译库(android/log.h)可以代替输入到控制台的工具的。在我们的SDK中由于之前是控制台程序所以用到了输入输出,编译的时候没有把这块排除出去,造成了一定的体积冗余。

9.STL的使用方式

对于C++的library,引用方式有2种:

  • 静态方式(static)

  • 动态方式(shared)

其中,静态方式在编译时会将用到的相关代码直接复制到目的文件中;而动态方式则会将相关的代码打成so文件,以便多次引用。由于编译器在编译时并不能知道所有被引用的地方,所以同时会打入了很多不相关的代码。

所以,如果项目中引用library的函数较多时,用动态方式可以避免多次拷贝,节省空间。相反,则直接使用静态方式会更节省空间。由于我们SDK的模块特别多,再加上整体APK里面已经有其他业务引入了动态库,所以我们用动态库的方式。

10.不使用Exception和RTTI

关于这两点在网上看到的没有实践过,不过拿过来可以作为包体积持续优化的参考。

RTTI

通过RTTI,能够通过基类的指针或引用来检索其所指对象的实际类型,即运行时获取对象的实际类型。C++通过下面两个操作符提供RTTI。

(1)typeid:返回指针或引用所指对象的实际类型。

(2)dynamic_cast:将基类类型的指针或引用安全的转换为派生类型的指针或引用。

RTTI的选项是默认关闭的的,而代码中其实并没有用到相关的功能,这里可以直接关闭。

Exception

使用C++的exception会增加包的大小,而目前JNI对C++的exception的支持是有bug的,比如下面这段代码就会引起程序的crash(对于低版本的android NDK)。因此要在程序中引入exception要自己实现相关逻辑,但是这样又会增加包体大小。对于开发者来说,exception可以帮助快速定位问题,而对于使用者并不是那么重要,这里可以去掉。

11.总结

本文介绍了删除无用模块,平台能力替代第三方库,使用 strip,设置编译器优化的 flag,使用gc-sections去除没有用到的函数,设置可见性,去除iostream等有助于动态库体积优化的方法。

原文链接: https://juejin.cn/post/7084238491089027079



推荐阅读

超实用的优质公众号推荐

学抓包就来"哆啦安全"学

玩转Android adb命令(adb降级)

Android APK和API漏洞扫描器

移动端过检测抓包最全解决方案

Android App开发Bug解决完美方案

JNI与NDK编程(基础到精通)最全总结

Android系统run-as命令原理(权限访问)

APP漏洞检测之静态动态安全检测APP的常见风险

Android系统中run-as和su命令的源码解析(升降权限)

Android系统定制之Android.mk和Android.bp语法详解(精通版)


最后,扫描关注,获取更多安全/开发相关课程,课程持续更新中!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存