STL的使用
对于C/C++的library,引用方式有2种:
-
静态方式(static)
-
动态方式(shared)
其中,静态方式在编译时会将用到的相关代码直接复制到目的文件中;而动态方式则会将相关的代码打成so文件,以便多次引用。由于编译器在编译时并不能知道所有被引用的地方,所以同时会打入了很多不相关的代码。
如果项目中引用library的函数较多时,用动态方式可以避免多次拷贝,节省空间。相反,则直接使用静态方式会更节省空间。
NDK开发中,可以通过gradle的设置来配置:
1 | defaultConfig{ |
不使用Exception和RTTI
C++的exception和RTTI功能在NDK中默认是关闭的,但是可以通过如下配置打开的:
Android.mk: APP_CPPFLAGS += -fexceptions -frtti
CMakeLists: set(CMAKE_CXX_FLAGS “${CMAKE_CXX_FLAGS} -fexceptions -frtti”)
-
RTTI
通过RTTI,能够通过基类的指针或引用来检索其所指对象的实际类型,即运行时获取对象的实际类型。C++通过下面两个操作符提供RTTI。(1)typeid:返回指针或引用所指对象的实际类型。
(2)dynamic_cast:将基类类型的指针或引用安全的转换为派生类型的指针或引用。
-
Exception
使用C++的exception会增加包的大小,而目前JNI对C++的exception的支持是有bug的,比如下面这段代码就会引起程序的crash(对于低版本的android NDK)。
使用strip配置
NDK toolchain不会自动的把方便调试的C++ 符号表(Symbol Table)中数据删除,而只会在打APK包的时候进行这一操作。这就导致了打成的AAR包中的SO体积明显偏大。
详细描述可以参见这个ISSUE: https://code.google.com/p/android/issues/detail?id=222831
找到原因后这个问题就很好解决了,可以手动的在链接选项中加入 strip参数,配置如下所示
1 | SET_TARGET_PROPERTIES(native-lib PROPERTIES LINK_FLAGS "-Wl,--gc-sections,--icf=safe,-s") |
也可以手动执行ndk提供的aarch64-linux-android-strip命令移除动态库中的调试信息。
设置编译器的优化flag
编译器有个优化flag可以设置,分别是-Os(体积最小),-O3(性能最优)等。这里将编译器的优化flag设置为-Os,以便减少体积。
Android.mk:
1
2LOCAL_CPPFLAGS += -Os
LOCAL_CFLAGS += -Os
CMakeLists:
1 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Os") |
使用 gc-sections去除没有用到的函数
有些时候代码量比较大的时候我们没办法手动发现无用的函数,这个时候可以可以开启编译器的gc-sections选项,让编译器自动的帮你做到这一点。
配置如下:
Android.mk:
1
2
3LOCAL_CPPFLAGS += -ffunction-sections -fdata-sections
LOCAL_CFLAGS += -ffunction-sections -fdata-sections
LOCAL_LDFLAGS += -Wl,--gc-sections
CMakeLists:
1 | # 去除未使用函数与变量 |
去除冗余代码
在NDK中,链接器还有一个选项 “-icf = safe”,可以用于去除代码中的冗余代码。但是要注意的是,这个选项也有可能去除定义好的inline函数,这里必须要做好权衡。
配置如下:
Android.mk:
1
LOCAL_LDFLAGS += -Wl,--gc-sections,--icf=safe
CMakeLists:
1 | SET_TARGET_PROPERTIES(native-lib PROPERTIES LINK_FLAGS "-Wl,--gc-sections,--icf=safe") |
设置编译器的 Visibility Feature
Visibility Feature就是用来控制在哪些函数可以在符号表中被输入,由于C++并不是完全面向对象的,非类的方法并没有public这种修饰符,因此,要用Visibility Feature来控制哪些函数可以被外部调用。
而JNI提供了一个宏-JNIEXPORT来控制这点。所以只要对函数加上这个宏,像这样:
1 | // JNIEXPORT就是控制可见的宏 |
然后在编译器的FLAGS选项开启 -fvisibility = hidden 就可以。这样,不仅可以控制函数的可见性,并且可以减少包体的大小。
CMakeLists:
1 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fvisibility=hidden") |
去除C++代码中的iostream相关代码
使用STL中的iostream相关库会明显的增加包的体积,而Android本身是有预编译库(android/log.h)可以代替输入到控制台的工具的。在我们的SDK中由于之前是控制台程序所以用到了输入输出,编译的时候没有把这块排除出去,造成了一定的体积冗余。
1 | /代替所有的iostream库里函数 |
删除无用模块
删除用不到的模块是包体积优化空间最大最快的
平台能力替代第三方库
即使是平台JNI层不支持,也可以单独依赖一个特定功能的库,而不是庞大的三方库。对于整个包体积来说,第三方模块往往相对来说是比较大的。
总结
本文介绍了在NDK开发中,要缩减SO包体积,有助于动态库体积优化的方法。